HashMap原理jdk7和jdk8的区别

本文详细对比了JDK1.7和JDK1.8中HashMap的实现方式、插入顺序、hash算法、扩容机制等关键区别,解析了红黑树引入的原因,以及解决链表与树频繁转换的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、hashMap的jdk1.7和jdk1.8区别

1、实现方式:

jdk7中使用数组+链表来实现,jdk8使用的数组+链表+红黑树

2、新节点插入到链表是的插入顺序不同

jdk7插入在头部,jdk8插入在尾部
jdk7中,如何在头部插入?看addEntry的createEntry方法:

    /**
     * Like addEntry except that this version is used when creating entries
     * as part of Map construction or "pseudo-construction" (cloning,
     * deserialization).  This version needn't worry about resizing the table.
     *
     * Subclass overrides this to alter the behavior of HashMap(Map),
     * clone, and readObject.
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

其实很简单的道理,就是创建一个new Entry,这个newEntry的next是e(参照new Entry<>(hash,key,value,e)的实现),这个e就是之前的头结点,然后把当前table的位置放上新建的Entry,也就是插入在链表的头部了。为什么jdk7要插入在头部呢?因为插入头部效率高,不需要遍历整个链表。
jdk8中,代码放在尾部,代码实现是putVal的一段代码:

                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if 

可以很容易看出是放在尾部。
jdk8中为什么新来的元素是放在节点的后面?我们知道jdk8链表大于8的时候就要树化,本身就要算这个链表的长度有多大,链表算大小就要一个个去遍历了,遍历到了才能知道数量,也可以直接把数据放到后面了,这样也是很方便,减少了把数据移动到头数组位置这一步,效率也提高了。而且,jdk7中出现的那个HashMap死循环bug不会有了,因为这里是只是平移,没有调换顺序。

3、jdk8的hash算法有所简化

jdk7默认初始化大小16,加载因子0.75。如果传入了size,会变为大于等于当前值的2的n次方的最小的数。

        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);

为什么是2次方数?因为indexFor方法的时候,h&(length-1),length是2的次方,那么length-1总是00011111等后面都是1的数,h&它之后,其实就相当于取余,与的效率比取余高,所以用了这种方式达到高效率。
下面是jdk7的indexfor的实现:

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

另外在hash函数里面方法里面,数会经过右移,为什么要右移?因为取余操作都是操作低位,hash碰撞的概率会提高,为了减少hash碰撞,右移就可以将高位也参与运算,减少了hash碰撞。
哈希函数如下:

    /**
     * Retrieve object hash code and applies a supplemental hash function to the
     * result hash, which defends against poor quality hash functions.  This is
     * critical because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     */
    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

所以总体来说jdk7的hash算法复杂
jdk8的右移比较简单,没有jdk7那么复杂。
jdk8的哈希函数如下:

    /* ---------------- Static utilities -------------- */

    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

为什么jdk8的hash函数会变简单?jdk8中我们知道用的是链表过度到红黑树,效率会提高,所以jdk8提高查询效率的地方由红黑树去实现,没必要像jdk那样右移那么复杂。

扩容机制有所优化

在jdk8中,实际上也是通过取余找到元素所在的数组的位置的,jdk8里面取余的方式在putVal里面:

i = (n - 1) & hash

上面那段代码就是。
我们假设,在扩容之前,key取余之后留下了n位。扩容之后,容量变为2倍,所以key取余得到的就有n+1位。在这n+1位里面,如果第1位是0,那么扩容前后这个key的位置还是在相同的位置(因为hash相同,并且余数的第1位是0,和之前n位的时候一样,所以余数还是一样,位置就一样了);如果这n+1位的第一位是1,那么就和之前的不同,那么这个key就应该放在之前的位置再加上之前整个数组的长度的位置,也就是这段代码:

                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;

这样子就减少了移动所有数据带来的消耗,jdk为了优化,真是“费尽心思”。

其他区别

节点表示

在jdk7中,节点是Entry,在jdk8中节点是Node,其实是一样的。

关于jdk7HashMap的一些细节

