HashMap之JDK7与JDK8区别

简介: Map是Java开发过程使用频率比较高的集合,并且在面试过程中经常会问道,HashMap的底层结构是什么?如何避免Hash碰撞等等,JDK8对HashMap进行了优化,这边文章重点介绍下HashMap在JDK7与JDK8的区别。

HashMap-JDK7

在jdk1.7及之前的版本,hashmap底层结构是数组+链表的形式。

上图中,每个Node是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。

put方法

public V put(K key, V value) {

   // 当插入第一个元素的时候,需要先初始化数组大小

   if (table == EMPTY_TABLE) {

       inflateTable(threshold);

   }

   // 如果 key 为 null,感兴趣的可以往里看,最终会将这个 entry 放到 table[0] 中

   if (key == null)

       return putForNullKey(value);

   // 1. 求 key 的 hash 值

   int hash = hash(key);

   // 2. 找到对应的数组下标

   int i = indexFor(hash, table.length);

   // 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,

   //    如果有,直接覆盖,put 方法返回旧值就结束了

   for (Entry<K,V> e = table[i]; e != null; e = e.next) {

       Object k;

       if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

           V oldValue = e.value;

           e.value = value;

           e.recordAccess(this);

           return oldValue;

       }

   }

   modCount++;

   // 4. 不存在重复的 key,将此 entry 添加到链表中,细节后面说

   addEntry(hash, key, value, i);

   return null;

}

数组初始化

在第一个元素插入 HashMap 的时候做一次数组的初始化,就是先确定初始的数组大小,并计算数组扩容的阈值。

private void inflateTable(int toSize) {

   // 保证数组大小一定是 2 的 n 次方。

   // 比如这样初始化:new HashMap(8),那么处理成初始数组大小是 8

   int capacity = roundUpToPowerOf2(toSize);

   // 计算扩容阈值:capacity * loadFactor

   threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

   // 算是初始化数组吧

   table = new Entry[capacity];

   initHashSeedAsNeeded(capacity); //ignore

}

添加节点到链表头

找到数组下标后,会先进行 key 判重,如果没有重复,就准备将新值放入到链表的表头

void addEntry(int hash, K key, V value, int bucketIndex) {

   // 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容

   if ((size >= threshold) && (null != table[bucketIndex])) {

       // 扩容,后面会介绍一下

       resize(2 * table.length);

       // 扩容以后,重新计算 hash 值

       hash = (null != key) ? hash(key) : 0;

       // 重新计算扩容后的新的下标

       bucketIndex = indexFor(hash, table.length);

   }

   // 往下看

   createEntry(hash, key, value, bucketIndex);

}

// 这个很简单,其实就是将新值放到链表的表头,然后 size++

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

}

数组扩容

如果当前的 size 已经达到了阈值,并且要插入的数组位置上已经有元素,那么就会触发扩容,扩容后,数组大小为原来的 2 倍。扩容就是用一个新的大数组替换原来的小数组,并将原来数组中的值迁移到新的数组中。

由于是双倍扩容,迁移过程中,会将原来 table[i] 中的链表的所有节点,分拆到新的数组的 newTable[i] 和 newTable[i + oldLength] 位置上。如原来数组长度是 16,那么扩容后,原来 table[0] 处的链表中的所有元素会被分配到新数组中 newTable[0] 和 newTable[16] 这两个位置

void resize(int newCapacity) {

   Entry[] oldTable = table;

   int oldCapacity = oldTable.length;

   if (oldCapacity == MAXIMUM_CAPACITY) {

       threshold = Integer.MAX_VALUE;

       return;

   }

   // 新的数组

   Entry[] newTable = new Entry[newCapacity];

   // 将原来数组中的值迁移到新的更大的数组中

   transfer(newTable, initHashSeedAsNeeded(newCapacity));

   table = newTable;

   threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

}

get方法

  1. 根据 key 计算 hash 值。
  2. 找到相应的数组下标:hash & (length - 1)。
  3. 遍历该数组位置处的链表,直到找到相等(==或equals)的 key。

