源码详解JDK7与JDK8中的HashMap

简介: 源码详解JDK7与JDK8中的HashMap

   HashMap作为Java中的重要的数据结构,不仅在平常工作中被大量使用,并且在企业面试中也是处于必问的重要角色,今天带大家从源码角度再次重新认识一下我们常用但陌生的HashMap。

       在从JDK7转化为JDK8时,HashMap的实现也发生了很大的改变,先来看一下它们的区别:

1.JDK7 中使用数组+链表,JDk8 中使用数组+链表+红黑树实现

2.新节点在插入到链表时插入的顺序不同(JDK7插入在头节点,JDK8插入在尾节点)

3.HASH算法有所简化

4.扩容机制有优化

       首先看存储结构,如果大家对红黑树比较陌生,可以先自行查看完全平衡二叉树(AVL)和红黑树的相关知识,篇幅有限不再赘述。这里只列出红黑树的一些性能特点:

1.调整规则没有完全平衡二叉树严格

2.插入效率比链表低,但查询效率比链表高

3. 红黑树查询效率介于链表和完全平衡二叉树之间,折中

先看看JDK7:

在HashMap中,我们最常用的操作大概就是put与get了,但是你真的了解他们的实现原理吗?

看看put操作的核心代码:

int hash=hash(key);
int i=indexFor(hash,table.length);
table[i]=newNode;

大体来说,包含三个操作:

1.计算传入key的hash值

2.通过indexFor方法计算下标值

3.将newNode加入链表,放进对应的数组下标中


注意,第三部我们直接将newNode赋值给table[i],也就是说把节点插入在了头结点上,而将原先的链表直接连在我们新插入的节点后面。

顺带一提,HashMap中支持key为null,数组第0个位置存放key=null的元素,只能有一个key=null的元素,第0个位置不存在数组。


而get操作就比较简单了,先找到数组的下标,再比较key是否和给定的key相同,不同则顺着链表找下一个,直到找到或为空。

(通过getEntry()方法,遍历比较hash值是否相等,比较key是否相等)

//key不为null,获取value
final Entry<K,V> getEntry(Object key) {
        if (size == 0) {//判断链表中是否有值
         //链表中没值,也就是没有value
            return null;
        }
       //链表中有值,获取key的hash值
        int hash = (key == null) ? 0 : hash(key);
        // 在“该hash值对应的链表”上查找“键值等于key”的元素
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //判断key是否相同
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;//key相等,返回相应的value
             }
        return null;//链表中没有相应的key
    }

既然被称为HashMap,那么计算hash值肯定是必要的一环,先看看JDK7中HashMap的hash方法:

final int hash(Object k){
    int h= hashSeed;
    if(0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String)k);
    }
    h ^= k.hashCode();
    h^= ( h>>>20) ^ (h>>>12);
    return h^ (h>>>7) ^(h>>>4);
}

在这当中,暂且不看hash种子,我们可以看到计算中存在大量的右移操作,那么为什么要进行右移呢,这是考虑到了碰撞性问题

之前提到使用indexFor来计算数组下标:

static int indexFor(int h , int length){    
    return h & (length-1);
}

这里提一点,HashMap的长度一定是一个二的次方数,这点是在它的初始化和扩容中被限定的。这里在计算下表时,一个二的次方数减去1,能够保证它的二进制数的后几位数字全部是1,便于计算下标。


举个例子,HashMap长度为16,这样计算出的hash值与0000 1111做与运算,只需要取后四位,就实现了数组下标的计算。而与操作的计算速度比取余操作是要快上一些的。

回到上面,继续讲为什么要进行大量的右移操作,还是以长度为16来看,如果几个key计算出的hash值为:

1010 0110
0010 0110
0000 0110

我们发现,只要后四位一样,hash值都一样,碰撞性很高,所以这时要引入右移操作,让高位也能参与到与运算。让链表分散,减少链表长度。

再看JDK8:

jdk8中引入了红黑树,但并不是说链表并不存在,查阅源码,我们可以发现两个非常关键的值:

static final int TREEIFT_THRESHOLD=8;
static final int UNTREEIFT_THRESHOLD=6;

当链表的元素超过8时,会自动转成红黑树;当红黑树的节点数小于6时,变回链表。