1、jdk7里面addEntry方法扩容的条件size>threshold,还有一个很容易忽略的,就是null!=table[bucketIndex],这个是什么意思?意思是如果当前放进来的值的那个位置也被占用了,才进行扩容,否则还是放到那个空的位置就行了,反正不存在hash冲突。(但是在jdk8里面取消了这个限制,只要达到了threshold,不管原来的位置空不空,还是要扩容)

2、jdk7resize方法多线程死循环的bug:http://www.importnew.com/22011.html 在jdk8的这个bug已经解决了。

关于jdk8HashMap的一些细节

1、jdk8 默认初始化大小16,加载因子0.75。还有一个默认的树化的大小8。非树化大小为6,也就是红黑树的元素小于6的时候,又会变回一个链表。为什么是8和6?
参考:
HashMap在JDK1.8及以后的版本中引入了红黑树结构,若桶中链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
2、jdk8的putVal方法:i = (n - 1) & hash这个其实也就是取余操作了。

关于HashMap的一些其他细节

1、为什么重写对象equals方法的时候,要重写hashcode,跟hashmap有关系吗?
java object类默认有一个hashcode方法,返回的是对象的地址。
hashmap是根据hashcode返回value的,如果没有重写hashcode,即使重写了equals方法之后,两个对象equals之后相同,但是hash之后却不同,在使用hashMap的时候通过一个对象作为key去获取另外一个对象为key存进去的value的时候,很有可能返回为空,这就是原因。所以和hashmap有关系,不如说和hashcode有关系。

2、ConcurrentModificationException为什么会有这个错误?
首先我们看一段代码:

  public static void main(String[] args) {
        HashMap<Integer,Integer> hashMap = new HashMap<>();
        hashMap.put(1,1);
        hashMap.put(2,2);
        for(Integer key:hashMap.keySet()){
            if(key.equals(1)){
                hashMap.remove(1);
            }
        }
    }

上面这个会报错
下面这个不会:

public static void main(String[] args) {
        HashMap<Integer,Integer> hashMap = new HashMap<>();
        hashMap.put(1,1);
        hashMap.put(2,2);
        for(Integer key:hashMap.keySet()){
            if(key.equals(2)){
                hashMap.remove(1);
            }
        }
    }

为什么呢?我们把上面的代码转换一下,我们把foreach变为下面这个也是一样的原理:

 Iterator<Integer> iterator = hashMap.keySet().iterator();
        while(iterator.hasNext()){
            Integer key = iterator.next();
            if(key.equals(1)){
                hashMap.remove(1);
            }
        }

看源码expectedModCount的位置,HashIterator.nextNode的位置会出现这个错误,看看源码。

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

在上面的例子中,到了一定的时间,expectedModCount是2,但是modCount变成3,就会报错。
在多线程情况下,一个线程在遍历,一个线程在remove就会出现这种情况。
其实也不一定在最后面添加的判断就不会,具体看hash实现情况。

3、设计hashmap需要解决的问题,1、找到hash函数 2、解决冲突

