HashMap 笔记整理

主要参考文章:
1、Java7/8中的HashMap和ConcurrentHashMap源码分析,看了都说好
2、源码分析之HashMap的红黑树实现
3、Java 集合深入理解(17):HashMap 在 JDK 1.8 后新增的红黑树结构
4、2.1.4. Java集合——HashMap

对于 HashMap,主要的几个问题。

1、解决哈希冲突的方法
2、为什么数组的长度是 2n
3、扩容机制的实现


这里只记录关键的源码相关的内容。基于 Java8

首先回答 问题 1

HashMap 中解决哈希冲突的方法就是使用 数组+链表 的结构(Java8 中的红黑树暂时忽略),数组的每个元素存储的是链表的头节点,而链表是由同一类 key-value 包装成节点组成的。

同一类的概念是指利用 key 的 hash 值映射为数组下标 index 时,多个不同的 key 映射的结果都为同一 index,此时,就把这多个 key 对应的节点组成一个链表,存放于数组[index] 中。

这样做的根本目的,也是为了加快查找速度。

而结合上文, 问题 2 也可以得到解答。

数组的长度是 2n,就是为了结合位运算提高 key 的映射效率,因为当数组长度 length 为 2 的 n 次方时,对于 key 的 hash 值,有 hash%length == hash&(length-1)

当 length 是 2n 时,此时(length - 1) 的二进制全是1, hash & (length -1) 相当于取 hash值的低 n 位( hash 值低 n 位组成的值 x 肯定是小于 2n 的,而此时 x % 2n 则就是 x 本身), 结果和 hash%length 一样的。

然后先看下几个关键的方法:


capacity:当前数组容量,始终保持2^n,可以扩容,扩容后的大小为当前的2倍,默认为16。

loadFactor:负载因子,默认为 0.75。

threshold:扩容的阈值,等于 capatity * loadFactor。

首先是其中一个构造方法:

public HashMap(int initialCapacity, float loadFactor) {
    ...
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

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;
}

这个构造方法是可以指定初始化数组的大小的,但是实际上并不是直接用该 initialCapacity 值,而是使用与 initialCapacity 最接近的且是 2n 的值。且这里不会立即初始化数组。


然后是 put() 方法:

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;
    
    // 如果数组为空或者长度为 0,则先根据 capacity 初始化对应大小的数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
        
    // (n - 1) & hash 即将对应的 hash 值映射到数组对应的下标 i
    // 如果 tab[i] 为 null,则表示该 key-value 还不存在于 map 中,是新的组合,
    //	则直接存放在 tab[i] 即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 否则,就表示下标 i 处已经有了多个节点,因此需要对这些节点进行比较
    // 就会有几种情况:
    //		1、这些节点中的某一节点的 key 值与当前 key 一致,
    //			则需要用当前 value 覆盖节点的原 value 值
    //		2、这些节点中不存在与当前 key 值相等的节点,
    //			因此当前 key-value 同样也是新的组合,需要添加到 tab[i] 的节点中
    else {
        Node<K,V> e; 
        K k;
        // 如果 tab[i] 的首个节点 p 的 key 即与当前 key 相等,则该节点 p 就是目标节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果 tab[i] 的首个节点 p 不是目标节点,且 tab[i] 处的数个节点组成的是红黑树,
        // 则针对该红黑树进行遍历插入
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 走到这里,就表示 tab[i] 处数个节点组成的还是单链表,
            // 且需要遍历这个单链表
            for (int binCount = 0; ; ++binCount) {
                // 如果遍历完之后,还是没有找到目标节点的 key 与 当前 key 值相等的,
                // 则把当前 key-value 实例成一个新的节点插入到链表末尾
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 在新加了节点之后,如果该链表的节点数达到了某个值,则将链表转换为红黑树
                    // TREEIFY_THRESHOLD == 8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果在遍历的过程中找到了目标节点的 key 与 当前 key 值相等的,
                // 则此时即赋值为了 p
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // e 不为 null,则表示还没有遍历完就找到了目标节点 p,
        // 则将当前的 value 覆盖原有的 value 值即可
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 如果总的节点数大于了 threshold,则需要将数组扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

// 用于将单链表转换为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 如果数组大小 < MIN_TREEIFY_CAPACITY(64) 则不转换,而是将数组进行扩容
    // 因为扩容之后就减少该单链表的节点数,提高遍历查询的效率(红黑树也是为了提高查询效率)
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 否则,就真的将单链表转换为红黑树
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 将 tab[index] 处的节点(Node 类型的)转换为 TreeNode 类型的
        TreeNode<K,V> hd = null, tl = null;
        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);
        // 将转换成的 TreeNode 节点们构成红黑树
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

put() 方法的实际作用就将 key-value 组合存放到 map 中,但是此时 key 值可能已经存在于 map 中了,则只要更新 value 即可,否则就表示原来不存在,需要新增到 map 中。

在新增了之后,当新增节点的链表上的元素个数 >= 8 时且数组的长度为 64 时,则会将该链表转换为红黑树,以提高遍历查询的效率。而如果该链表上的元素个数 >= 8 时且数组的长度小于 64,则只会将数组扩容,而不触发转换红黑树的操作。

另外,当 map 存储的元素个数大于 threshold 时,也会触发扩容。


然后是 get(key) 方法:

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;
}

