目录

Day1、为什么JDK1.8中HashMap从头插入改成尾插入

存储方式

静态常量

插入元素

扩容

拓展问题

1.为什么JDK1.8采用红黑树存储Hash冲突的元素?

2.为什么在长度小于8时使用链表,不一直使用红黑树?

3.为什么要使用红黑树而不使用AVL树?

4.为什么数组容量必须是2次幂?

5.为什么单链表转为红黑树要求桶内的元素个数大于8?


Day1、为什么JDK1.8中HashMap从头插入改成尾插入

头插入和尾插入是什么意思,首先我们知道,HashMap的实现原理是数组+单链表的形式,当出现hash冲突的时候,元素会以链表的形式存储,JDK1.7中元素插入链表是从头部插入,JDK1.8中是从尾部插入。

看看JDK1.8中HashMap的源码,从头捋一遍。

存储方式

    //以数组的形式存储Hash值,当存在hash冲突时使用单链表存储transient Node<K,V>[] table;/*** Basic hash bin node, used for most entries.  (See below for* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)*/static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey()        { return key; }public final V getValue()      { return value; }public final String toString() { return key + "=" + value; }public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}}

HashMap默认采用数组+单链表方式存储元素,当元素出现哈希冲突时,会存储到该位置的单链表中。

静态常量

    /*** The default initial capacity - MUST be a power of two.* 初始容量为16,要求必须为2的幂*/static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/*** The maximum capacity, used if a higher value is implicitly specified* by either of the constructors with arguments.* MUST be a power of two <= 1<<30.* 最大容量为2的30次方*/static final int MAXIMUM_CAPACITY = 1 << 30;/*** The load factor used when none specified in constructor.* 扩展因子为0.75,当容量超过当前容量0.75时自动扩容*/static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** The bin count threshold for using a tree rather than list for a* bin.  Bins are converted to trees when adding an element to a* bin with at least this many nodes. The value must be greater* than 2 and should be at least 8 to mesh with assumptions in* tree removal about conversion back to plain bins upon* shrinkage.* hash冲突使用单链表存储,当单链表结点数超过8之后,转为红黑树存储*/static final int TREEIFY_THRESHOLD = 8;/*** The bin count threshold for untreeifying a (split) bin during a* resize operation. Should be less than TREEIFY_THRESHOLD, and at* most 6 to mesh with shrinkage detection under removal.* hash冲突红黑树中的结点数少于6时传华为单链表存储*/static final int UNTREEIFY_THRESHOLD = 6;/*** The smallest table capacity for which bins may be treeified.* (Otherwise the table is resized if too many nodes in a bin.)* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts* between resizing and treeification thresholds.* hash冲突单链表转化为红黑树的条件除了结点数大于8之外,还要求数组容量大于64*/static final int MIN_TREEIFY_CAPACITY = 64;

HashMap默认采用数组+单链表方式存储元素,当元素出现哈希冲突时,会存储到该位置的单链表中。与JDK1.7相比,单链表不会一直增加元素,当元素个数超过8个时,会尝试将单链表转化为红黑树存储。但是在转化前,会再判断一次当前数组的长度,只有数组长度大于64才处理。否则,进行扩容操作。

