HashMap在JDK1.7和JDK1.8的区别(源码解读)

本文详细阐述了JDK1.7和JDK1.8中HashMap的区别,包括存储结构、初始化方式、插入数据方式、hash值计算、扩容策略等方面的变化。JDK1.8引入了红黑树,优化了插入和查找效率,同时在扩容策略上进行了优化,避免了多线程环境下的一些问题。此外,还解析了put和get方法的源码,解释了HashMap的常见问题及其解决方案。
摘要由CSDN通过智能技术生成

HashMap在JDK1.7和JDK1.8的区别

一.区别

  1. 存储结构不同:JDK1.7是数组+链表,JDK1.8则是数组+链表+红黑树结构;
  2. 初始化方式不同:JDK1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而JDK1.8则是直接调用resize()扩容;
  3. 插入数据方式不同:插入键值对的put方法的区别,JDK1.8中会将节点插入到链表尾部,而JDK1.7中是采用头插,因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。;
  4. hash值计算不同:JDK1.7采用9次扰动(4次位运算+5次异或运算),JDK1.8采用2次扰动(1次位运算+1次异或运算)
  5. 扩容时JDK1.8会保持原链表的顺序,而JDK1.7会颠倒链表的顺序;而且JDK1.8是在元素插入后检测是否需要扩容,JDK1.7则是在元素插入前;
  6. 扩容后数据存储位置的计算方式不同:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
  7. 扩容策略不同:JDK1.7中是只要不小于阈值就直接扩容2倍;而JDK1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数大于6就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。

二.JDK1.8put、get方法源码解读

HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。
1)put方法,直接调用putVal:

   public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

putVal方法:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //定义存放元素的节点数组,节点,变量n,i;
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        /*
            步骤1:判断table是否为空
            1)table 表示存储在Map中的元素的数组
            2)(tab = table) == null 表示将table赋值给tab,并且判断tab是否为null。
            3)(n = tab.length) == 0 表示,将tab的长度赋值给n,并判断n 是否等于-
         */
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 步骤2:计算index,并对null做处理
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 步骤3:节点key存在,直接覆盖value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 步骤4:判断该链为红黑树
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {// 步骤5:该链为链表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //链表长度大于8转换为红黑树进行处理
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 将链表转为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    // key已经存在直接覆盖value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        //要添加的元素和链表中存在的元素相等了,则跳出for循环,不需要再比较后面的元素了,直接进入下面的if语句去替换e的值
                        break;
                    // 说明新添加的元素和当前节点不相同,继续找下一个元素
                    p = e;
                }
            }
            // e不为空,说明上面找到了一个去存储Key-Value的Node
            if (e != null) {
                //把e节点的值赋值给oldValue
                V oldValue = e.value;
                //根据条件判断是否替换
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //回调函数,把e节点移动到map最后
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 统计数据改变次数
        ++modCount;
        // 步骤6:超过最大容量就扩容
        if (++size > threshold)
            resize();
         //回调函数,新节点插入之后回调,根据evict判断是否需要删除最老插入的节点
        afterNodeInsertion(evict);
        return null;
    }

2)get方法:根据key的Hash找到具体的Node节点,直接返回node.value

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

getNode方法:获取目标key的Node

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 判断node数组已经初始化,根据key的hash找到first node
    if ((tab = table) != null 
        && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        // 检查第一个Node是不是要找的Node
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))//判断条件是hash值要相同,key值要相同
            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;
}

三.常见问题及参考回答

1.hashMap初始值的大小和负载因子的大小?

答:hashMap初始长度就是16,负载因子是0.75。hashMap所容纳的最大数据量为:长度*负载因子。即当长度达到这个值的时候就会发生扩容

2.hashMap线程安全方面会出现什么问题?

答:在JDK1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。在JDK1.8中,在多线程环境下,会发生数据覆盖的情况,所以多线程还是用concurrentHashMap。

3.当两个对象的 hashCode 相同会发生什么?

答: hashCode 相同,不一定就是相等的(equals方法比较),所以两个对象所在数组的下标相同,碰撞发生,又因为 HashMap 使用链表存储对象,这个 Node 会存储到链表中。

4.hash冲突的解决办法?

答:开放定址法、链地址法、再哈希法、建立公共溢出区。

5.为什么hashmap的在链表元素数量超过8时改为红黑树?为什么小于6又转为链表?

答:链表取元素是从头结点一直遍历到对应的结点,这个过程的复杂度是o(n) ,红黑树基于二叉树的结构,查找元素的复杂度为o(logn),所以用红黑树存储可以提高搜索的效率。当链表长度为6时 :查询的平均长度为 n/2=3, 红黑树为 log(6)=2.6,当链表长度为8时 :链表 8/2=4,红黑树为log(8)=3

6.数组的长度为什么是2的次幂

答:hashMap在操作中,首先会对key进行hash得到哈希值,然后哈希值会与哈希数组长度取余得到一个索引值,即 h = hash % n,这个索引值h就是对应哈希桶数组的索引。&运算速度快,比%取模运算块,根据计算,Java的%、/操作比&慢10倍左右,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n。当数组长度不为2的n次幂 的时候,hashCode 值与数组长度减一做与运算 的时候,会出现重复的数据,会导致数组元素分布不均匀。

7.hashMap 和 hashTable 有什么区别?

答:hashMap是线程不安全的,hashTable 是线程安全的;hashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 hashTable 不允许;hashMap默认初始化数组的大小为16,hashTable默认初始化数组的大小为11,前者扩容时,扩大2n倍,后者扩大2n+1;hashMap需要重新计算hash值,而hashTable直接使用对象的hashCode。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值