get() 方法则比较简单直观,先根据 key 的 hash 值得到对应的映射下标 index,然后在 tab[index] 对应的数个元素中去查找与 key 相等的节点即可。只不过找的时候分为在链表中找或者在红黑树中找两种情况。

以及扩容方法 resize()

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;// 原数组大小
    int oldThr = threshold; // 原扩容阀值
    int newCap, newThr = 0;
    if (oldCap > 0) {
    	 // 如果原数组大小已经达到了 MAXIMUM_CAPACITY (1<<30)
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;// 则将扩容阀值设置为 Integer.MAX_VALUE
            // 且直接返回原数组,不再进行扩容
            return oldTab;
        }
        // 否则新的数组大小为原大小的 2 倍且小于 MAXIMUM_CAPACITY
        // 且原数组大小 >= DEFAULT_INITIAL_CAPACITY(16)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // oldCap <= 0 且 oldThr > 0
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // oldCap <= 0 且 oldThr <= 0
    else {               // zero initial threshold signifies using defau
        newCap = DEFAULT_INITIAL_CAPACITY;
        // newThr = 0.75 * 16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPAC
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @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;
                // 如果 oldTab[j] 只有一个元素,
                // 则直接置于 newTab[e.hash & (newCap - 1)] 处
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果 oldTab[j] 处是红黑树
                // 并且元素个数小于链表还原阈值 UNTREEIFY_THRESHOLD(6),
                // 就会把桶中的树形结构缩小或者直接还原(切分)为链表结构
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 否则,就将 oldTab[j] 处的单链表分割为两个单链表
                // 一个置于 newTab[j],另一个置于 newTab[j + oldCap] = hiHead;
                else { // preserve order
                    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;
}

对于扩容机制的实现,请先记住一个前提,因为数组的长度 length 总是为 2n,因此 hash&(length-1)hash%length 等价。

然后用具有的实例,来讲解一下扩容的关键机制。

假设扩容之前,length = 16(24),则对于数组 tab, tab[5] 对应的元素的 hash 值有:

(5)   		 101 & 1111
(21=5+16)    10101 & 1111
(37=5+16*2) 100101 & 1111
(53=5+16*3) 110101 & 1111

length = 16*2 = 32 (25)时 (新数组长度为扩容前的两倍) ,有 tab[5] 对应的元素为 5、37,tab[5+16] 对应的元素为 21、53。

e.hash & oldCap ,即 hash & 24( 10000(2) ) ,是为了判断第高 5 位是否为 1:

即与 hash & (25-1) (11111(2)) 的效果一致,当 length = 16 时,取的是低 4 位,当为 32 后,就要取低 5 位了,因此第 5 位的值(0或者1) 就影响着在新数组中的位置。

  • 如果第 5 位为 1,则取低 5 的值比原来取的低 4 的值 5( 101(2) )多了 16( 因为第 5 位为 1,则有 10000(2) ), 即有取低 5 的值 10101(2)
  • 如果第 5 位为 0,则取低 5 的值与原来取的低 4 的值 5 一致,不需要变化,即有取低 5 的值 00101(2)

因此在源码中有:newTab[j] = loHead;newTab[j + oldCap] = hiHead;

这就是对 问题 3 扩容机制实现的关键点的理解。


另外,还有几个点需要补充下:

首先是 HashMap 中求 hash 值的算法:

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

如果 key 为空,则对应的哈希值直接为 0,否则就将 key 的 hashCode() 的值的高 16 位与低 16 位进行异或。

因为在映射到数组下标时,只取低 n 位,此时如果只是直接利用 hashCode() 的值的话,就会增大碰撞的几率,而将高低 16 位进行异或,就相当于将 hashCode() 的值的高位也参与了元算,就可以在一定程度上减少碰撞的几率,且因为是位元算,效率也较高。

补充: 哈希函数的构造方法 与 解决冲突的方法
https://blog.youkuaiyun.com/tian_110/article/details/43192595

以及 HashMap 中关于 hash 的几点冲突:
1、key 对应的对象的 hashcode() 方法不完善,导致不同的 key 生成相同的 hash 值,此时就需要借助 equals() 方法进行判断是否相等
2、不同 key 对应的 hash 值不同,但是映射的数组下标相同


以及,1.8 中与红黑树相关的内容,参见:

教你初步了解红黑树

Java 集合深入理解(17):HashMap 在 JDK 1.8 后新增的红黑树结构

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值