前言:把这段时间复习的关于集合类的东西整理出来,特别是HashMap相关的一些东西,以前都没有很注意1.7 ->> 1.8的变化问题,但后来发现这其实变化挺大的,并且不少整理的面试资料都没有更新(包括我以前整理的…)html
答:Map接口和Collection接口是全部集合框架的父接口:java
Collection接口的子接口包括:Set接口和List接口git
Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等github
Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等web
List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等面试
答:算法
HashMap没有考虑同步,是线程不安全的;Hashtable使用了synchronized关键字,是线程安全的;数组
HashMap容许K/V都为null;后者K/V都不容许为null;安全
HashMap继承自AbstractMap类;而Hashtable继承自Dictionary类;微信
图引用自:https://blog.csdn.net/u011240877/article/details/53358305
答:下面先来分析一下源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
// 1.若是table为空或者长度为0,即没有元素,那么使用resize()方法扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.计算插入存储的数组索引i,此处计算方法同 1.7 中的indexFor()方法
// 若是数组为空,即不存在Hash冲突,则直接插入数组
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 3.插入时,若是发生Hash冲突,则依次往下判断
else {
HashMap.Node<K,V> e; K k;
// a.判断table[i]的元素的key是否与须要插入的key同样,若相同则直接用新的value覆盖掉旧的value
// 判断原则equals() - 因此须要当key的对象重写该方法
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// b.继续判断:须要插入的数据结构是红黑树仍是链表
// 若是是红黑树,则直接在树中插入 or 更新键值对
else if (p instanceof HashMap.TreeNode)
e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 若是是链表,则在链表中插入 or 更新键值对
else {
// i .遍历table[i],判断key是否已存在:采用equals对比当前遍历结点的key与须要插入数据的key
// 若是存在相同的,则直接覆盖
// ii.遍历完毕后任务发现上述状况,则直接在链表尾部插入数据
// 插入完成后判断链表长度是否 > 8:如果,则把链表转换成红黑树
for (int binCount = 0; ; ++binCount) {
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 (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 对于i 状况的后续操做:发现key已存在,直接用新value覆盖旧value&返回旧value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 插入成功后,判断实际存在的键值对数量size > 最大容量
// 若是大于则进行扩容
if (++size > threshold)
resize();
// 插入成功时会调用的方法(默认实现为空)
afterNodeInsertion(evict);
return null;
}
图片简单总结为:
答:经过分析源码咱们知道了HashMap经过resize()
方法进行扩容或者初始化的操做,下面是对源码进行的一些简单分析:
/**
* 该函数有2中使用状况:1.初始化哈希表;2.当前数组容量太小,须要扩容
*/
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) {
// 针对状况2:若扩容前的数组容量超过最大值,则再也不扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 针对状况2:若没有超过最大值,就扩容为原来的2倍(左移1位)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 针对状况1:初始化哈希表(采用指定或者使用默认值的方式)
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
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;
if (oldTab != null) {
// 把每个bucket都移动到新的bucket中去
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 order
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;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
参考资料:https://juejin.im/post/5ab99afff265da23a2291dee
答:在解决这个问题以前,咱们首先须要知道什么是哈希冲突,而在了解哈希冲突以前咱们还要知道什么是哈希才行;
Hash,通常翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入经过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间一般远小于输入的空间,不一样的输入可能会散列成相同的输出,因此不可能从散列值来惟一的肯定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
全部散列函数都有以下一个基本特性:根据同一散列函数计算出的散列值若是不一样,那么输入值确定也不一样。可是,根据同一散列函数计算出的散列值若是相同,输入值不必定相同。
当两个不一样的输入值,根据同一散列函数计算出相同的散列值的现象,咱们就把它叫作碰撞(哈希碰撞)。
在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特色是:寻址容易,插入和删除困难;链表的特色是:寻址困难,但插入和删除容易;因此咱们将数组和链表结合在一块儿,发挥二者各自的优点,使用一种叫作链地址法的方式能够解决哈希冲突:
这样咱们就能够将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,咱们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4
(即2的四次方16)要远小于int类型的范围,因此咱们若是只是单纯的用hashCode取余来获取对应的bucket这将会大大增长哈希碰撞的几率,而且最坏状况下还会将HashMap变成一个单链表,因此咱们还须要对hashCode做必定的优化
上面提到的问题,主要是由于若是使用hashCode取余,那么至关于参与运算的只有hashCode的低位,高位是没有起到任何做用的,因此咱们的思路就是让hashCode取值出的高位也参与运算,进一步下降hash碰撞的几率,使得数据分布更平均,咱们把这样的操做称为扰动,在JDK 1.8中的hash()函数以下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与本身右移16位进行异或运算(高低位异或)
}
这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动);
经过上面的链地址法(使用散列表)和扰动函数咱们成功让咱们的数据分布更平均,哈希碰撞减小,可是当咱们的HashMap中存在大量数据时,加入咱们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度下降至O(logn);
简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:
1. 使用链地址法(使用散列表)来连接拥有相同hash值的数据;
2. 使用2次扰动函数(hash函数)来下降哈希冲突的几率,使得数据分布更平均;
3. 引入红黑树进一步下降遍历的时间复杂度,使得遍历更快;
答:hashCode()
方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap一般状况下是取不到最大值的,而且设备上也难以提供这么多的存储空间,从而致使经过hashCode()
计算出的哈希值可能不在数组大小范围内,进而没法匹配存储位置;
面试官:那怎么解决呢?
答:
HashMap本身实现了本身的hash()
方法,经过两次扰动使得它本身的哈希值高低位自行进行异或运算,下降哈希碰撞几率也使得数据分布更平均;
在保证数组长度为2的幂次方的时候,使用hash()
运算以后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操做更加有效率,二来也是由于只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;
面试官:为何数组长度要保证为2的幂次方呢?
答:
只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也能够减小冲突次数,提升HashMap的查询效率;
若是 length 为 2 的次幂 则 length-1 转化为二进制一定是 11111……的形式,在于 h 的二进制与操做效率会很是的快,并且空间不浪费;若是 length 不是 2 的次幂,好比 length 为 15,则 length - 1 为 14,对应的二进制为 1110,在于 h 与操做,最后一位都为 0 ,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费至关大,更糟的是这种状况中,数组可使用的位置比数组长度小了不少,这意味着进一步增长了碰撞的概率,减慢了查询的效率!这样就会形成空间的浪费。
面试官:那为何是两次扰动呢?
答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提升对应数组存储下标位置的随机性&均匀性,最终减小Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;
答:
不一样 | JDK 1.7 | JDK 1.8 |
---|---|---|
存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
初始化方式 | 单独函数:inflateTable() |
直接集成到了扩容函数resize() 中 |
hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 |
存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树 |
插入数据方式 | 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) |
扩容后存储位置的计算方式 | 所有按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) |
答:String、Integer等包装类的特性可以保证Hash值的不可更改性和计算准确性,可以有效的减小Hash碰撞的概率
都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不一样的状况
内部已重写了equals()
、hashCode()
等方法,遵照了HashMap内部的规范(不清楚能够去上面看看putValue的过程),不容易出现Hash值计算错误的状况;
面试官:若是我想要让本身的Object做为K应该怎么办呢?
答:重写hashCode()
和equals()
方法
重写hashCode()
是由于须要计算存储数据的存储位置,须要注意不要试图从散列码计算中排除掉一个对象的关键部分来提升性能,这样虽然能更快但可能会致使更多的Hash碰撞;
重写`equals()`方法,须要遵照自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的惟一性;
答:ConcurrentHashMap 结合了 HashMap 和 HashTable 两者的优点。HashMap 没有考虑同步,HashTable 考虑了同步的问题。可是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。
面试官:ConcurrentHashMap的具体实现知道吗?
参考资料:http://www.importnew.com/23610.html
答:在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构以下:
该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
Segment 是一种可重入的锁 ReentrantLock,每一个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先得到对应的 Segment 锁。
在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,结构以下:
插入元素过程(建议去看看源码):
若是相应位置的Node尚未初始化,则调用CAS插入相应的数据;
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
若是相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,若是该节点的hash不小于0,则遍历链表更新节点或插入新节点;
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
若是该节点是TreeBin类型的节点,说明是红黑树结构,则经过putTreeVal方法往红黑树中插入节点;若是binCount不为0,说明put操做对数据产生了影响,若是当前链表的个数达到8个,则经过treeifyBin方法转化为红黑树,若是oldVal不为空,说明是一次更新操做,没有对元素个数产生影响,则直接返回旧值;
若是插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;
答:
是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操做时,有可能会产生 fail-fast 机制。
例如:假设存在两个线程(线程一、线程2),线程1经过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
缘由:迭代器在遍历时直接访问集合中的内容,而且在遍历过程当中使用一个 modCount 变量。集合在被遍历期间若是内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素以前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;不然抛出异常,终止遍历。
解决办法:
1. 在遍历过程当中,全部涉及到改变modCount值得地方所有加上synchronized。
2. 使用CopyOnWriteArrayList来替换ArrayList
答:
这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合,即存储在这两个集合中的元素位置都是有顺序的,至关于一种动态的数组,咱们之后能够按位置索引来取出某个元素,而且其中的数据是容许重复的,这是与 HashSet 之类的集合的最大不一样处,HashSet 之类的集合不能够按索引号去检索其中的元素,也不容许有重复的元素。
ArrayList 与 Vector 的区别主要包括两个方面:
同步性:
Vector 是线程安全的,也就是说它的方法之间是线程同步(加了synchronized 关键字)的,而 ArrayList 是线程不安全的,它的方法之间是线程不一样步的。若是只有一个线程会访问到集合,那最好是使用 ArrayList,由于它不考虑线程安全的问题,因此效率会高一些;若是有多个线程会访问到集合,那最好是使用 Vector,由于不须要咱们本身再去考虑和编写线程安全的代码。
数据增加:
ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的我的超过了容量时,就须要增长 ArrayList 和 Vector 的存储空间,每次要增长存储空间时,不是只增长一个存储单元,而是增长多个存储单元,每次增长的存储单元的个数在内存空间利用与程序效率之间要去的必定的平衡。Vector 在数据满时(加载因子1)增加为原来的两倍(扩容增量:原容量的 2 倍),而 ArrayList 在数据量达到容量的一半时(加载因子 0.5)增加为原容量的 (0.5 倍 + 1) 个空间。
答:
LinkedList 实现了 List 和 Deque 接口,通常称为双向链表;ArrayList 实现了 List 接口,动态数组;
LinkedList 在插入和删除数据时效率更高,ArrayList 在查找某个 index 的数据时效率更高;
LinkedList 比 ArrayList 须要更多的内存;
面试官:Array 和 ArrayList 有什么区别?何时该应 Array 而不是 ArrayList 呢?
答:它们的区别是:
Array 能够包含基本类型和对象类型,ArrayList 只能包含对象类型。
Array 大小是固定的,ArrayList 的大小是动态变化的。
ArrayList 提供了更多的方法和特性,好比:addAll(),removeAll(),iterator() 等等。
对于基本类型数据,集合使用自动装箱来减小编码工做量。可是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。
答:HashSet的底层其实就是HashMap,只不过咱们HashSet是实现了Set接口而且把数据做为K值,而V值一直使用一个相同的虚值来保存,咱们能够看到源码:
public boolean add(E e) {
return map.put(e, PRESENT)==null;// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
}
因为HashMap的K值自己就不容许重复,而且在HashMap中若是K/V相同时,会用新的V覆盖掉旧的V,而后返回旧的V,那么在HashSet中执行这一句话始终会返回一个false,致使插入失败,这样就保证了数据的不可重复性;
答:
Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。咱们不须要担忧等待生产者有可用的空间,或消费者有可用的对象,由于它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,好比ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz
欢迎关注公众微信号:wmyskxz_javaweb
分享本身的Java Web学习之路以及各类Java学习资料
想要交流的朋友也能够加qq群:3382693
我没有三颗心脏