我是一名很普通的双非大三学生,跟不少同窗同样,有着一颗想进大厂的梦。接下来的几个月内,我将坚持写博客,输出知识的同时巩固本身的基础,记录本身的成长和锻炼本身,备战2021暑期实习面试!奥利给!!java
Map这个你们庭真的是成员不少呢,咱们能够简单回忆一下有哪些,我这里例举几个:HashMap、TreeMap、LikedHashMap、ConcurrentHashMap(线程安全)、WeekHashMap、HashTable。不记的话,能够搜搜其余文章回顾一下哦。本文只讨论HashMapnode
HashMap基本是咱们在平常使用中频率特别高的一个数据结构类型了,同时也是面试常常问到的,围绕着HashMap能展开一系列问题,好比:面试
本文不对源码作过深的讨论,由于我以为实习生应该还不须要了解的那么透彻,咱们须要作的是知道这些东西,源码什么的,每一步怎么作的,感兴趣的同窗能够本身看一下。算法
1.HashMap中常见的属性api
//HashMap的 初始容量为 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的扩容因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//转换红黑树的临界值,当链表长度大于此值时,会把链表结构转换为红黑树结构
static final int TREEIFY_THRESHOLD = 8;
//转换链表的临界值,当元素小于此值时,会将红黑树结构转换成链表结构
static final int UNTREEIFY_THRESHOLD = 6;
//当数组容量大于 64 时,链表才会转化成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//记录迭代过程当中 HashMap 结构是否发生变化,若是有变化,迭代时会 fail-fast
transient int modCount;
//HashMap 的实际大小,可能不许(由于当你拿到这个值的时候,可能又发生了变化)
transient int size;
//存放数据的数组
transient Node<K,V>[] table;
// 扩容的门槛,有两种状况
// 若是初始化时,给定数组大小的话,经过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方,好比你给定初始化大小 19,实际上初始化大小为 32,为 2 的 5 次方。
// 若是是经过 resize 方法进行扩容,大小 = 数组容量 * 0.75
int threshold;
/** HashMap中的内部类 **/
//链表的节点 1.7以前叫Entry,1.8以后叫Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
//红黑树的节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {}
复制代码
上面已经对属性写了注释,下面在补充一点:数组
什么是扩容因子("加载因子,负载因子")? 扩容因子是用来判断当前数组("哈希桶")何时须要进行扩容,假设因子为0.5,那么HashMap的初始化容量是16,则16*0.5 = 8个元素的时候,HashMap就会进行扩容。安全
为何扩容因子是0.75? 这是均衡了时间和空间损耗算出来的值,由于当扩容因子设置比较大的时候,至关于扩容的门槛就变高了,发生扩容的频率变低了,但此时发生Hash冲突的概率就会提高,当冲突的元素过多的时候,变成链表或者红黑树都会增长了查找成本(hash 冲突增长,链表长度变长)。而扩容因子太小的时候,会频繁触发扩容,占用的空间变大,好比从新计算Hash等,使得操做性能会比较高。数据结构
HashMap初始化容量是多少? 在不指定capacity状况下,初始化容量是16,但不是初始化的时候就建立了一个16大小的数组,而是在第一次put的时候去判断是否须要初始化,我感受这有一点懒加载的味道。而且咱们常常在一些文章,包括阿里巴巴开发手册中看到:"集合初始化时,指定集合初始值大小",若是有不少数据须要储存到 HashMap 中,建议 HashMap 的容量一开始就设置成足够的大小,这样能够防止在其过程当中不断的扩容,影响性能,好比HashMap 须要放置1024 个元素,因为没有设置容量初始大小,随着元素不断增长,容量 7 次被迫扩大,resize 须要重建hash 表,严重影响性能。多线程
初始化容量为何是16?为何每次扩容是2的幂次方? 由于在使用是2的幂的数字的时候,Length-1的值是全部二进制位全为1,这种状况下,index的结果等同于HashCode后几位的值。只要输入的HashCode自己分布均匀,Hash算法的结果就是均匀的。这是为了实现均匀分布。深刻解释能够看这篇文章:HashMap中hash(Object key)原理,为何(hashcode >>> 16)。源码分析
Node节点是什么?。 node节点在1.7以前也叫entry节点,是HashMap存储数据的一个节点,主要有四个属性,hash,key,value,next,其实就是一个标准的链表节点,很容易理解。
链表何时会转换成红黑树? 当链表长度大于等于 8 时,此时的链表就会转化成红黑树,转化的方法是:treeifyBin,此方法有一个判断,当链表长度大于等于 8,而且整个数组大小大于 64 时,才会转成红黑树,当数组大小小于 64 时,只会触发扩容,不会转化成红黑树
加强 for 循环进行删除,为何会出现ConcurrentModificationException? 由于加强 for 循环过程其实调用的就是迭代器的 next () 方法,当你调用 map#remove () 方法进行删除时,modCount 的值会 +1,而这时候迭代器中的 expectedModCount 的值却没有变,致使在迭代器下次执行 next () 方法时,expectedModCount != modCount 就会报 ConcurrentModificationException 的错误。这实际上是一种快速失败的机制,java.util下面的集合类基本都是快速失败的,实现都同样,都是依靠这两个变量。
可使用迭代器的remove()方法去删除,由于 Iterator.remove () 方法在执行的过程当中,会把最新的modCount 赋值给 expectedModCount,这样在下次循环过程当中,modCount 和 expectedModCount 二者就会相等。
从文章开头贴出的代码属性中咱们能够看出,1.8版本的HashMap的底层其实是一个数组+链表+红黑树的结构,可是在1.7的时候是没有红黑树的,这正是1.8版本中对查询的优化,咱们都知道链表的查询时间复杂度是O(n)的,由于它须要一个一个去遍历链表上全部的节点,因此当链表长度过长的时候,会严重影响 HashMap 的性能,而红黑树具备快速增删改查的特色,这样就能够有效的解决链表过长时操做比较慢的问题。因而在1.8中引入了红黑树。
咱们来看1.8中HashMap的新增图示,就不在晒那一大段的新增代码了。
为何须要从新计算Hash?
1.7中计算下标:
static int indexFor(int h, int length) {
return h & (length-1);
}
// n 表示数组的长度,i 为数组索引下标
1.8中计算下标:tab[i = (n - 1) & hash])
// 1.8中的高位运算
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
复制代码
在jdk1.7中有indexFor(int h, int length)方法。jdk1.8里没有1.8中用tab[(n - 1) & hash]代替但原理同样。从上咱们能够看出是由于长度扩大之后,Hash的规则也随之改变。这在1.7和1.8中差异不大,可是1.7须要与新的数组长度进行从新hash运算,这个方式是相对耗性能的,而在1.8中对这一步进行了优化,采用高位运算取代了ReHash操做,其实这是一种规律,由于每次扩容,其实元素的新位置就是原位置+原数组长度,不懂的能够看jdk8之HashMap resize方法详解(深刻讲解为何1.8中扩容后的元素新位置为原位置+原数组长度)
咱们都知道1.7以前,JDK是头插法,考虑头插法的缘由是不用遍历链表,提升插入性能,但在JDK8已经改成尾插法了,不存在这个死循环问题,因此问题就出在头插法这。 我以为这篇文章在这写的很清楚,想深刻了解一下产生死循环的过程,能够看这篇文章:老生常谈,HashMap的死循环。总结一下来讲就是:Java7在多线程操做HashMap时可能引发死循环,缘由是扩容转移后先后链表顺序倒置,在转移过程当中修改了原来链表中节点的引用关系。Java8在一样的前提下并不会引发死循环,缘由是扩容转移后先后链表顺序不变,保持以前节点的引用关系。
那么1.8以后,HashMap就是线程安全的了嘛? 首先咱们要知道所谓线程安全是对(读/写)两种状况都是数据一致而言的,而只读且不变化的话,HashMap也是线程安全的,之因此不安全是在写的时候,索引构建的时候会产生构建不一致的状况,好比没法保证上一秒put的值,下一秒get的时候仍是原值,因此线程安全仍是没法保证,因此要问面试官有没有读写并存的状况。且java.util下面的集合类基本都不是线程安全的。因此HashMap 是非线程安全的,咱们能够本身在外部加锁,或者经过 Collections#synchronizedMap 来实现线程安全,Collections#synchronizedMap 的实现是在每一个方法上加上了 synchronized 锁;或者使用ConcurrentHashMap
HashMap 的内容虽然较多,但大多数 api 都只是对数组 + 链表 + 红黑树这种数据结构进行封装而已。那么看完这些,你能触类旁通答出一下问题了吗?