HashMap详解(基于JDK 1.8)

时间:2020-06-30 22:57:20   收藏:0   阅读:91

HashMap详解(基于JDK 1.8)

简介


内部实现

存储结构

结点Node<K,V>的属性:

final int hash; // 对应的哈希值
final K key; // 键
V value; // 值
Node<K,V> next; // 指向下一个结点的指针

存储方式

HashMap的一些字段


具体方法(功能实现)

确定哈希桶数组索引位置

put方法流程

  1. 判断哈希桶数组table是否为空或null,若是,则resize()进行扩容。
  2. 根据键key计算hash值,得到待插入的数组索引i。若为空,则直接插入。并跳转至步骤6;若非空,则向下执行。
  3. 判断哈希桶数组中table[i]的首个元素的key是否与待插入的key一致,若相同,则直接覆盖,否则向下执行。
  4. 判断table[i]是否为treeNode(即判断待插入的位置是使用红黑树还是链表保存键值对),若是红黑树,则在树中插入键值对。
  5. 若不是红黑树,则判断链表长度是否大于8,若大于,则将链表转为红黑树,在树中插入键值对。否则在链表中插入键值对。(遍历链表时,若发现key重复,则直接覆盖,从而完成插入)
  6. 插入成功后,判断实际存在的键值对数量size是否超过最大容量threshold,若超过,则进行扩容。

注:

扩容机制

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length; // 获取旧桶的大小
        int oldThr = threshold; // 获得旧的threshold
        int newCap, newThr = 0;
        if (oldCap > 0) { // 若旧桶的大小大于0
            if (oldCap >= MAXIMUM_CAPACITY) { // 若旧的threshold大于2^29,则直接设为最大值,并返回
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY) // 否则,若旧的threshold大于16,而加倍后仍然小于2^29
                newThr = oldThr << 1; // double threshold // 则加倍
        }
        else if (oldThr > 0) // 若旧的threshold大于0,则threshold不进行改变
            newCap = oldThr;
        else {               // 以上条件都不满足,则桶的大小设为16,threshold设为12
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) { // 计算新的桶大小对应的新的threshold
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr; // 设置threshold,使之生效
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab; // 创建新桶
        if (oldTab != null) { // 若旧的桶非空,则需要将其中的键值对迁移到新桶中
            for (int j = 0; j < oldCap; ++j) { // 对于哈希桶数组中的每个元素进行检查
                Node<K,V> e;
                if ((e = oldTab[j]) != null) { // 若数组当前位置有键值对,则需要处理,否则直接到一个数组元素
                    oldTab[j] = null; //旧桶设为null,方便GC
                    if (e.next == null) // 若当前位置上只有一个元素,则直接将其迁移到新数组的新位置(经过hash计算)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode) // 若当前位置是以红黑树方式存储键值对,则对红黑树分裂
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // 若是链表,则将旧的链表分裂成两个新的链表
                        Node<K,V> loHead = null, loTail = null; // 此链表的键值存放在原来的位置
                        Node<K,V> hiHead = null, hiTail = null; // 此链表的键值对放在新位置(旧的位置+旧的桶大小)
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) { // 存放在旧位置上的键值对
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else { // 存放在新位置上的键值对
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) { // 分裂的链表1放在旧的位置上
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) { // 分裂的链表2放在新的位置上
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

JDK 1.8在扩容机制上的关键点:

遍历方式

两种遍历HashMap的方式:

// 第一种方式:将key 和 value 同时取出
Iterator<Map.Entry<String,Integer>> entryIterator = map.entrySet().iterator();
while(entryIterator.hasNext()){
    Map.Entry<String,Integer> next = entryIterator.next();
}

// 第二种方式:将key取出,若要value,还需通过key遍历一遍
Iterator<String> iterator = map.keySet().iterator();
while(iterator.hasNext()){
    String key = iterator.next();
}

JDK1.8中的HashMap


线程安全性


小结

  1. 扩容是一个特别耗性能的操作,所以当使用HashMap,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
  2. 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
  3. HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。
  4. JDK1.8引入红黑树大程度优化了HashMap的性能。

参考:

原文:https://www.cnblogs.com/truestoriesavici01/p/13216222.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!