插入元素

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}/*** Implements Map.put and related methods** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value* @param evict if false, the table is in creation mode.* @return previous value, or null if none*/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 {//hash冲突时,采用尾插法Node<K,V> e; K k;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) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

JDK1.8中首先判断此时是否为树结构,如果是,插入新元素为树中的一个结点,否则遍历链表道末尾,将新元素未插入到链表末尾,然后判断链表长度是否超过转化数的临界值。

与之对比,JDK1.7中的插入元素源码。

    public V put(K key, V value) {if (key == null)return putForNullKey(value);int hash = hash(key);int i = indexFor(hash, table.length);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++;//插入新结点addEntry(hash, key, value, i);return null;}void addEntry(int hash, K key, V value, int bucketIndex) {//判断是否需要扩容if ((size >= threshold) && (null != table[bucketIndex])) {resize(2 * table.length);hash = (null != key) ? hash(key) : 0;bucketIndex = indexFor(hash, table.length);}//插入新的结点createEntry(hash, key, value, bucketIndex);}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++;}/*** Creates new entry.*/Entry(int h, K k, V v, Entry<K,V> n) {value = v;//将原链表连接到新元素的后边,即从头部插入next = n;key = k;hash = h;}

扩容

JDK1.8扩容:

 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) // initial capacity was placed in thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaultsnewCap = 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;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;//重点在这,旧表到新表要重新hashif (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 { // preserve orderNode<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;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;}

JDK1.7扩容:

    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];boolean oldAltHashing = useAltHashing;useAltHashing |= sun.misc.VM.isBooted() &&(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);boolean rehash = oldAltHashing ^ useAltHashing;transfer(newTable, rehash);table = newTable;threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);}void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;for (Entry<K,V> e : table) {while(null != e) {Entry<K,V> next = e.next;if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);}int i = indexFor(e.hash, newCapacity);e.next = newTable[i];//每次rehash的元素都从头部插入newTable[i] = e;e = next;//遍历原链表}}}

JDK1.7中扩容时,每个元素的rehash之后,都会插入到新数组对应索引的链表头,所以这就导致原链表顺序为A->B->C,扩容之后,rehash之后的链表可能为C->B->A,元素的顺序发生了变化。在并发场景下,扩容时可能会出现循环链表的情况。而JDK1.8从头插入改成尾插入元素的顺序不变,避免出现循环链表的情况。

拓展问题

1.为什么JDK1.8采用红黑树存储Hash冲突的元素?

红黑树本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。能够加快检索速率。

2.为什么在长度小于8时使用链表,不一直使用红黑树?

桶中元素的插入只会在hash冲突时发生,而hash冲突发生的概率较小,一直维护一个红黑树比链表耗费资源更多,在桶中元素量较小时没有这个必要。

3.为什么要使用红黑树而不使用AVL树?

红黑树与AVLl树,在检索的时候效率差不多,都是通过平衡来二分查找。但红黑树不像avl树一样追求绝对的平衡,红黑树允许局部很少的不完全平衡,这样对于效率影响不大,但省去了很多没有必要的调平衡操作,avl树调平衡有时候代价较大,所以效率不如红黑树。

4.为什么数组容量必须是2次幂?

索引计算公式为i = (n - 1) & hash,如果n为2次幂,那么n-1的低位就全是1,哈希值进行与操作时可以保证低位的值不变,从而保证分布均匀,效果等同于hash%n,但是位运算比取余运算要高效的多。

5.为什么单链表转为红黑树要求桶内的元素个数大于8?

当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,而一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件

同理,少于6就从红黑树转回单链表是为了节省维护一个树的资源消耗,而选择6作为临界值,是因理想情况下一个bin中元素个数达到6的概率是0.00001316,达到7的概率为0.00000094,二者跨度较大,可以减小树和链表之间频繁转化的可能性。

