HashMap深度解析

简介:

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 HashTable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)

类图结构:

在这里插入图片描述
特性:

1 HashMap 允许key,value都为空。
2 HashMap 在操作键值对时不是线程安全的。
3 HashMap 底层以数组+单链表(节点数大于8则,单链表转红黑树)存储键值对。
4 HashMap实际就是 Node<K,V>[] table 数组,Node<K,V>节点实现了Map.Entry<K,V>。

主要方法:

1 put 方法详解:

  public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    // 根据key计算hash值
   static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
// 根据hash值,将键值对存放到HashMap中
 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 判断Node数组是否为空,空的就需要对数组初始化(resize 中会判断是否进行初始化)。
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // (n - 1) & hash]计算数组下标,如果数组下标所在位置的节点为空(为空表明没有 Hash 冲突)就直接在当前位置创建一个新Node节点即可。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 如果当前桶(数组下标)有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e
            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);
            // 如果是个链表,就需要将当前的 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;
                }
            }
            // 如果存在相同的 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;
    }

根据key计算数组下标的步骤如下:
(1)h = key.hashCode():根据key值计算hashCode。这里的 hashCode() 是一个 native 方法,根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。
(2)h >>> 16:将得到的hashCode无符号右移16位,低位移出(舍弃),高位的空位补符号位,即正数补0,负数补1。
(3 )(h = key.hashCode()) ^ (h >>> 16):h与h右移16位进行异或运算(两者相等为0,不等为1)得到hash值。
(4)计算(n - 1) & hash 的值即为数组下标。n代表数组大小,初始化时默认是16。

在这里插入图片描述

2 get 方法详解:

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;
    // 根据key和hash定位到数组下标
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 。
        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;
}

3 resize 方法详解:

// 初始化或加倍数组大小 : 如果为null,则分配初始容量。否则,进行2次幂扩展。
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
     // 旧的数组容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 旧的扩容阈值
    int oldThr = threshold; 
    // 声明新的数组容量和扩容阈值
    int newCap, newThr = 0; 
    // 旧的数组容量大于0说明本次是扩容操作
    if (oldCap > 0) { 
    // 扩容前的数组大小如果已经达到最大(2^30)了
        if (oldCap >= MAXIMUM_CAPACITY) { 
         // 修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 扩容:oldCap*2,即把新的数组容量 "newCap" 扩大2倍   
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 新的扩容阈值同样扩大2倍
            newThr = oldThr << 1; 
    }
    // 当 oldThr > 0 就说明用户调用了有参构造方法(指定了初始容量,并被构造方法 "缓存" 到了threshold中了)
    else if (oldThr > 0) 
        newCap = oldThr;
        // 初始化的数组容量为缺省的 16,初始化的扩容阈值为缺省的 16 * 0.75
    else {
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
     // 第一次resize()时,计算扩容阈值
    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];
    table = newTab;
   //不是第一次reizie()时
    if (oldTab != null) {
        // 遍历之前的table,重新hash排序
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
            // 删除了旧节点的引用
                oldTab[j] = null; 
                // 当桶里只有一个节点而没有链表,重新计算索引位置落桶
                if (e.next == null) 
                // 注意:扩容前的元素地址为 (oldCap - 1) & e.hash ,所以这里的新的地址只有两种可能,一是地址不变,二是变为 老位置+oldCap
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 创建两条链表, loHead是用来保存新链表上的头元素的,loTail是用来保存尾元素的,直到遍历完链表。
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                  
                      //  这里如果判断成立,那么该元素的地址在新的数组中就不会改变。因为oldCap的最高位的1,在e.hash对应的位上为0,所以扩容后得到的地址是一样的,位置不会改变 ,在后面的代码的执行中会放到loHead中去,最后赋值给newTab[j];
                        if ((e.hash & oldCap) == 0) { 
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                       // 如果判断不成立,那么该元素的地址变为 原下标位置+oldCap,也就是lodCap最高位的1,在e.hash对应的位置上也为1,所以扩容后的地址改变了,在后面的代码中会放到hiHead中,最后赋值给newTab[j + oldCap]
                        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;
}

上述 (e.hash & oldCap) == 0 即可将原桶中的数据分成2类:元素的位置要么是在原位置,要么是“j + oldCap”(当前位置索引+原容量的值)。这也是resize方法扩容后为什么是原来的2倍的原因。

JDK1.7中,resize时,index取得时,全部采用重新hash的方式进行了。JDK1.8对这个进行了改善:

以前要确定index的时候用的是(e.hash & oldCap-1),是取模取余,而这里用到的是(e.hash & oldCap),它有两种结果,一个是0,一个是oldCap,比如oldCap=8,hash是3,11,19,27时,(e.hash & oldCap)的结果是0,8,0,8,这样3,19组成新的链表,index为3;而11,27组成新的链表,新分配的index为3+8;
JDK1.7中重写hash是(e.hash & newCap-1),也就是3,11,19,27对16取余,也是3,11,3,11,和上面的结果一样,但是index为3的链表是19,3,index为3+8的链表是27,11,也就是说1.7中经过resize后数据的顺序变成了倒叙,而JDK1.8没有改变顺序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值