HashMap部分源码

51 阅读7分钟

HashMap

引言

以下为我为HashMap翻译的引用

HashMap允许value为null和key为null。但是key为null只能有一个。它不保证元素的顺序。

假设hash函数将数据均匀的分布在桶中,get和put的时间复杂度为O(1)。

只有initial capacity和 load factor会影响一个HashMap实例的性能。

如果有很多映射将要存进HashMap,那么创建一个初始带有大容量的将会使映射的存储更加有效率,比它自己rehash来增加表的大小更有效率。

这是线程不安全的。

当桶内的结点多于TREEIFY_THRESHOLD(8)时,会从链表转换为树结构(红黑树),红黑树在结点较多的情况下可以加快搜索速度,但是存储空间增大。一般不会发生转换,这个概率太小了。当树结构退化为链表时,需要结点小于等于UNTREEIFY_THRESHOLD(6)。

新人第一次看源码,有错误或者增加的地方,还望指出,谢谢大家。

初始化

先了解一个函数

// 返回一个大于等于cap的2^n。例如cap = 3 ,return 4。
static final int tableSizeFor(int cap) {
    // 这一步很重要,确保想8这种2的n次幂的,不会翻倍。
    // 如果cap = 8,没有cap - 1,那么最后结果会出现16。
    int n = cap - 1;
    // 下面就都是移位了,将最高位的1后面都填充1。
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    // 最后判断条件,n + 1是让结果回归2的次幂。
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

各个初始化函数

// 指定初始大小和负载因子的构造方法。
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    // 这里会确保initialCapacity变为2的n次幂
    // threshold阈值,HashMap进行reHash的阈值
    this.threshold = tableSizeFor(initialCapacity);
}
​
// 只有初始大小的构造方法,默认负载因子
// DEFAULT_LOAD_FACTO = 0.75f
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 负载因子,注意这里没有出现初始大小赋值,这会在另一个方法中出现。
// 提一句,默认大小是16。
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
​
// 这个是将其他Map转换成HashMap,看一下这个putMapEntries方法
public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
}
​
// resize()和putVal()方法等下解析
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        // m的大小小于等于0,就啥也不用做了,就是空map。
        if (s > 0) {
            // table == null就是未初始化的时候
            if (table == null) { // pre-size
                // 计算需要的大小
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            // table != null && s > threshold这就说明新加入的map容量大于阈值了,需要重新计算size了。
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

resize

// 作用:初始化table size或者翻倍。
// 如果table是空的,那么久初始化一个
// 否则,因为我们的大小使用的是2的幂,这些元素要么在原地,或者移到两倍的地方。
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    // 检查table是否初始化
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 原来的table太大了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // table未初始化,且旧阈值大于0(记住阈值的设定在第一个、第二个和第四个构造函数中设定过,其他的都没有设定过)
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 默认初始化(只有第三种构造,才会触发这里的逻辑)
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 计算默认阈值
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 这里会计算新的阈值(记住阈值的设定在第一个、第二个和第四个构造函数中设定过,其他的都没有设定过,也就是说有自定义的负载因子)
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 下面的操作就是,将扩大的hashMap中的元素重新哈希。
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    // 注意这个e.hash & (newCap - 1)
                    // 这里会确定元素保留在原位还是移动旧Cap大小的位置
                    // 例如元素hash为7 0x0111 旧cap为 4 0x0100那么计算出来的位置就是 0x0011 3
                    // 新cap是旧cap的两倍,8 0x1000 那么计算出来的位置就是 0x0111 7 增加了旧cap 4大小的位置
                    // 核心目标是通过位运算和结构拆分(链表/树),高效地将旧数组中的元素分配到新数组中。其设计充分利用了哈希表的容量特性(2 的幂次方),避免了重复计算哈希值,同时保持了数据结构的有序性和性能平衡。
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 尾插法可以避免多线程死循环,但还是线程不安全。
                    // jdk1.7 是头插法,会陷入多线程死循环
                    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) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

理一下核心逻辑

(1)先检查当前Map是否初始化(即是否存在元素)

(2)初始化过的(size > 0),将阈值变为原来的两倍或者Integer.MAX_VALUE

(3)未初始化过的,就要分两种情况了,有初始阈值(这里阈值都是2的幂,你会问这里为啥不是乘法的值,接下来会初始化这种阈值)的,直接将阈值赋给容量。否则赋默认初始大小(16),计算默认负载因子(0.75) * 默认初始大小(16)。

(4)为(3)中的第一种情况计算新的阈值

(5)将扩大的hashMap中的元素重新哈希。

长度使用2的幂的原因猜想:

省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了(引用了tech.meituan.com/2016/06/24/…)。那么就有机会将长的链表或者树的长度减少,这样更有利于搜索。

get

// 这个方法会返回key的映射value或者不存在,返回null。
// 当然这个方法返回null有两种可能,key不存在或者value存储的就是null。
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
​
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                // 分两种情况搜索
                // 第一种是已经进化成树结点了
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 第二种是 还是链表结点,那就一个个找。
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

put

// 装入指定的key和指定的value。如果map中已经存在对应的key了,那么旧value将会被替换掉
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
​
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 如果没有初始化,那就先进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 如果当前桶是空的,那就直接new一个node插入就行了。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 否则就要看是否存在key,存在就直接找到修改旧值就行了,否则执行插入
        else {
            Node<K,V> e; K k;
            // 第一个判断,是否存在key,这里只是判断了第一个结点,剩下的结点也要判断,在下面的两个逻辑里判断
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 树的插入
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 链表的插入
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 插入前判断是否会进化成树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 继续判断是否存在key。
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 修改旧值,最后返回旧值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 插入之后,发现大于阈值了,那就需要resize。
        if (++size > threshold)
            resize();
        // 这个函数我不知道干啥的
        afterNodeInsertion(evict);
        return null;
    }