Day1、为什么JDK1.8中HashMap从头插入改成尾插入相关推荐

  1. HashMap为何从头插入改为尾插入

    微信公众号:I am CR7 如有问题或建议,请在下方留言; 最近更新:2018-09-21 前言 前面对于HashMap在jdk1.8中元素插入的实现原理,进行了详细分析,具体请看:HashMap之 ...

  2. JDK1.7和JDK1.8中HashMap是线程不安全的,并发容器ConcurrentHashMap模型

    一.HashMap是线程不安全的 前言 只要是对于集合有一定了解的一定都知道HashMap是线程不安全的,我们应该使用ConcurrentHashMap.但是为什么HashMap是线程不安全的呢,之前 ...

  3. [Java]JDK1.7中HashMap的并发死链

    [Java]JDK1.7中HashMap的并发死链 HashMap的并发死链现象发生在扩容时,在扩容过程中**transfer()**方法负责把旧的键值对转移到新的表中,其代码如下: void tra ...

  4. jdk1.8中HashMap扰动函数及数组长度为什么是2的n次方介绍

    文章目录 前言 一.什么是二进制? 二.计算机采用二进制的原因 三.十进制与二进制相互转换 十进制转成二进制 二进制转换为十进制 与.或.异或运算 按位异或 按位与运算 按位或运算 Jdk1.8中Ha ...

  5. 详述 JDK1.7 中 HashMap 会发生死链的原因

    文章目录 前置知识 死循环执行步骤1 死循环执行步骤2 死循环执行步骤3 解决方案 总结 前置知识 HashMap死循环是一个比较常见.比较经典的问题,在日常的面试中出现的频率比较高,所以接下来咱们通 ...

  6. 七、JDK1.7中HashMap扩容机制

    导读 前面文章一.深入理解-Java集合初篇 中我们对Java的集合体系进行一个简单的分析介绍,上两篇文章二.Jdk1.7和1.8中HashMap数据结构及源码分析 .三.JDK1.7和1.8Hash ...

  7. 八、JDK1.8中HashMap扩容机制

    导读 前面文章一.深入理解-Java集合初篇 中我们对Java的集合体系进行一个简单的分析介绍,上两篇文章二.Jdk1.7和1.8中HashMap数据结构及源码分析 .三.JDK1.7和1.8Hash ...

  8. JDK1.7中HashMap底层实现原理

    JDK1.7中HashMap底层实现原理 一.数据结构 HashMap中的数据结构是数组+单链表的组合,以键值对(key-value)的形式存储元素的,通过put()和get()方法储存和获取对象. ...

  9. 7-7 字符串中的大写字母改成小写字母 (10 分)

    把一个字符串中的大写字母改成小写字母,其他字符不变. 输入格式: 在 一行中输入长度小于20的字符串.在字符串中不要出现换行符,空格,制表符. 输出格式: 直接输出变化后的字符串. 输入样例: asD ...

最新文章

  1. 移动端开发者眼中的前端开发流程变迁与前后端分离
  2. 超过efficientnet
  3. I.MX6 WIFI wireless_tools 移植
  4. [YTU]_2621(B 继承 圆到圆柱体)
  5. 简单介绍android studio中的Logcat
  6. 『ACM-算法-lowbit』算法竞赛进阶指南--lowbit运算,找到二进制下所有是1的位
  7. 怎样使用navicat将mysql的数据表导出保存(转储SQL文件)
  8. 无意中发现Markdown,最终解放了我
  9. 抓包分析arp攻击Linux,从抓包分析角度分析arp攻击
  10. iOS开发,更改状态栏(StatusBar)文字颜色为白色
  11. arduino液位传感器_「雕爷学编程」Arduino动手做(24)——水位传感器模块
  12. 需求分析,我们应当怎样做
  13. 测试新人如何提高工作效率
  14. Nodejs 获取本机IP地址
  15. 工作记录--------unbuntu20搭建微信和Foxmail
  16. 中序遍历 java_java二叉树中序遍历递归和非递归实现
  17. 网易云音乐、微博成新规后首批IPO企业 招股书披露数据安全风险
  18. 简单介绍一下HBase、Cassandra、Voldemort、Redis、VoltDB、MySQL(转)
  19. 7.camera驱动06-自己实现v4l2驱动-虚拟摄像头
  20. 你用过Elasticsearch Percolate 反向检索吗?

热门文章

  1. 中国公民身份证编号规则
  2. Android手机无法使用google地图的问题的解决方案
  3. 《深入剖析Kubernetes》-张磊——白话容器基础(二):隔离与限制
  4. Ubuntu python3.6的安装
  5. 【译】第三篇 SQL Server代理警报和操作员
  6. C#(VS2008)服务编写-安装和部署
  7. 《勋伯格和声学》读书笔记(六):关于获得较好和声的一些提示
  8. JAVA自定义协议解析
  9. Java中字符串开头,java中如何判断字符串是以什么开头
  10. CVPR2021:百篇AR/VR关联性研究成果汇总