HashMap 源码解析

500 阅读2分钟

HashMap 源码解析:

扰动函数:

/**
* 1,h>>>16,右移16位,将原来高位移动到低位
* 2. h ^ (h>>>16) 异或原来的高位和低位,增加水“随机性”,减少hash碰撞,数据分配均匀
*/
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

使用扰动函数就是为了增加随机性,让数据元素更加均衡的散列,减少碰撞。

初始化容量和负载因子

初始化容量

  1. 初始化容器会寻找2的倍数的最小值,最大容量MAXIMUM_CAPACITY为1<<30 (2^30)
  2. jdk8使用按位或(|)的方法,先-1,在|=向右移位1、2、4、8、16位,最后+1
/**
 * 1. 初始化容器会寻找2的倍数的最小值,最大容量MAXIMUM_CAPACITY为1<<30 (2^30)
 * @param initialCapacity 初始化容量
 * @param loadFactor 负载因子
 */
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);
}

/**
* jdk8和jdk11的实现不同:
* jdk11使用Integer中numberOfLeadingZeros方法
*/
static final int tableSizeFor(int cap) {
    int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

/**
* jdk8使用按位或(|)
*/
static final int tableSizeFor(int cap) {
    // 先减1
    int n = cap - 1;
    // 向右移位1、2、4、8、16位,把二进制的各个位置都填1
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    // 当各个位置都是1以后,就是一个标准的2倍数减1,然后结果+1返回
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
public final class Integer extends Number implements Comparable<Integer> {
    public static int numberOfLeadingZeros(int i) {
        // HD, Count leading 0's
        if (i <= 0)
            return i == 0 ? 32 : 0;
        int n = 31;
        if (i >= 1 << 16) { n -= 16; i >>>= 16; }
        if (i >= 1 <<  8) { n -=  8; i >>>=  8; }
        if (i >= 1 <<  4) { n -=  4; i >>>=  4; }
        if (i >= 1 <<  2) { n -=  2; i >>>=  2; }
        return n - (i >>> 1);
    }
}

负载因子

默认0.75f, 负载因子决定数据量多少以后进行扩容,如果希望用更多的空间换取时间,可以把负载因子调的更小一些,减少碰撞。

static final float DEFAULT_LOAD_FACTOR = 0.75f;

扩容元素拆分

  1. 原hash和扩容新增的长度,进行与(&)运算,如果值为0,则下标不变;如果不为0,那么在原来的下标的基础上加新增的长度;

  2. 这样的好处就是不用计算每一个数组中的元素的哈希值。

  3. 扩容时计算出新的 newCap、newThr,这是两个单词的缩写,一个是 Capacity ,另一个是阀 Threshold

  4. newCap 用于创新的数组桶 new Node[newCap];

  5. 随着扩容后,原来那些因为哈希碰撞,存放成链表和红黑树的元素,都需要进行拆分存放到新的位置中。

链表树化

  1. 链表树化的条件有两点;链表长度大于等于 8、桶容量大于 64,否则只是扩容,不会树化。
  2. 链表树化的过程中是先由链表转换为树节点,此时的树可能不是一颗平衡树。同时在树转换过程中会记录链表的顺序,tl.next = p,这主要方便后续树转链表和拆分更方便。
  3. 链表转换成树完成后,在进行红黑树的转换。先简单介绍下,红黑树的转换需要染色和旋转,以及比对大小。在比较元素的大小中,有一个比较有意思的方法,tieBreakOrder 加时赛,这主要是因为 HashMap 没有像 TreeMap 那样本身就有 Comparator 的实现。