看看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;
    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;
        //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);
        //是链表结构 
       else {
            for (int binCount = 0; ; ++binCount) { //binCount是遍历链表过程中计数
                //遍历链表,循环到尾结点,把新元素加在尾部,break
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //判断是否大于变成树的阈值-1,7会变成8,变成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //找到相等元素也break
                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;
}

看完这段代码,我们就明白了为什么jdk8中使用的是尾插法。因为在判断是使用链表或红黑树的过程中,要判断是否超过8个元素,至少需要遍历一遍,所以使用尾插法,新元素可以直接插入在尾结点。

get方法大体思路不变,计算下标,然后遍历,只不过是比jdk7中多加上一个判断是试用链表存储还是红黑树存储的步骤。


而jdk8也精简了它的hash算法:

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

只右移16位,精简了hash算法,配合红黑树提高查询效率。

最后看一下两者的扩容操作,扩容是对数组进行扩容,而不是链表或红黑树。我们在初始化时数组的默认长度是16,前面提到当存储的元素很多时会发生hash碰撞。我们扩容的目的是将长链表的长度减短,提高查询效率。

由于扩容的源码比较长,就不贴在这里,只列出核心思想:

jdk7中:

只有当数量大于阈值,且当前插入位置不为空时才会进行扩容,并且容量为原先2倍。

在将老的table转移到新的table时,需要重新计算数组的下标。

扩容后,重新计算下标。以从16位扩容到32位为例:

h      1010 0110
31     0001 1111
结果    0000 0110    (与之前相同)
h     1011 0110
31    0001 1111
结果  0001 0110    (与之前不同,相当于比之前加了16)

扩容后,数组下标可能改变,也可能不变。这时要看扩容的那一位的哈希值是1还是0,如果是1则不同,0则相同。

在这个过程中,有可能造成死锁问题,具体内容不再赘述。


jdk8中:

JDK8扩容中,为了避免之前提到的死锁问题,改进了扩容方法。通过判断这1位是0还是1,是0则不变。如果是1 ,加上原先数组大小。

 newTab[j + oldCap] = hiHead;
   //oldCap是原先的数组长度

总结:

扩容这一操作非常耗时,默认达到75%按照2倍进行扩容,这个75%也就是factor扩容因子。


JDK7中扩容是在节点还没有加到HashMap前发生的;

JDK8中扩容是在节点加到HashMap后发生的。


JDK7扩容是一个一个元素计算然后转移

JDK8是先遍历,判断哪些是放到新数组的低位,哪些是高位,然后将low的元素和high的元素分别组合起来,一次性转移到新的数组中。

相关文章
|
2月前
|
Java
让星星⭐月亮告诉你,HashMap中保证红黑树根节点一定是对应链表头节点moveRootToFront()方法源码解读
当红黑树的根节点不是其对应链表的头节点时,通过调整指针的方式将其移动至链表头部。具体步骤包括:从链表中移除根节点,更新根节点及其前后节点的指针,确保根节点成为新的头节点,并保持链表结构的完整性。此过程在Java的`HashMap$TreeNode.moveRootToFront()`方法中实现,确保了高效的数据访问与管理。
31 2
|
2月前
|
Java 索引
让星星⭐月亮告诉你,HashMap之往红黑树添加元素-putTreeVal方法源码解读
本文详细解析了Java `HashMap` 中 `putTreeVal` 方法的源码,该方法用于在红黑树中添加元素。当数组索引位置已存在红黑树类型的元素时,会调用此方法。具体步骤包括:从根节点开始遍历红黑树,找到合适位置插入新元素,调整节点指针,保持红黑树平衡,并确保根节点是链表头节点。通过源码解析,帮助读者深入理解 `HashMap` 的内部实现机制。
40 2
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
62 0
|
2月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
63 5
|
2月前
|
算法 索引
让星星⭐月亮告诉你,HashMap的resize()即扩容方法源码解读(已重新完善,如有不足之处,欢迎指正~)
`HashMap`的`resize()`方法主要用于数组扩容,包括初始化或加倍数组容量。该方法首先计算新的数组容量和扩容阈值,然后创建新数组。接着,旧数组中的数据根据`(e.hash & oldCap)`是否等于0被重新分配到新数组中,分为低位区和高位区两个链表,确保数据迁移时的正确性和高效性。
69 3
|
2月前
|
Java 索引
让星星⭐月亮告诉你,HashMap中红黑树TreeNode的split方法源码解读
本文详细解析了Java中`HashMap`的`TreeNode`类的`split`方法,该方法主要用于在`HashMap`扩容时将红黑树节点从旧数组迁移到新数组,并根据`(e.hash & oldCap)`的结果将节点分为低位和高位两个子树。低位子树如果元素数少于等于6,则进行去树化操作;若多于6且高位子树非空,则进行树化操作,确保数据结构的高效性。文中还介绍了`untreeify`和`replacementNode`方法,分别用于将红黑树节点转换为普通链表节点。
29 2
|
2月前
|
存储 Java
HashMap之链表转红黑树(树化 )-treefyBin方法源码解读(所有涉及到的方法均有详细解读,欢迎指正)
本文详细解析了Java HashMap中链表转红黑树的机制,包括树化条件(链表长度达8且数组长度≥64)及转换流程,确保高效处理大量数据。
106 1
|
2月前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
36 1
|
2月前
|
Java 关系型数据库 开发工具
idea创建不了spring2.X版本,无法使用JDK8,最低支持JDK17 , 如何用idea创建spring2.X版本,使用JDK8解决方案
本文提供了解决方案,如何在IDEA中创建Spring 2.X版本的项目并使用JDK8,尽管Spring 2.X已停止维护且IDEA不再直接支持,通过修改pom.xml或使用阿里云的国内源来创建项目。
137 0
idea创建不了spring2.X版本,无法使用JDK8,最低支持JDK17 , 如何用idea创建spring2.X版本,使用JDK8解决方案
|
2月前
|
存储 缓存 Java
HashMap源码解析
HashMap源码解析