1. JDK 1.7中的HashMap
jdk1.8中的HashMap底层原理是在jdk1.7的基础上改进的,所以,建议先了解jdk1.7的底层原理,先熟悉jdk1.7 的源码。
首先大致了解一下HashMap的实现原理,1.7中HashMap是采用数组加链表的方式实现的,因为HashMap存储元素是按key和value 的形式存储,HashMap中就定义了一个Entry类来封装元素的key和value,并且实现链表结构,定义一个Entry类型的数组,向HashMap中put元素时,首先算出key的哈希值hashCode,然后根据hashCode,再用一个算法(下面会讲),将hashCode 与数组的索引一一对应,不同的元素可能算出的是同一个索引(哈希碰撞),就将相同索引的元素以链表的形式存储(链表法),1.7中采用插入头结点的方式,也就是说,当一个元素插入对应的数组下的链表时,将这个结点变为头结点,arr[i] = entry。
1.1 Entry 类
首先要理解这个Entry类,要不然下面的源码都看不懂。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
1.2 重要属性
- static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
默认数组大小为16
- static final int MAXIMUM_CAPACITY = 1 << 30
数组的最大容量,一般情况下只要内存够用,哈希表不会出现问题
- static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认的负载因子。因此初始情况下,当存储的所有节点数 > (16 * 0.75 = 12 )时,就会触发扩容。
默认负载因子(0.75)在时间和空间成本上提供了很好的折衷,元素个数与数组长度的比值,也就是说,当元素个数不超过数组长度*负载因子时,hashMap效率高。这个数如果过大,存的数越多,空间利用率较高,冲突的机会就会变大,这个值过小,冲突的机会小,空间浪费。如果初始容量大于最大条目数除以负载因子,rehash操作将不会发生。
- static final Entry<?,?>[] EMPTY_TABLE = {};
空的Entry数组,jdk1.7中,建了一个Entry类,来封装key和value
- transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
数组,table默认是空的,HashMap采用的是懒加载模式,只有往HashMap里存放数据时,才会进行初始化。
- transient int size;
元素个数
- int threshold;
扩容阈值 threshold = capacity * loadFactor
- final float loadFactor;
加载因子,默认为0.75f;
1.3 构造函数
public HashMap(int initialCapacity, float loadFactor) {
//如果初始值为0,就报异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//阈值和设置的数组长度相同
threshold = initialCapacity;
//HashMap中,init()是空的,用不到
init();
}
还有其他的构造函数,基本上都是调用上面的构造函数俩完成的,在这里就不列举了。
1.4 put()方法
大致步骤如下:
- 首先判断数组是否为空,如果等于空就调用inflateTable(int toSize)进行初始化数组;
- 判断key值是否为空,如果HashMap中含有key为空的元素就将,oldValue覆盖,返回oldValue,如果没有,,就在table[0]位置添加一个key为null的Entry,并且返回null,方法结束;1.7中,key 可以为null
- 如果key不为空的话,调用hash(Object k)方法,计算key对应的hashCode;
- 根据key的hashCode,调用indexFor(int h, int length)方法,来计算出key对应数组中位置的索引;
- 遍历数组中的元素,如果有相同的key,就进行覆盖,返回oldValue,方法结束;
- 如果没有相同的key,就调用addEntry() 方法进行插入;
- 在插入之前,判断是否需要扩容,如果需要就进行扩容,然后再插入元素;
public V put(K key, V value) {
//判断数组是否为空,如果等于空,就初始化数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//判断键值是否为空
if (key == null)
return putForNullKey(value);
//算出key对应的hashCode
int hash = hash(key);
//根据hashCode找到对应的索引
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;
}
inflateTable()方法
inflateTable(threshold)方法用来初始化数组, inflateTable(threshold)方法中,利用roundUpToPowerOf2(toSize)方法计算出刚好大于阈值threshold并且是2的次方的数,比如,threshold为10,则capacity为16,也就是数组的长度为16,这里你肯定会好奇为什么数组长度得是2的次方数,先留着这个问题,继续看
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);
//重新计算阈值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
putForNullKey()方法
1.7中,key可以为空,如果HashMap中含有key为空的元素就将,oldValue覆盖,返回oldValue,没有找到的话,就在table[0]位置添加一个key为null的Entry,调用addEntry()方法
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
计算hash值以及对应的索引
计算key的哈希值,根据哈希值算出对应的数组索引,首先看索引的计算方法,hash值与数组长度减1的与运算,比如一个key的hashCode是20,计算索引时,20 & 15 , 一个int类型的数占4个字节,32位,也就是0000 0000 0000 0000 0000 0000 0001 0100 & 0000 0000 0000 0000 0000 0000 0000 1111 ,结果为 1111(前面的值省略),可以看出,hash值的二进制数的后四位是什么,得出的索引就是什么,只要数组长度为2的次方,算出的索引数肯定小于数组长度,这也是为什么,初始化数组长度为2的n次方,也就是说,索引的值只与后几位数有关,前面很多位都没有参与到计算中,这样的话,得到的索引相同的肯定很多,于是,HashMap中,就将hashCode就往左移,进行多次的异或运算,这样,算出的索引值,不仅仅只与后几位数字有关了。
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
//计算索引
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
addEntry方法(判断是否需要扩容,然后添加节点Entry)
执行流程:
- 判断是否需要扩容,size(每次添加一个entry size++)>=threshold(阈值)并且当前这个key的hash算出的位置必须有元素才扩容
- 如果满足扩容条件,调用扩容方法resize(2 * table.length),table长度扩大2倍,然后重新算当前key的hash和位置bucketIndex.
- 调用createEntry()方法,添加节点.
// 添加节点到链表
void addEntry(int hash, K key, V value, int bucketIndex) {
/*
* 扩容机制必须满足两个条件
* (1) size大于等于了阈值
* (2) 到达阈值的这个值有没有发生hash碰撞
* 所以阈值在默认情况下是12 是一个重要节点
* 扩容范围是12-27
* 最小12进行扩容,最大27时必须进行扩容
* 分析最小12扩容
* 当size是12时,判断有没有hash碰撞,有扩容,没有继续不扩容.
* 分析最大27扩容
* 当12没有进行扩容时,size大于阈值就一直满足了
* 就只需要判断接下来的hash有没碰撞,有就扩容,没有就不扩容
* 最大是一种极端情况,前面11个全部在一个table索引上,接下来
* 15个全部没有碰撞,11+15=26,table所有索引全部有值,在插入一个
* 值必须碰撞就是26+1=27最大进行扩容
* */
if ((size >= threshold) && (null != table[bucketIndex])) {
// 扩容(方法里面重点讲)
resize(2 * table.length);
// 计算hash,null时为0
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];//取到数组的位置的entry
table[bucketIndex] = new Entry<>(hash, key, value, e);//新entry加到链表的头部,并把数组指向新entry
size++;
}
1.5 扩容
执行流程:
(1) 计算oldTable的长度,如果oldTable的长度已经是最大值了,那么就把阈值设置成Integer.MAX_VALUE,return.
(2) 根据新的容量创建table.
(3) 调用transfer方法转移数据.
(4) 将新table赋值给旧table,重新计算阈值.
//对数组进行扩容
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 如果之前的HashMap已经扩充到最大了,那么就将临界值threshold设置为最大的int值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 根据新传入的newCapacity创建新Entry数组
Entry[] newTable = new Entry[newCapacity];
// 用来将原先table的元素全部移到newTable里面,重新计算hash,然后再重新根据hash分配位置
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 再将newTable赋值给table
table = newTable;
// 重新计算临界值,扩容公式在这儿(newCapacity * loadFactor)
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];
newTable[i] = e;
e = next;
}
}
}
1.7 扩容问题
数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这个操作是极其消耗性能的。所以如果我们已经预知HashMap中元素的个数,那么预设初始容量能够有效的提高HashMap的性能。
重新调整HashMap大小,当多线程的情况下可能产生条件竞争。因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了,出现循环链表,HashMap扩容死循环问题源码分析。
1.8 get()方法(通过key获取数据)
执行流程:
- 判断key是否为空,如果为空的话,就调用getForNullKey()方法;
- 不为空的话,调用getEntry()方法来获取Entry对象;
- 最后判断entry是否为空,为空返回null,不为空返回entry中的value值;
// get方法
public V get(Object key) {
// key等于null
if (key == null)
return getForNullKey();
// 不为null是查找
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
getForNullKey()方法(遍历table[0]位置数据,找到key==null的返回)
private V getForNullKey() {
// 没数据
if (size == 0) {
return null;
}
// 从table[0]处遍历链表,找到key=null的返回
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
getEntry()方法(根据hash算出位置,遍历当前位置的数据,找到key和hash相同的返回)
final Entry<K,V> getEntry(Object key) {
// 没数据
if (size == 0) {
return null;
}
// 获取hash
int hash = (key == null) ? 0 : hash(key);
// 获取table的位置,找到hash和key相同的返回
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
2. 解决哈希碰撞的算法
• 开放定址法
开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
• 链地址法
将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该j结点链在以该单元为头结点的链表的尾部,HashMap中采用的就是这种方法。
• 再哈希法
当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。
• 建立公共溢出区
将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。
3. jdk1.8中的HashMap
1.7中,虽然采取了好几个措施比如扩容来尽量减少某一个链表会很长的情况出现,但是,还有可能出现某一个链表的长度很长,所以1.8中,为了彻底解决这个问题,HashMap采用的是数组+链表+红黑树的形式。
3.5 put()方法
大致流程:
-
判断当前table有没有初始化,没有就调用resize()方法初始化.(resize方法既是扩容也是初始化)
-
判断算出的位置i处有没有值,没有值,创建一个新的node,插入i位置.
-
当前i位置有值,判断头结点的hash和key是否和传入的key和hash相等,相等则记录这个e.
-
与头节点的key和hash不同,判断节点是否是树节点,如果是,调用树节点的插入方法putTreeVal()方法.
-
不是树结构,那证明是链表结构,遍历链表结构,并记录链表长度binCount.主要做了两步:
在链表里找到和传入的key和hash相等的基点,并记录到p
如果没有找到,创建一个节点,插入链表的尾部,并判断链表长度有没有大于等于8,如果是就调treeifyBin方法决定是否需要树化. -
判断前面记录的e节点是否为空,不为空证明找到了相同的基点,那就替换value,返回oldValue.
-
整个插入流程已经结束,接下来要判断是否需要扩容,如果(++size > threshold)满足,那么就调用扩容方法resize();
jdk 1.8中,采用的是尾插法,这样可以避免扩容的时候,出现死循环的情况。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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[i]为空,就把这个值放到数组中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果不为空,但是要插入的元素与table[i]的哈希值相等或者==并且equals的话,就将table[i]赋给e,然后最后对e进行判断,
//如果不为空,就把原来的值给覆盖,并且返回
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(tab, hash),treeifyBin这个方法中还进行了判断,
//如果数组等于空或者数组的长度小于64,就把数组扩容,而不是直接转换成红黑树,因为扩容之后还可以放更多元素,
//链表还是很短
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//判断链表中的除第一个以外的元素是否与要插入的元素key相等,如果有,就结束循环,没的话,就把e(null)赋给p,
//进行下次循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//判断e是否为空,如果不为空,就说明,HashMap中有相同的key,如果为空,就没有相同的key
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;
}
3.6 resize()方法(初始化和扩容都是创建新的table)
大致流程:
- 获得老table数据,如果oldCap>0,说明已经初始化过了,进行扩容,如果老table已经足够大则不再扩容,只调节阈值。
- 老table扩容后的范围也符合要求直接将容器大小跟阈值都扩容,扩为原来的两倍 。
- 如果oldCap不大于0,说明是初始化操作,如果oldThr>0,说明是带参数构造函数则需要将阈值复制给容器容量。
- 否则认为该容器初始化时未传参,需初始化。
- 如果老table有数据,新table大小设置好了但是阈值没设置成功。此时要设置新阈值。
- 创建新容器。
- 老table成功扩容为新table,进行数据的转移。
数据不为空并且是单独的节点则直接重新hash分配新位置。
数据不为空并且后面是一个链表,则要把链表数据进行区分看哪些分到老地方哪些分到新地方。
如果该节点类型是个红黑树则调用split.
1.7中是先扩容后进行插入,1.8中是先插入元素后进行扩容,另外1.8中初始化数组和扩容都是利用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;
//oldCap如果大于0,就是对元素组进行扩容
if (oldCap > 0) {
//如果数组已经是最大了,就不扩容了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果不是最大数组,就扩为原来的两倍,oldCap >= 16时,阈值也扩大为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//下面的是初始化操作
//oldCap = 0,oldThr > 0,进入此if说明创建map时用的是带参的构造函数,
//public HashMap(int initialCapacity, float loadFactor) 或者public HashMap(int initialCapacity),
//带参的构造中,initialCapacity,初始容量值,不管输入几,都会使用tableSizefor()方法
//计算出接近initialCapacity并且是2的n次幂的数来作为初始化容量(初始化容量 == oldThr),
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//都等于0的话,就调用无参的构造函数,
//然后将参数newCap(新的容量)、newThr(新的阈值)初始化
else {
// zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) { //判断新的阈值是否为0,如果为0的话,说明使用的是有参的构造函数,容量有值,阈值为0
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;//把新的阈值赋给threshold
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //创建新的数组
table = newTab;//把新的数组赋给table
//下面就是将老的数组中的元素移到新的数组中
if (oldTab != null) {//如果原来的table有数据
//遍历旧的table,进行数据迁移
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
//进行链表复制,
//方法比较特殊:它并没有根jdk 1.7一样重新计算元素在数组中的位置
//而是通过(e.hash & oldCap) == 0来判断元素的位置,如果等于0,说明没有结点位置没变,
//不等于0,说明结点位置为原来的位置+oldCap。
// 链表结构的数据 lo 表示位置不变 hi表示位置是原来的位置+oldCap
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);
//位置不变还是j,将链表的头结点放在数组上
if (loTail != null) {
loTail.next = null; // 尾部节点的next设置为null
newTab[j] = loHead; //设置头结点指向table[j] (第一次循环时,head=tail,接下来循环给tail追加节点)
}
//位置变化是j+oldCap,将链表的头结点放在数组上
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
//jdk1.8中,元素迁移,使用的是尾插法,元素在链表中的相对位置没有发生变化
//jdk1.7中,使用的是头插法,元素位置发生了变化。
}
}
}
}
return newTab;
}
3.7 关于链表元素迁移时位置的计算
- 这里如果(e.hash & oldCap) == 0成立,那么该元素的地址在新的数组中就不会改变。因为oldCap的最高位的1,在e.hash对应的位上为0,所以扩容后得到的地址是一样的,位置不会改变 ,在后面的代码的执行中会放到loHead中去,最后赋值给newTab[j];
- 如果判断不成立,那么该元素的地址变为 原下标位置+oldCap,也就是lodCap最高位的1,在e.hash对应的位置上也为1,所以扩容后的地址改变了,在后面的代码中会放到hiHead中,最后赋值给newTab[j + oldCap]
- 举个栗子来说一下上面的两种情况:
设:oldCap=16 二进制为:0001 0000
oldCap-1=15 二进制为:0000 1111
e1.hash=10 二进制为:0000 1010
e2.hash=26 二进制为:0101 1010
e1在扩容前的位置为:e1.hash & oldCap-1 结果为:0000 1010
e2在扩容前的位置为:e2.hash & oldCap-1 结果为:0000 1010
结果相同,所以e1和e2在扩容前在同一个链表上,这是扩容之前的状态。
现在扩容后,需要重新计算元素的位置,在扩容前的链表中计算地址的方式为e.hash & oldCap-1
那么在扩容后应该也这么计算呀,扩容后的容量为oldCap*2=32 0010 0000 newCap=32,新的计算方式应该为
e1.hash & newCap-1
即:0000 1010 & 0001 1111
结果为0000 1010与扩容前的位置完全一样。
e2.hash & newCap-1
即:0101 1010 & 0001 1111
结果为0001 1010,为扩容前位置+oldCap。
而这里却没有e.hash & newCap-1 而是 e.hash & oldCap,其实这两个是等效的,都是判断倒数第五位是0,还是1。如果是0,则位置不变,是1则位置改变为扩容前位置+oldCap。
4. 相关面试题
4.1 JDK1.7和1.8中HashMap的区别
- 1.7 中使用数组加链表的方式实现,1.8中使用数组加链表加红黑树的方式实现,当链表长度大于等于8,并且元素个数大于等于64的时候会将链表转换为红黑树。
- 1.7 中使用的是头插法,1.8中使用的是尾插法
4.1 JDK1.8中为什么要采用红黑树而不采用其他树呢?
因为,对于我们的HashMap来说,即要保证插入的效率(put)也要保证查询的效率(get),链表的插入效率高,但是结点很多的时候查询的效率不高,而红黑树对这两种操作,比较均衡,插入和查询的效率都可以,是一个比较折中的方法,是基于一个综合角度来考虑的。当一个链表中的元素超过或者等于8个时,并且这个时候数组的长度大于64,这个时候就要把链表改成红黑树,因为当链表中元素大于或等于8个时,查询的效率不高,当红黑树只有6个结点,就把红黑树改成链表。
4.2 JDK1.8中为什么hashmap为什么不直接就用红黑树呢?
因为树节点所占空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。也就是说,节点少的时候,尽管时间复杂度上,红黑树比链表好一点,但是红黑树所占空间比较大,综合考虑,认为只能在节点太多的时候,红黑树占空间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树,以空间换时间。
4.3 JDK1.8中链表转换为树为什么是8,而不是7、9等等呢?
受随机分布的hashCode影响,链表中的节点频率遵循泊松分布,根据统计,链表中节点数是8的概率已经接近千万分之六,大于8的概率更小。
4.4 JDK1.8中链表和树之间的转换为什么一个是8,一个是6呢?
因为这是为了防止频繁的插入和删除元素,比如,当一个链表中有7个元素,你插入一个元素,把链表改成红黑树,如果删除的话,就变成链表,这样的话,影响HashMap的效率。
本人第一次写博客,有不足的地方或者写的不好的地方,欢迎指正。