<think>嗯,用户现在问的是HashMapJDK7JDK8之间的实现差异。这个问题其实挺经典的,属于Java面试中的高频考点。从用户提供的引用内容来看,他已经收集了相当详细的资料,说明是有备而来的。 用户可能正在准备Java面试,或者需要深入理解HashMap的底层优化。考虑到之前讨论过高并发交易系统设计,用户的技术水平应该在中高级阶段,所以回答需要足够深入专业。 从引用材料看,JDK7JDK8HashMap差异主要集中在五个方面:数据结构、hash算法、初始化时机、扩容机制线程安全处理。其中数据结构从纯数组+链表变成数组+链表/红黑树这个改动最值得重点说明,因为这是解决哈希冲突导致性能退化的关键优化。 在回答组织上,应该先对比核心架构差异,再依次展开其他优化点。特别要注意引用材料中提到的几个关键细节:比如JDK8的hash函数简化(异或+移位)、插入扩容顺序变化(先插后扩)、树化阈值等。这些具体数值(如8/6)都是面试常考点。 线程安全部分虽然引用没提,但作为重要差异点需要补充,可以简要说明并发场景下的死循环问题。最后用总结表格收尾比较清晰,方便用户记忆。 相关问题的设计要覆盖可能延伸的考点:树化退化的条件、为什么选红黑树、线程安全替代方案等。这些问题既能考察理解深度,又实际开发相关。 对了,回答时要注意把引用标记自然地融入内容中,特别是那些具体的优化点描述。数学公式方面主要涉及树高计算(logN)扩容倍数,需要用LaTeX标注清楚。</think>以下是HashMapJDK7JDK8中的核心实现差异对比,结合源码级分析: --- ### 一、数据结构差异 | **特性** | **JDK7** | **JDK8** | |-------------------|------------------------------|---------------------------------------| | **底层结构** | 数组 + 单向链表 | 数组 + 单向链表/红黑树 | | **树化条件** | 不支持树化 | 链表长度≥8且数组长度≥64时树化[^4] | | **退化条件** | - | 树节点数≤6时退化为链表[^4] | | **节点结构** | `Entry<K,V>` | `Node<K,V>`(链表)/`TreeNode<K,V>`(树)| > **树化优化意义**: 当哈希冲突严重时,JDK7链表查询时间复杂度为$O(n)$,而JDK8红黑树可降至$O(\log n)$。极端情况下(10,000元素单链表),查询效率提升约**100倍**[^3]。 --- ### 二、关键算法优化 #### 1. **Hash函数优化** ```java // JDK7:4次位扰动 h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); h ^= (h >>> 7) ^ (h >>> 4); // JDK8:1次位扰动(效率提升) static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` **效果**:高位参与运算减少哈希碰撞,扰动计算效率更高[^2]。 #### 2. **插入逻辑变化** ```java // JDK7:头插法(多线程易导致死循环) void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); // 新节点插入链表头部 } // JDK8:尾插法 + 树化检测 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 尾插 if (binCount >= TREEIFY_THRESHOLD - 1) // 触发树化检查 treeifyBin(tab, hash); } ``` **并发安全**:尾插法避免扩容时链表成环[^4]。 --- ### 三、扩容机制升级 | **阶段** | JDK7 | JDK8 | |----------------|-------------------------------|-------------------------------| | **扩容顺序** | 先扩容再插入新元素 | 先插入新元素再统一扩容[^4] | | **触发条件** | size ≥ threshold 且发生哈希冲突 | ++size > threshold | | **重哈希计算** | 所有节点重新计算位置 | 利用高位掩码优化(位置=原位置或原位置+旧容量) | **JDK8扩容优化公式**: 新位置 = 原位置 或 原位置 + 旧容量 通过高位判断:`(e.hash & oldCap) == 0` **优势**:避免重新计算哈希,扩容效率提升50%+[^3]。 --- ### 四、初始化与内存 - **延迟初始化**: JDK7在构造函数中创建数组,JDK8首次调用`put()`时才创建数组[^3],减少内存占用。 - **容量计算**: JDK8使用`tableSizeFor()`保证容量为2的幂: ```java static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; // 位运算确保最高位后全为1 n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } ``` --- ### 五、线程安全问题 - **JDK7**:多线程扩容可能因头插法导致**循环链表**(死循环) - **JDK8**:尾插法解决循环链表问题,但仍存在**数据覆盖**(非原子操作) **解决方案**:改用`ConcurrentHashMap` --- ### 总结对比表 | **对比维度** | JDK7 | JDK8 | |------------------|-------------------------------|-------------------------------| | 数据结构 | 数组+链表 | 数组+链表/红黑树 | | Hash扰动 | 4次位运算 | 1次异或+移位 | | 插入方式 | 头插法 | 尾插法 | | 扩容顺序 | 先扩容再插入 | 先插入再扩容 | | 树化支持 | 不支持 | 支持 | | 初始化时机 | 构造函数创建数组 | 首次put时创建数组 | | 线程安全性 | 扩容死循环 | 数据覆盖 | > 通过以上优化,JDK8HashMap在**哈希冲突严重**的场景下(如10,000元素单链表),查询性能从$O(n)$提升至$O(\log n)$,并发扩容效率提升显著[^3][^4]。 --- ### 相关问题 1. **为什么JDK8选用红黑树而非AVL树实现树化?** 2. **HashMap在多线程环境下如何避免数据覆盖问题?** 3. **树化阈值为什么设置为8?退化阈值为什么是6?** 4. **JDK8扩容重哈希算法如何通过位运算避免重新计算下标?** 5. **HashMap与ConcurrentHashMap在高并发场景下的性能差异?**
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值