public V get(Object key) {

   // 之前说过,key 为 null 的话,会被放到 table[0],所以只要遍历下 table[0] 处的链表就可以了

   if (key == null)

       return getForNullKey();

   //

   Entry<K,V> entry = getEntry(key);


   return null == entry ? null : entry.getValue();

}



final Entry<K,V> getEntry(Object key) {

   if (size == 0) {

       return null;

   }


   int hash = (key == null) ? 0 : hash(key);

   // 确定数组下标,然后从头开始遍历链表,直到找到为止

   for (Entry<K,V> e = table[indexFor(hash, table.length)];

        e != null;

        e = e.next) {

       Object k;

       if (e.hash == hash &&

           ((k = e.key) == key || (key != null && key.equals(k))))

           return e;

   }

   return null;

}

Java7 Con

HashMap-JDK8

在jdk1.8中,HashMap底层实现采用的是数组+链表+红黑树的数据结构,在链表长度达到一定的阈值时,可能会将链表转换成红黑树,将查找操作的时间复杂度由O(N)优化到O(logN)。其put方法将节点插入链表时改换成尾插法,在扩容后链表元素依然保持原来的顺序。

Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode

put分析

public V put(K key, V value) {

   return putVal(hash(key), key, value, false, true);

}


//  onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 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;

   // 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度

   // 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量

   if ((tab = table) == null || (n = tab.length) == 0)

       n = (tab = resize()).length;

   // 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了

   if ((p = tab[i = (n - 1) & hash]) == null)

       tab[i] = newNode(hash, key, value, null);


   else {// 数组该位置有数据

       Node<K,V> e; K k;

       // 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点

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

                   // TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 8 个

                   // 会触发下面的 treeifyBin,也就是将链表转换为红黑树

                   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,那么 e 为链表中[与要插入的新值的 key "相等"]的 node

                   break;

               p = e;

           }

       }

       // e!=null 说明存在旧值的key与要插入的key"相等"

       // 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值

       if (e != null) {

           V oldValue = e.value;

           if (!onlyIfAbsent || oldValue == null)

               e.value = value;

           afterNodeAccess(e);

           return oldValue;

       }

   }

   ++modCount;

   // 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容

   if (++size > threshold)

       resize();

   afterNodeInsertion(evict);

   return null;

}

resize方法

final Node<K,V>[] resize() {

   Node<K,V>[] oldTab = table;

   int oldCap = (oldTab == null) ? 0 : oldTab.length;

   int oldThr = threshold;

   int newCap, newThr = 0;

   if (oldCap > 0) { // 对应数组扩容

       if (oldCap >= MAXIMUM_CAPACITY) {

           threshold = Integer.MAX_VALUE;

           return oldTab;

       }

       // 将数组大小扩大一倍

       else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                oldCap >= DEFAULT_INITIAL_CAPACITY)

           // 将阈值扩大一倍

           newThr = oldThr << 1; // double threshold

   }

   else if (oldThr > 0) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候

       newCap = oldThr;

   else {// 对应使用 new HashMap() 初始化后,第一次 put 的时候

       newCap = DEFAULT_INITIAL_CAPACITY;

       newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

   }


   if (newThr == 0) {

       float ft = (float)newCap * loadFactor;

       newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

                 (int)ft : Integer.MAX_VALUE);

   }

   threshold = newThr;


   // 用新的数组大小初始化新的数组

   Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

   table = newTab; // 如果是初始化数组,到这里就结束了,返回 newTab 即可


   if (oldTab != null) {

       // 开始遍历原数组,进行数据迁移。

       for (int j = 0; j < oldCap; ++j) {

           Node<K,V> e;

           if ((e = oldTab[j]) != null) {

               oldTab[j] = null;

               // 如果该数组位置上只有单个元素,那就简单了,简单迁移这个元素就可以了

               if (e.next == null)

                   newTab[e.hash & (newCap - 1)] = e;

               // 如果是红黑树,具体我们就不展开了

               else if (e instanceof TreeNode)

                   ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

               else {

                   // 这块是处理链表的情况,

                   // 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序

                   // loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表,代码还是比较简单的

                   Node<K,V> loHead = null, loTail = null;

                   Node<K,V> hiHead = null, hiTail = null;

                   Node<K,V> next;

                   do {

                       next = e.next;

                       if ((e.hash & oldCap) == 0) {

                           if (loTail == null)

                               loHead = e;

                           else

                               loTail.next = e;

                           loTail = e;

                       }

                       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;

                       // 第二条链表的新的位置是 j + oldCap,

                       newTab[j + oldCap] = hiHead;

                   }

               }

           }

       }

   }

   return newTab;

}

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;

   if ((tab = table) != null && (n = tab.length) > 0 &&

       (first = tab[(n - 1) & hash]) != null) {

       // 判断第一个节点是不是就是需要的

       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;

}

