上一篇文章 Android 中高级面试必知必会 中 JAVA 部分的第一部分就是容器,容器是 JAVA 中很是重要的一个部分,也是面试时考察基础知识很重要的一环,咱们首先来看下图,关于容器部分的整体框架html
集合框架主要分为两大类: Collection 和 Map。java
Collection 是 List、Set 等集合高度抽象出来的接口,它包含了这些集合的基本操做,它主要又分为两大部分:List和Set。node
Map 是一个映射接口,其中的每一个元素都是一个 key-value 键值对,一样抽象类 AbstractMap 经过适配器模式实现了 Map 接口中的大部分函数。像咱们经常使用的 HashMap、LinkedHashMap 都是继承自 Map 接口。面试
今天咱们主要讲解容器中的 HashMap 部分。算法
咱们首先来看下面几个问题,读者能够本身试着回答一下。
一、 HashMap 的实现原理?底层数据结构?
二、 HashMap 的扩容,扩容因子?
三、 什么是哈希碰撞,如何解决哈希碰撞?
四、 HashMap 是线程安全的吗?shell
要讲到 HashMap,咱们主要分为如下主要模块进行讲解。编程
Hash ,通常直接音译为“哈希”或“散列”,就是把任意长度的输入,经过散列算法,变化为固定长度的输出,输出值则称为散列值。数组
常见的 Hash 函数有一下几种。安全
两个不一样的输入值,通过同一散列函数计算出的散列值相同的现象叫作哈希碰撞。bash
常见的解决哈希碰撞的方法有以下几种:
在 JDK1.6,JDK1.7 中,HashMap 采用数组+链表实现,即便用链表处理冲突,同一 hash 值的链表都存储在一个链表里。可是当位于一个桶中的元素较多,即 hash 值相等的元素较多时,经过 key 值依次查找的效率较低。
而 JDK1.8 中,HashMap 采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减小了查找时间。
加载因子(默认0.75):为何须要使用加载因子,为何须要扩容呢?
由于若是填充比很大,说明利用的空间不少,若是一直不进行扩容的话,链表就会愈来愈长,这样查找的效率很低,由于链表的长度很大(固然最新版本使用了红黑树后会改进不少),扩容以后,将原来链表数组的每个链表分红奇偶两个子链表分别挂在新链表数组的散列位置,这样就减小了每一个链表的长度,增长查找效率。
构造 hash 表时,若是不指明初始大小,默认大小为 16(即 Node 数组大小 16),若是 Node[] 数组中的元素达到(填充比 * Node.length)从新调整 HashMap 大小 变为原来 2 倍大小,扩容很耗时
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
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;
}
/*把新表的长度设置为旧表长度的两倍,newCap=2*oldCap*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
/*把新表的门限设置为旧表门限的两倍,newThr=oldThr*2*/
newThr = oldThr << 1; // double threshold
}
/*若是旧表的长度的是0,就是说第一次初始化表*/
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);
}
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;//把新表赋值给table
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)//说明这个node没有链表直接放在新表的e.hash & (newCap - 1)位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
/*若是e后边有链表,到这里表示e后面带着个单链表,须要遍历单链表,将每一个结点重*/
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;//记录下一个结点
//新表是旧表的两倍容量,实例上就把单链表拆分为两队,
             //e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对
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) {//lo队不为null,放在新表原位置
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {//hi队不为null,放在新表j+oldCap位置
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
复制代码
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
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;
}
/*把新表的长度设置为旧表长度的两倍,newCap=2*oldCap*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
/*把新表的门限设置为旧表门限的两倍,newThr=oldThr*2*/
newThr = oldThr << 1; // double threshold
}
/*若是旧表的长度的是0,就是说第一次初始化表*/
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);
}
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;//把新表赋值给table
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)//说明这个node没有链表直接放在新表的e.hash & (newCap - 1)位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
/*若是e后边有链表,到这里表示e后面带着个单链表,须要遍历单链表,将每一个结点重*/
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;//记录下一个结点
//新表是旧表的两倍容量,实例上就把单链表拆分为两队,
             //e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对
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) {//lo队不为null,放在新表原位置
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {//hi队不为null,放在新表j+oldCap位置
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
复制代码
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; /*若是table的在(n-1)&hash的值是空,就新建一个节点插入在该位置*/ if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); /*表示有冲突,开始处理冲突*/ else { Node<K,V> e; K k; /*检查第一个Node,p是否是要找的值*/ 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); //若是冲突的节点数已经达到8个,看是否须要改变冲突节点的存储结构,                          //treeifyBin首先判断当前hashMap的长度,若是不足64,只进行 //resize,扩容table,若是达到64,那么将冲突的存储结构为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } /*若是有相同的key值就结束遍历*/ if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } /*就是链表上有相同的key值*/ if (e != null) { // existing mapping for key,就是key的Value存在 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue;//返回存在的Value值 } } ++modCount; /*若是当前大小大于门限,门限本来是初始容量*0.75*/ if (++size > threshold) resize();//扩容两倍 afterNodeInsertion(evict); return null; } 复制代码
关于 HashMap 线程不安全这一点,《Java 并发编程的艺术》一书中是这样说的
HashMap 在并发执行 put 操做时会引发死循环,致使 CPU 利用率接近 100%。由于多线程会致使 HashMap 的 Node 链表造成环形数据结构,一旦造成环形数据结构,Node 的 next 节点永远不为空,就会在获取 Node 时产生死循环。
其实死循环并非发生在put操做时,而是发生在扩容时。详细的解释能够看下面几篇博客:
Java HashMap 的死循环
HashMap在java并发中如何发生死循环
如何线程安全的使用 HashMap?
//Hashtable
Map<String, String> hashtable = new Hashtable<>();
//synchronizedMap
Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
//ConcurrentHashMap
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
复制代码
具体实现原理在后续文章中会有所涉猎,请持续关注。
以后会就行更新 Java 中的集合相关内容,我会根据内容多少决定分几篇文章去讲,大体内容如我整理脑图
为避免失联或想第一时间查看个人文章更新,可关注个人微信公众号 KevenZheng ,以后会陆续更新上述目录的内容,敬请关注。
如需转载,请联系我或注明出处!