基本概念
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。另外,HashMap是非线程安全的,也就是说在多线程的环境下,可能会存在问题,而Hashtable是线程安全的。
主要参数
/**
* The default initial capacity - MUST be a power of two.
* 默认初始容量-必须为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.
* 最大容量,如果两个构造函数都使用参数隐式指定了更高的值,则使用该容量。 必须是两个<= 1 << 30的幂。
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
* 在构造函数中未指定时使用的负载系数。(当表容量达到3/4时才会再散列)
*/
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.
* 使用树而不是列表列出容器的容器计数阈值。 将元素添加到至少具有这么多节点的bin中时,bin会转换为树。 该值必须大于2,并且至少应为8才能与树删除中的假设(收缩时转换回普通箱)相啮合。
* 当链表的长度 >= 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.
* 在调整大小操作期间用于取消树状化(拆分)bin的bin计数阈值。 应小于TREEIFY_THRESHOLD,并且最大为6以与移除下的收缩检测相啮合。
* 在resize()扩容的时候,HashMap的数据存储位置会重新进行计算,在重新计算存储的位置后,当原有的红黑树数量 <= 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.
* 可将其分类为树木的最小桌子容量。(否则,如果bin中的节点过多,则将调整表的大小。)应至少为4 * TREEIFY_THRESHOLD,以避免调整大小和树化阈值之间的冲突。
* 为了避免扩容/调整树化阀值之间的冲突,这个值不能 < 4 * TREEIFY_THRESHOLD。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
默认负载因子(DEFAULT_LOAD_FACTOR)值为什么是0.75?
官方解释:
通常,默认的加载因子(0.75)在时间和空间成本之间提供了一个很好的方案。较高的值会减少空间的开销,但会增加查找的成本(在HashMap类的大多数操作中都能得到体现,包括最常用的( get() 操作和 put() 操作)。在设置映射表的初始容量的时候,应该考虑映射中预期的 Entry 数及其负载因子,以最大程度地减少 rehash(重新哈希)操作的次数。如果初始容量大于最大条目数 / 负载因子 ,则将不会进行任何哈希操作; 如果要将许多映射存储到HashMap实例中,则创建具有足够大容量的映射将比使它根据需要增长表的自动重新哈希处理更有效地存储映射。
构造方法
HashMap提供四个构造方法:
1.public HashMap(int initialCapacity, float loadFactor)
/** * Constructs an empty <tt>HashMap</tt> with the specified initial capacity and load factor. * 使用指定的初始容量和负载因子构造一个空的<tt> HashMap * @param initialCapacity the initial capacity 初始化容量 * @param loadFactor the load factor 加载因子 * @throws IllegalArgumentException if the initial capacity is negative or the load factor is nonpositive * 异常:如果初始容量小于零或者加载因子小于等于零 或者 加载因子是NaN 则抛出异常 */ public HashMap(int initialCapacity, float loadFactor) { //当指定的 initialCapacity (初始容量) < 0 的时候会抛出 IllegalArgumentException 异常 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //当指定的 initialCapacity (初始容量)= MAXIMUM_CAPACITY(最大容量) 的时候 if (initialCapacity > MAXIMUM_CAPACITY) //初始容量就等于 MAXIMUM_CAPACITY (最大容量) initialCapacity = MAXIMUM_CAPACITY; //当 loadFactory(负载因子)< 0 ,或者不是数字的时候会抛出 IllegalArgumentException 异常 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; //tableSizeFor()的主要功能是返回一个比给定整数大且最接近2的幂次方整数 //比如我们给定的数是12,那么tableSizeFor()会返回2的4次方,也就是16,因为16是最接近12并且大于12的数 this.threshold = tableSizeFor(initialCapacity); }
前面的初始化代码容易理解,接下来看tableSizeFor()方法,tableSizeFor()源码如下:
/**
* Returns a power of two size for the given target capacity.
* 对于给定的目标容量,返回两倍大小的幂。
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
首先明白 >> 与 >>> 的区别:
1、>> :按二进制形式把所有的数字向右移动对应位数,低位移出(舍弃),高位的空位补符号位,即正数补零,负数补1。符号位不变。2、>>> : 二进制右移补零操作符,左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充。
分析tableSizeFor()方法:
int n = cap - 1;
假设cap = 10,则n = cap - 1 = 9
则 n |= n >>> 1为:
n ------------- 00001001 n >>> 1 ------------- 00000100 n |= n >>> 1 《==》即n = n | n >>> 1 ------------- 00001101
进而 n |= n >>> 2(这里的 n 是接着上个 n 的值)为:
n ------------ 00001101 n >>> 2 ------------ 00000011 n |= n >>> 2 《==》即n = n | n >>> 2 ------------ 00001111
由于此时 n |= n >>> 2 为:00001111, 第一个1后面所有位均为 1,后续的操作均不会改变 n 的值,十进制均为15。
由此得出规律如下:
假定n的二进制形式为00..01XXXXXXX,(X代表可为0也可为1,X前面的1为从最高位开始第一个为1的那一位)
第一步: n |= n >>> 1; 也就是n变为n与n右移一位之后异或运算后的值,即
n 00..01XXXXXXX n>>>1 00..001XXXXXX n |= n >>> 1 00..011XXXXXX
第二步: n |= n >>> 2; 也就是n变成n与n右移两位之后异或运算的值,即
n 00..011XXXXXX n>>>2 00..00011XXXX n |= n >>> 2 00..01111XXXX
后面相类似
n 00..01XXXXXXX n >>> 1 00..011XXXXXX n |= n >>> 1 00..011XXXXXX n >>> 2 00..00011XXXX n |= n >>> 2 00..01111XXXX n >>> 4 00..000001111 n |= n >>> 4 00..011111111 n >>> 8 00..000000000 n |= n >>> 8 00..011111111 n >>> 16 00..011111111 n |= n >>> 16 00..011111111
得出方法tableSizeFor()的功能是:返回一个大于等于且最接近 cap 的2的幂次方整数。
为什么容量必须为2的整数幂?
因为获取 key 在数组中对应的下标是通过 key 的哈希值与数组长度 -1 进行与运算,如:i = (n - 1) & hash
1、n 为 2 的整数次幂,这样 n-1 后之前为 1 的位后面全是 1,这样就能保证 (n-1) & hash 后相应的位数既可能是 0 又可能是 1,这取决于 hash 的值,这样能保证散列的均匀,同时与运算效率高
2、如果 n 不是 2 的整数次幂,会造成更多的 hash 冲突
在Java8中,HashMap中key的hash值是由hash(key)方法计算的,hash方法代码如下:
计算key.hashCode()并将哈希的较高位(XOR)扩展为较低。 因为该表使用2的幂次掩码,所以仅在当前掩码
上方的位中变化的哈希集将始终发生冲突。 (众所周知的示例是在小表中包含连续整数的Float键集。)因此,
我们应用了一种变换,将向下传播较高位的影响。 在速度,实用性和位扩展质量之间需要权衡。 由于许多常见的
哈希集已经合理分布(因此无法从扩展中受益),并且由于我们使用树来处理容器中的大量冲突集,因此我们仅以
最便宜的方式对一些移位后的位进行XOR,以减少系统损失, 以及合并最高位的影响,否则由于表范围的限制,这
些位将永远不会在索引计算中使用。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里使用异或运算的好处是可以将hashcode高位和低位的值进行混合做异或运算,而且混合后,低位的信息中加
入了高位的信息,这样高位的信息被变相的保留了下来。掺杂的元素多了,生成的hash值的随机性就越大
HashMap中存储数据的table的index是由key的hash值决定的,在HashMap中存储数据的时候,我们希望数据能够均匀地分布,以防止哈希冲突自然地我们就会想到用 % 取余操作来实现。
取余(%)操作:如果除数是2的幂次方则等价于其除数减一的与操作(&)
即:X % 2^n = X & (2^n – 1)
假设n为3,则2^3 = 8,表示成2进制就是1000。2^3 -1 = 7 ,即0111。
此时X & (2^3 – 1) 就相当于取X的2进制的最后三位数。
从2进制角度来看,X / 8相当于 X >> 3,即把X右移3位,此时得到了X / 8的商,而被移掉的部分(后三位),则是X % 8,也就是余数。
如:
6 % 8 = 6 ,6 & 7 = 6
10 % 8 = 2 ,10 & 7 = 2
resize()方法的摘录:
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
其中:e.hash & (newCap - 1) 等价于 index = e.hash % newCap
这就是为什么一定要求cap要为2的幂次方的原因 。
2.public HashMap(int initialCapacity)
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial capacity and the default load factor (0.75).
* 构造一个空的<tt> HashMap </ tt>,它具有指定的初始容量和默认的负载系数(0.75)
*
* @param initialCapacity the initial capacity. 指定的初始容量
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
//调用第一个构造方法
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
3.public HashMap()
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity (16) and the default load factor (0.75).
* 使用默认的初始容量构造一个空的HashMap(16)和默认负载系数(0.75)。
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
4.public HashMap(Map<? extends K, ? extends V> m)
源码:
//构造一个和指定Map有相同的mappings的HashMap,初始容量能充足的容下指定的Map,负载因子为0.75
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
putMapEntries(m,false)方法源码:
//将m的所有元素存入到该HashMap实例中
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取 m 中元素的个数
int s = m.size();
//当 m 中有元素的时候,需要将Map中的元素放入此HashMap实例中
if (s > 0) {
//判断table是否已经初始化,如果table已经初始化,则先初始化一些变量(table的初始化指在put的时候)
if (table == null) { // pre-size
//根据待插入的Map的大小(size)计算要创建的 HashMap 容量
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
//将要创建的 HashMap 容量存在 threshold 中
if (t > threshold)
//threshold是一个整型变量
threshold = tableSizeFor(t);
}
//如果table初始化过了,因为别的函数也会调用到它,所以有可能HashMap已经被初始化过了
//判断待插入的 Map 的 大小(size),如果size > threshold ,则先进行 resize() 扩容
else if (s > threshold)
resize();
//然后开始遍历,待插入的 Map ,将每一个<Key,Value> 插入到该HashMap实例中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
//然后调用 putVal 函数进行元素的插入操作
putVal(hash(key), key, value, false, evict);
}
}
}
根据源码可以知道,如果table为null的话,则需要初始化table,计算得出需要的容量值,如果容量值大于默认的,那么就用tableSizeFor()方法计算新的容量值并赋值给threshold;如果table不为null并且map的size > threshold,则需要resize()进行扩容。最后再把指定的Map进行遍历,将所有(Key、Value)通过putVal()方法添加到我们创建的新的Map中 。
putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法:
//实现 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;
//如果table为空或者长度为0,则进行resize()(扩容)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//确定插入table的位置,算法是上面提到的 (n - 1) & hash,在 n 为 2 的时候,相当于取模操作
if ((p = tab[i = (n - 1) & hash]) == null)
//找到key值对应的位置并且是第一个,直接插入
tab[i] = newNode(hash, key, value, null);
//在table的 i 的位置发生碰撞,分两种情况
//1、key值是一样的,替换value值
//2、key值不一样的
//而key值不一样的有两种处理方式:1、存储在 i 的位置的链表 2、存储在红黑树中
else {
Node<K,V> e; K k;
//第一个Node的hash值即为要加入元素的hash
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 {
//如果不是TreeNode的话,即为链表,然后遍历链表
for (int binCount = 0; ; ++binCount) {
//链表的尾端也没有找到key值相同的节点,则生成一个新的Node
//并且判断链表的节点个数是不是到达转换成红黑树的上界达到,则转换成红黑树
if ((e = p.next) == null) {
//创建链表节点并插入尾部
p.next = newNode(hash, key, value, null);
//超过了链表的设置长度(默认为8)则转换为红黑树
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;
}
}
//如果e不为空
if (e != null) { // existing mapping for key
//就替换就的值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
putVal()方法中用到了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;
//判断Node的长度,如果不为零
if (oldCap > 0) {
//判断当前Node的长度,如果当前长度超过 MAXIMUM_CAPACITY(最大容量值)
if (oldCap >= MAXIMUM_CAPACITY) {
//新增阀值为 Integer.MAX_VALUE
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果小于这个 MAXIMUM_CAPACITY(最大容量值),并且大于 DEFAULT_INITIAL_CAPACITY (默认16)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
//进行2倍扩容
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//指定新增阀值
newCap = oldThr;
//如果数组为空
else { // zero initial threshold signifies using defaults
//使用默认的加载因子(0.75)
newCap = DEFAULT_INITIAL_CAPACITY;
//新增的阀值也就为 16 * 0.75 = 12
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数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = 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 { // preserve order
//如果节点不为空,且为单链表,则将原数组中单链表元素进行拆分
Node<K,V> loHead = null, loTail = null;//保存在原有索引的链表
Node<K,V> hiHead = null, hiTail = null;//保存在新索引的链表
Node<K,V> next;
do {
next = e.next;
//哈希值和原数组长度进行&操作,为0则在原数组的索引位置
//非0则在原数组索引位置+原数组长度的新位置
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;
}
线程安全问题
HashMap 在并发时可能出现的问题
1、put()操作的时候导致的多线程数据不一致
比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的 hash桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的 hash桶索引和线程B要插入的记录计算出来的 hash桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为
2、resize()而引起死循环
这种情况发生在HashMap自动扩容时,当2个线程同时检测到元素个数超过 数组大小 × 负载因子。此时2个线程会在put()方法中调用了resize(),两个线程同时修改一个链表结构会产生一个循环链表(JDK1.7中,会出现resize前后元素顺序倒置的情况)。接下来再想通过get()获取某一个元素,就会出现死循环
注:JDK7使用是hash值与数组长度-1 这个掩码进行与运算,得到Entry元素的新下标位置,得到的结果直接就是下标位置 ;
Jdk1.8中是使用hash值与 数组长度 进行与运算,得到的是0 或者非零。如果是0表示新位置下标不变,如果不是0那么表示位置有变动,如果有变动下标位置是原位置加上数组长度。
HashMap与HashTable有什么不同?
除了非同步和允许键值为null之外,HashMap和HashTable大致相同
HashMap如何解决线程安全问题?
既然HashMap不是线程安全的,那么如何使它成为线程安全的呢?,通过以下代码就可以使HashMap为线程安全的
HashMap<String,Integer> hashMap = new HashMap<String,Integer>();
Map<String, Integer> map = Collections.synchronizedMap(hashMap);
HashMap与HashTable异同?
相同点
- 都实现了Map接口,保存了Key-Value(键值对)
- 两者的数据结构类似。HashMap和HashTable都是由数组元素为链表头节点的数组组成。
不同点
1.两者继承的父类不同:
HashMap继承AbstractMap类,而HashTable继承Dictionary类。
2.HashMap是线程不安全的,而HashTable是线程安全的:
HashMap在外部没有做同步处理的情况下,是非Synchronize(同步)的,不能保证在多并发条件下对数据的保护,容易产生脏数据;HashTable中的方法是Synchronize的;可以在多并发环境下直接使用HashTable;在单核CPU中HashMap的效率是比HashTable高的,而HashTable更适合在多线程中使用。
3.提供contains:
HashMap没有contians,只有containsKey和containsValue,认为contians会容易引起误解;HashTable保留了contains,containsValue和containsKey方法。(但contains和containsValue的功能是相同的)
4.对key和value的空值允许情况:
HashTable中key和value都不允许为null;HashMap中空值可以作为Key,也可以有一个/多个Key的值为空值;(其中的get()方法返回null的意思产生歧义,可能是该Key上的值为null,也可能是不存在该Key)。
原因(也是HashMap与HashTable的区别之一): 当变量为null时,其hashcode(哈希值)也为null; 而两者对Key和Value的取值方式不同
HashMap中的Key值是直接取变量对应的 hashcode值 作为Key;
HashTable中会通过hash运算计算获取Value对应的Key;5.HashMap和HashTable内部的数组初始化和扩容方式也不相同:
HashMap的hash数组默认长度大小为16,扩容方式为2的指数:
length_HashMap = 16 * 2n(n为扩容次数)
HashTable的hash数组默认长度大小为11,扩容方式为两倍加一:
length_HashTable = 上一次HashTable数组长度 * 2 + 1
HashMap如何解决线程安全问题?
既然HashMap不是线程安全的,那么如何使它成为线程安全的呢?
通过以下代码就可以使HashMap为线程安全的
HashMap<String,Integer> hashMap = new HashMap<String,Integer>();
Map<String, Integer> map = Collections.synchronizedMap(hashMap);
hash冲突问题
为什么会hash冲突?
hash冲突即:根据key经过一个函数f(key)得到的结果作为地址去存放当前的(key:value)键值对(这个是hashmap的存值方式),但是却发现算出来的地址上已经有数据了,这就是所谓的hash冲突。
hash冲突的几种情况
两个节点的key值相同(hash值一定相同),导致冲突
两个节点的key值不同,由于hash函数的局限性导致hash值相同,导致冲突
两个节点的key值不同,hash值不同,但hash值对数组长度取模后相同,导致冲突
如何解决hash冲突?
解决hash冲突的方法主要有两种,一种是开放寻址法,另一种是链表法
开放寻址法
开放寻址法的原理很简单,就是当一个Key通过hash函数获得对应的数组下标已被占用的时候,可以寻找下一个空档位置
链表法
链表法也正是被应用在了HashMap中,HashMap中数组的每一个元素不仅是一个Entry对象,还是一个链表的头节点。每一个Entry对象通过next指针指向它的下一个Entry节点。当新来的Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中即可
jdk1.7与1.8的HashMap的差异?
一、JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法
那么为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法就是能够提高插入的效率,但是也会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题
二、扩容后数据存储位置的计算方式不一样
在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式
三、结构不同
JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(N)变成O(logN)提高了效率)
四、性能
JDK8中resize方法并没有比JDK7中性能更好。Entry的key最坏的情况下在Map中是一个链表,JDK8为优化这个问题在链表数目大于8的时候转化为红黑树,但是resize中,又必需拆解和重建红黑树。
拆解过程
和普通的Entry链表一样,顺序遍历(此时只用它的next指针),使用上述判断方式,分离成需要变动和不需要变动的两个列表。
重建过程
如果列表长度小于8,去掉树结构指针,维持成一个链表
如果列表长度大于8,重新构造成一棵树。
总的来看,JDK7的resize时间复杂度n,JDK8的复杂度为nlog(n)