相关文章
|
20天前
|
安全
HashTable与HashMap的区别
(1)HashTable的每个方法都用synchronized修饰,因此是线程安全的,但同时读写效率很低 (2)HashTable的Key不允许为null (3)HashTable只对key进行一次hash,HashMap进行了两次Hash (4)HashTable底层使用的数组加链表HashTable与HashMap的区别
23 2
|
14天前
|
Java 编译器 API
深入解析:JDK与JVM的区别及联系
在Java开发和运行环境中,JDK(Java Development Kit)和JVM(Java Virtual Machine)是两个核心概念,它们在Java程序的开发、编译和运行过程中扮演着不同的角色。本文将深入解析JDK与JVM的区别及其内在联系,为Java开发者提供清晰的技术干货。
22 1
|
23天前
|
小程序 Java 程序员
JDK 和 JRE 有什么区别
JDK(Java Development Kit)是Java开发工具包,包含编译器、调试器等开发工具,用于开发Java程序。JRE(Java Runtime Environment)是Java运行环境,包含Java虚拟机和类库,用于运行Java程序。简言之,JDK用于编写Java程序,JRE用于运行这些程序。
35 1
|
2月前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
32 1
|
2月前
|
存储 缓存 Java
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
41 1
|
2月前
|
Dubbo Java 应用服务中间件
剖析Tomcat线程池与JDK线程池的区别和联系!
剖析Tomcat线程池与JDK线程池的区别和联系!
131 0
剖析Tomcat线程池与JDK线程池的区别和联系!
|
2月前
|
Java 关系型数据库 开发工具
idea创建不了spring2.X版本,无法使用JDK8,最低支持JDK17 , 如何用idea创建spring2.X版本,使用JDK8解决方案
本文提供了解决方案,如何在IDEA中创建Spring 2.X版本的项目并使用JDK8,尽管Spring 2.X已停止维护且IDEA不再直接支持,通过修改pom.xml或使用阿里云的国内源来创建项目。
100 0
idea创建不了spring2.X版本,无法使用JDK8,最低支持JDK17 , 如何用idea创建spring2.X版本,使用JDK8解决方案
|
2月前
|
Oracle Java 关系型数据库
jdk17安装全方位手把手安装教程 / 已有jdk8了,安装JDK17后如何配置环境变量 / 多个不同版本的JDK,如何配置环境变量?
本文提供了详细的JDK 17安装教程,包括下载、安装、配置环境变量的步骤,并解释了在已有其他版本JDK的情况下如何管理多个JDK环境。
1128 0
|
4月前
|
安全 Java
【Java集合类面试十六】、HashMap与ConcurrentHashMap有什么区别?
HashMap是非线程安全的,而ConcurrentHashMap通过减少锁粒度来提高并发性能,检索操作无需锁,从而提供更好的线程安全性和性能。
|
4月前
|
存储 安全 Java
Hashtable 和 HashMap 的区别
【8月更文挑战第22天】
165 0