【集合框架】之深入分析HashMap

在前面的文章【Java集合框架总结】https://blog.csdn.net/moni_mm/article/details/80065576中对集合框架进行概要分析之后,在此结合JDK源码对HashMap进行深入分析。

提出并解决问题如下:

问题1:初始容量为什么是16,为什么必须是2的幂?

问题2: hash方法为什么是无符号右移16位?

问题3:

问题4:

问题5:


HashMap

  • 非线程安全
  • 继承于AbstractMap
  • 实现了Map、Cloneable、java.io.Serializable接口
  • 基于数组+链表+红黑树

重要对象

  • DEFAULT_INITIAL_CAPACITY :默认的初始容量是16,必须是2的幂
  • MAXIMUM_CAPACITY :最大容量,1<<30
  • table:Entry[]数组类型,每一个Entry本质上是一个单向链表
  • size:HashMap的大小,键值对的数量
  • threshold:HashMap的阈值,threshold=容量*加载因子
  • DEFAULT_LOAD_FACTOR:默认加载因子0.75
  • loadFactor:实际加载因子
  • modCount:HashMap被改变的次数,用来实现fail-fast机制
  • HashMap扩容时,将容量变为原来的2倍
  • TREEIFY_THRESHOLD :树形和列表的阈值,默认8
  • UNTREEIFY_THRESHOLD :树形转换回链式的阈值,默认6
  • MIN_TREEIFY_CAPACITY :哈希表的最小树形化容量,默认64

问题1:初始容量为什么是16,为什么必须是2的幂?

  • 指定为16是从性能考虑。避免重复计算
  • 数组的长度总是 2 的幂,使Hash散列更均匀
    HashMaphash函数通过 hash & (table.length - 1) 来得到该对象的index.仅与hash值的低n位有关
    因为使用的掩码是2的次幂,高于掩码的位组成的哈希集合总是冲突,所以把高位移到低位。
    混合原始哈希码的高位和低位,以此来加大低位的随机性。

构造方法

可指定初始容量和负载因子,或从其他Map初始化


核心方法

hash()
key的hash值高16位不变,低16位与高16位异或作为key的最终hash值

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

问题2: hash方法为什么是无符号右移16位?

设计者权衡了speed, utility, and quality

在JDK1.7中,使用四次移位

static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

tableSizeFor()

返回一个比给定整数大且最接近的2的幂次方整数

问题3: tableSizeFor如何实现?

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

先取n-1,五次右移后做运算
超过最大值取最大值,否则取n-1
作为阈值threshold

  • 第一次右移1位:最高位前2位置1
  • 第二次右移2位:最高位前4位置1
  • 第三次右移4位:最高位前8位置1
  • 第四次右移8位:最高位前16位置1
  • 第五次右移8位:最高位前32位置1

put

通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点.

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;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            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) {
                    // 插入到链表的最后面(Java7 是插入到链表的最前面)
                    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(== 或 equals)
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //e!=null 说明存在旧值的key与要插入的key"相等"
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //判断阈值,决定是否扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

具体过程

如果table为空或大小为0,触发resize,用默认值初始化
(n - 1) & hash找到buckets,为空则在此插入

if ((tab = table) == null || (n = tab.length) == 0)
      n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
      tab[i] = newNode(hash, key, value, null);

如果产生碰撞,取出该节点p

如果hash相等且(key ==equal),即就是要找的节点e

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

如果不是要找的节点,就判断p是红黑是还是链表节点,调用不同的插入方法

如果插入后超过8个,会触发 treeifyBin将链表转换为红黑树
如果在该链表中找到了相等的 key(== 或 equals)

如果e!=null 说明存在旧值的key与要插入的key 相等
进行 “值覆盖”,然后返回旧值

判断阈值,决定是否扩容


未完待续


猜你喜欢

转载自blog.csdn.net/moni_mm/article/details/80722194