JDK 1.8 HashMap源码解析

JDK 1.8 HashMap源码解析

1.8的HashMap较之前的版本有所变更,结构由原先的数组+单向链表 变成 数组+单向链表+红黑树,如图:
这里写图片描述
当hash冲突节点数达到8个的时候,就会将单向链表转成红黑树。
具体我们来看源码:

成员变量

默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
默认填装因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
将链表转化成红黑树的链表元素数量阈值
static final int TREEIFY_THRESHOLD = 8;
进行remove操作的时候,将红黑树转成链表的链表元素数量阈值
static final int UNTREEIFY_THRESHOLD = 6;
将链表转化成红黑树的数组元素数量阈值
static final int MIN_TREEIFY_CAPACITY = 64;
数组大小,2的幂次倍
transient Node<k,v>[] table; 
存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
存放元素的个数,注意这个不等于数组的长度。
transient int size;
每次扩容和更改map结构的计数器
transient int modCount;   
扩容阈值
int threshold;
填充因子
final float loadFactor;

内部结构

链表节点。持有key的hash值,key,value以及下一个节点的引用。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    ...
}

构造方法

构造方法比较简单,会调用tableSizeFor来给扩容阈值赋值

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;
    this.threshold = tableSizeFor(initialCapacity);
}

静态方法

获得hash值,这里不是直接使用Object本身的hashCode方法,而是取hashCode高16位和低16位进行异或运算。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

返回大于入参的最小2的幂次倍,>>> 表示无符号位移,高位取0。 比如传入9,返回 2^4 = 16。

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

核心方法

这里先大概讲下put的处理逻辑:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断数组table是否初始化过
    if ((tab = table) == null || (n = tab.length) == 0)
        //进行初始化
        n = (tab = resize()).length;
    //(n - 1) & hash 得到的数组索引是否存在引用
    if ((p = tab[i = (n - 1) & hash]) == null)
        //新建node节点
        tab[i] = newNode(hash, key, value, null);
    //存在key相同或hash碰撞
    else {
        Node<K,V> e; K k;
        //key的相同
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //将node p 的引用赋值给e
            e = p;
        //hash 碰撞 且 碰撞节点的结构为红黑树
        else if (p instanceof TreeNode)
            //将key放入红黑树结构中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //hash 碰撞 且 碰撞节点的结构为单向链表
        else {
            for (int binCount = 0; ; ++binCount) {
                //遍历链表到末端
                if ((e = p.next) == null) {
                    //新建node节点
                    p.next = newNode(hash, key, value, null);
                    //如果链表的节点数>8且数组大小大于等于64则将链表转成红黑树(这里>=7是因为从链表的第二个元素开始遍历)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //可能存在跟链表结构中的node的key相同则结束循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //更新循环条件
                p = e;
            }
        }
        //如果存在对应key的映射值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            //不是调用onlyIfAbsentPut方法,且oldValue不为空,就将新值赋给找到的引用
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //hashmap中空操作,在linkhashmap中开放的回调方法
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //数组大小超过阈值则扩容
    if (++size > threshold)
        resize();
    //hashmap扩展点,在linkhashmap中开放的回调方法
    afterNodeInsertion(evict);
    return null;
}

然后看一下第二个if中不起眼的判断条件,其实看了前面的hash()和resizeTableFor()两个方法,我心里就一直有疑问,为什么要对hashCode进行高低位异或运算,以及tableSize必须是2的幂次倍?答案就在下面的这行代码里。其中n是数组长度即resizeTableFor()的返回值,2^x 。hash是hash()的返回值。这里做了一个与运算,结果就是即将落到数组中的索引。
关于n,这里举两个例子方便大家理解:

  • n = 64 即 01000000 , n - 1 为 00111111.
    根据与运算的规则,其结果范围为00000000-00111111,即0-15.是不是立马就想到了取模,即hash % (n - 1),对,其实 n = 2^x 的时候,它俩作用是等价的。(原谅我之前都只会用%)。
    他两虽然等价,但不等效,位运算的性能毋庸置疑,且+-*/%中%的性能是最低的。

    (p = tab[i = (n - 1) & hash]) == null
    
  • n = 65 即 01000001 ,n - 1 为 01000000.
    其结果仅可能为01000000或00000000,即0或64.所以相当一部分的空间都将被浪费,且极易出现碰撞。

关于hash,因为要跟n-1进行与运算,一般情况下,n都不会太大,造成hash的高位都没有参与到计算(n不大的时候高位基本都是0),即使hash的高位不同,但低位相同还是会被映射到同一个索引下。所以在使用取得native的hashCode之上又对低位进行和高位的异或运算,使高位也参与到运算中。(这里保留一个疑问,即便这么做可以避免高位不同,低位相同的碰撞。但也能导致新的碰撞可能,即,高位不同,低位也不同,异或后却相同。所以目前我还是不知道这么做是否真的可以达到降低碰撞的目的)。

综合的来看,整体权衡设计的很巧妙。

然后我们再来看下HashMap是怎么实现红黑树的,

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //判断table是否达到树化的阈值
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        //将Node节点转成持有前后引用的TreeNode(还没完全树化)
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
                //树化
            hd.treeify(tab);
    }
}


/**
 * Forms tree of the nodes linked from this     node.
 * @return root of tree
 */
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
    next = (TreeNode<K,V>)x.next;
    x.left = x.right = null;
    //处理树头
    if (root == null) {
        x.parent = null;
        x.red = false;
        root = x;
    }
    else {
        K k = x.key;
        int h = x.hash;
        Class<?> kc = null;
        for (TreeNode<K,V> p = root;;) {
            int dir, ph;
            K pk = p.key;
            //比较 hashCode() 方法的值,一样的话比较compare方法的值,如果还一样就比较native hashcode() 方法的值
            if ((ph = p.hash) > h)
                dir = -1;
            else if (ph < h)
                dir = 1;
            else if ((kc == null &&
                      (kc = comparableClassFor(k)) == null) ||
                     (dir = compareComparables(kc, k, pk)) == 0)
                dir = tieBreakOrder(k, pk);

            TreeNode<K,V> xp = p;
            //根据上述比较结果,将节点放置在父节点的左右两侧
            if ((p = (dir <= 0) ? p.left : p.right) == null) {
                x.parent = xp;
                if (dir <= 0)
                    xp.left = x;
                else
                    xp.right = x;

                //没插入一个节点都有可能破坏红黑树的结构,所以插入之后进行旋转平衡操作
                root = balanceInsertion(root, x);
                break;
            }
        }
    }
}
moveRootToFront(tab, root);

}

猜你喜欢

转载自blog.csdn.net/qq250782929/article/details/81162729