HashMap原理
预备知识#
二进制运算#
<<
左移
高位溢出,低位补零。
eg:010100 <<2 得:010000
>>
右移
若移动前的二进制为正数,右移后低位溢出,高位补零;若移动前的二进制位负数,右移后低位溢出 ,高位补1;
eg:010010 >>2 得:000100
110010 >>2得: 111100
>>>
无符号右移
>>>
表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0。
^
异或运算
参加运算的两个数据,按二进制位进行“异或”运算。
运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0;
即:参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为 0。
&
与运算
运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
即:两位同时为“1”,结果才为“1”,否则为0。
例如:3&5 即 0000 0011 & 0000 0101 = 0000 0001 因此,3&5的值得1。
JDK1.7中的HashMap#
在JDK1.7中,HashMap底层是通过数组+链表的形式来实现的。
关键属性#
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable{
/**
* The default initial capacity - MUST be a power of two.
*/
//默认初始容量-必须是2的幂。
//代表HashMap的初始化容量:16(1 << 4 等于二进制10000转换为十进制为16)
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.
*/
//最大容量,如果隐式指定了更高的值由任何一个带参数的构造函数调用。必须是2的幂<= 1<<30。
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
//默认加载因子:当构造函数中没有指定时使用的加载因子。 也就是四分之三
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* An empty table instance to share when the table is not inflated.
*/
//空实例
static final Entry<?,?>[] EMPTY_TABLE = {};
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
//HashMap表格,根据需要调整大小。长度必须总是2的幂。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/**
* The number of key-value mappings contained in this map.
*/
//此映射中包含的键-值映射的数目。HashMap中元素个数。
transient int size;
//******以下省略******
}
HashMap中的Entry对象,代表每一个元素。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //Key
V value; //Value
Entry<K,V> next; //链表的下一个元素
int hash; // key的hash值
}
构造方法#
在jdk1.7里,HashMap的构造方法没有什么复杂的逻辑,大部分都是一些验证和默认值的初始化。
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
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;
init();//此处init()方法,在HashMap中是一个空方法,但在LinkedHashMap中有具体的实现。
}
put方法#
public V put(K key, V value) {
//如果HashMap为空,则进行初始化 见P1
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//key为null时处理逻辑 见P2
if (key == null)
return putForNullKey(value);
//计算出当前key的hash值 见P3
int hash = hash(key);
//根据hash和数组容量 计算出当前key的数组下标 见P4
int i = indexFor(hash, table.length);
//此处为链表遍历 见P5
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++;
//元素新增操作(及扩容操作) 见P6
addEntry(hash, key, value, i);
return null;
}
P1 inflateTable(threshold);
/**
* Inflates the table.
*/
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//找到一个大于等于toSize的2的幂次方数
//为什么在HashMap里面数组的大小一定是2的幂次方数?
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
P2 putForNullKey(value);
数组下标为0的位置 固定的存储key为null的值。
/**
* Offloaded version of put for null keys
*/
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;
}
P3 int hash = hash(key);
可以看到代码中对key的hashcode进行了大量的异或、右移操作。
/**
* Retrieve object hash code and applies a supplemental hash function to the
* result hash, which defends against poor quality hash functions. This is
* critical because HashMap uses power-of-two length hash tables, that
* otherwise encounter collisions for hashCodes that do not differ
* in lower bits. Note: Null keys always map to hash 0, thus index 0.
*/
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);
}
在计算hash值的时候:为什么不直接使用key的hashCode(),而是要进行右移和异或^操作?
TODO
P4 int i = indexFor(hash, table.length);
该方法是计算出当前key的数组下标。
/**
* Returns index for hash code h.
*/
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);
}
我们知道int
类型 占4个字节,共32位。
如果按照数组默认初始容量16来看,h & (length-1)
计算出来的数组下标一定是0到15之间吗?
此处int h
为当前key计算后的hash值,我们假设h为85,默认初始容量为length为16:
如果数组容量不为2的幂次方数会出现什么问题呢?
此处我们假设h为85,数组容量table.length为17:
为什么在HashMap里面数组的大小一定是2的幂次方数?
因为这类数字转换为二进制以后,它可以保证只有在某一个bit位上面是1,进而Length - 1的值的二进制所有位均为1,这种情况下,Index的结果就等于hashCode的最后几位。只要输入的hashCode本身符合均匀分布,Hash算法的结果就是均匀的。
HashMap的长度为2的幂次方的最终原因是为了减少Hash碰撞,尽量使Hash算法的结果均匀分布。
在计算key的数组下标时,为什么要用&操作?不用%操作?
在jdk1.4里面,在某些操作系统上取余操作%是比较慢的,而与操作&是直接基于bit位来运行的,效率是非常高的。
P5 链表遍历
我们先看这段代码:
public static void main(String[] args) {
HashMap<Object, Object> hashMap = new HashMap<>();
Object put1 = hashMap.put(1, 1);
Object put2 = hashMap.put(1, 2);
System.out.println(put1);
System.out.println(put2);
}
输出:
null
1
以上这段代码的逻辑就对应源码中链表遍历的逻辑。
//链表遍历
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;
}
}
hash值不相等,那么两个对象不相等。
hash值相等,两个对象不一定相等。
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
这段代码key.equals(k)
是放到最后的,为什么这么做呢?
在某些情况下,我们会重写这个对象的equals方法,并且这个equals方法的逻辑会比较复杂,那么它的效率就比较慢,所以将equals方法放到最后。
P6 addEntry(hash, key, value, i);
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
//扩容操作
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//添加元素的操作
createEntry(hash, key, value, bucketIndex);
}
扩容操作
扩容是指扩容数组。
void addEntry(int hash, K key, V value, int bucketIndex) {
//扩容操作
//当hashMap中的元素个数超过数组容量 * loadFactor(负载因子),并且null != talbe[bucketIindex](不为空则代表发生hash冲突),就会进行扩容操作
//可以看到当数组容量>=threshold,并且null != table[bucketIndex](这个其实没有什么实际作用,在jdk1.8的时候就删除了)时,就会进行扩容操作,新的容量为2 * table.length
//也就是说,默认情况下数组大小为16,当元素个数等于或超过16*0.75=12时,就会把数组的大小扩展为2 * 16=32,然后重新计算出每个元素在数组中的位置。
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容方法
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//添加元素的操作
createEntry(hash, key, value, bucketIndex);
}
//******
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//此处创建一个新的Entry数组对象,容量是前数组大小的两倍,
//如果按默认数组容量16来计算,则此处为32
Entry[] newTable = new Entry[newCapacity];
//transfer转移
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* Transfers all entries from current table to newTable.
*/
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);
}
//(JDK1.7进行了重新计算`h & (length-1)`,但在JDK1.8中利用了扩容规律没有进行重新计算)
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
扩容规律:
结论:每次扩容都是翻倍的 与原来的(n-1) & hash结果相比,只是多了一个bit位,
所以节点要么在原位置,要么就会被分配到"原位置下标+旧容量大小"这个位置。
因此在扩容HashMap计算元素新下标位置的时候h & (length-1)
,只需要看看原来的hash值新增的哪个bit位是1还是0就可以了。
(对应bit位:为0表示索引没有变化,为1表示新的索引位置:原索引+旧容量)
(JDK1.7进行了重新计算h & (length-1)
,但在JDK1.8中利用了这个特点没有进行重新计算)
在JDK1.8中,利用了这种巧妙的方式,提高了计算新索引下标位置的效率,而且同时,由于新增的bit位是0还是1,这是随机的,因此在resize的过程中,保证了扩容之后不会出现更严重的hash冲突,均匀的把之前冲突的节点分散到新的桶中去了。
添加元素操作
/**
* Like addEntry except that this version is used when creating entries
* as part of Map construction or "pseudo-construction" (cloning,
* deserialization). This version needn't worry about resizing the table.
*
* Subclass overrides this to alter the behavior of HashMap(Map),
* clone, and readObject.
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
//将数组bucketIndex位置的元素实例指向新建Entry引用e
Entry<K,V> e = table[bucketIndex];
//新建Entry对象,并将上面的e指向此对象的next属性上
//再将数组bucketIndex位置替换为新的Entry对象
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
createEntry简单一句话总结就是:链表头插法以及元素的向下移动。
get方法#
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
/**
* Returns the entry associated with the specified key in the
* HashMap. Returns null if the HashMap contains no mapping
* for the key.
*/
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : 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;
}
JDK1.8中的HashMap#
在JDK1.7中,HashMap底层是通过数组+链表+红黑树的形式来实现的。
关键属性#
/**
* The default initial capacity - MUST be a power of two.
*/
//默认初始容量-必须是2的幂。
//代表HashMap的初始化容量:16(1 << 4 等于二进制10000转换为十进制为16)
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.
*/
//最大容量,如果隐式指定了更高的值由任何一个带参数的构造函数调用。必须是2的幂<= 1<<30。
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
//默认加载因子:当构造函数中没有指定时使用的加载因子。 也就是四分之三
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.
*/
//树化阈值
//1.8新增 当链表的长度 >=8 - 1 时会转换为红黑树
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.
*/
//1.8新增 退化阈值 当红黑树的长度 <=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.
*/
//1.8新增 红黑树的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* The number of key-value mappings contained in this map.
*/
//此映射中包含的键-值映射的数目。HashMap中元素个数。
transient int size;
为什么要加红黑树?
在JDK1.7里面,虽然做了很多的措施(扩容....),来使哈希冲突的概率下降,但是还是会出现某个位置链表特别长的情况,所以增加了红黑树。
为什么树化阈值为8,链表退化阈值为6?
为了避免反复转化
构造方法#
JDK1.8中HashMap的构造方法较JDK1.7要简单很多
/**
* The load factor used when none specified in constructor.
*/
//默认加载因子:当构造函数中没有指定时使用的加载因子。 也就是四分之三
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
put方法#
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
//hash(key)方法见P1
//putVal()方法在下面
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;
// 判断数组是否为空 , 为空则调用resize()方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
//初始化、扩容都在resize()中 见P3
n = (tab = resize()).length;
// 如果当前数组索引位置的元素为null则直接创建新的节点并存到该索引位置的数组中
// 和JDK1.7的对比
//其实这里的(n - 1) & hash就是JDK1.7中的indexFor()方法体中的(length - 1) & h,只不过在1.8中做了简化处理
if ((p = tab[i = (n - 1) & hash]) == null)
//tab[i] = newNode(hash, key, value, null);在JDK1.7中时在createEntry()方法中完成创建Entry的
//newNode()方法 见P2
tab[i] = newNode(hash, key, value, null);
else {// 走到这个else代码块中说明当前key已经在数组中存在了 || 发生了hash碰撞
Node<K,V> e; K k;
//判断当前key在数组中是否存在, 存在则直接替换
//(因为onlyIfAbsent默认为false,代表直接替换。反之上面的注释:onlyIfAbsent if true, don't change existing value)
//(p.hash是指数组上已经存在的元素的hash, hash是指新元素的hash)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断当前节点类型是否是红黑树节点
else if (p instanceof TreeNode)
// 将key、value存储到红黑树节点中 , 如果key已经存在 , 则返回之前的节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {// 代码进入到这个else代码块就说明发生了hash碰撞
for (int binCount = 0; ; ++binCount) {
// 判断当前节点是否是链表的最后一个节点
if ((e = p.next) == null) {
// 直接将当前要存储的key、value存储到上一个节点的next元素中
// 在JDK1.7中的会先将之前存储的节点取出来, 然后将之前的节点作为新节点的next元素存储到数组中
// 而在JDK1.8中会直接将新的节点放到之前存储节点的next元素中
// 也就是说JDK1.7中的链表插入顺序是从头部开始插入, 而在1.8中时从尾部开始插入
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 转换红黑树 见P4
treeifyBin(tab, hash);
break;
}
// 当前节点不是最后一个节点, 判断当前节点的key与要存储的key是否相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// e在只有当前key已经存在时才会完成初始化], 这里是统一处理, 返回key之前对应的value, 并判断是否要替换value,默认是替换
//onlyIfAbsent if true, don't change existing value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 这里是个空方法体
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 在JDK1.7中扩容操作是在存储数据之前发生的
// 在JDK1.8中扩容操作是在存储数据之后发生的
if (++size > threshold)
resize();
// 这里是个空方法体
afterNodeInsertion(evict);
return null;
}
P1 hash(key)
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
计算当前key的hash值的 , 为了减少hash碰撞发生的概率 , 获取当前key的hashCode
值后再进行一次位运算和一次异或运算
P2 newNode()
/*
* The following package-protected methods are designed to be
* overridden by LinkedHashMap, but not by any other subclass.
* Nearly all other internal methods are also package-protected
* but are declared final, so can be used by LinkedHashMap, view
* classes, and HashSet.
*/
// Create a regular (non-tree) node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
jdk1.7 Entry
jdk.18 Node
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
P3 resize()
/**
* 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代码块中说明此时table数组已经完成过初始化
if (oldCap > 0) {
// 判断当前容量是否已达到最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 将新数组的容量设置为旧容量的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 这一步的操作等同于 oldThr * 2
newThr = oldThr << 1; // double threshold
}
// 进入到这个代码块说明在创建HashMap时使用的有参构造器
// 在翻阅代码后我们发现threshold的值会在有参构造器中被初始化 , 此时被初始化了就会使用指定的容量来完成table数组的初始化
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 进入到这个else代码块中说明数组还未完成初始化 , 进行初始化操作
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//只有调用有参构造器才让newThr == 0
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) {
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
// loHead和loTail存储的是转移数据后仍然存储在当前索引位置的元素
Node<K,V> loHead = null, loTail = null;
// hiHead和hiTail存储的是转移数据后存储到[j + oldCap]索引位置的元素
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//TODO 待补充
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;
}
P4 treeifyBin
TODO 待补充
HashMap在JDK1.7与JDK1.8的区别#
1.7 数组+链表
1.8 数组+链表+红黑树
1.7 节点对象是Entry对象
1.8 节点对象是Node对象
1.7 初始化、扩容方法是分开的
1.8 初始化、扩容是一个方法
1.7 扩容条件:元素个数大于阈值且当前新增元素没有hash冲突
1.8 扩容条件:元素个数大于阈值
1.7 链表新增节点时,是头插法
1.8 链表新增节点时,是尾插法
1.7 有专程存储null的方法
1.8 无专程存储null的方法
扩容
JDK1.7
是先扩容再添加元素
会重新计算key的hash值和索引位置
转移数据时会颠倒Entry链表的顺序
转移数据时不会将当前链表拆分 , 并且会重新计算索引位置
在并发下触发扩容可能会造成死循环
JDK1.8
是先添加元素再扩容
不会重新计算key的hash值
转移数据时不会颠倒Entry链表的顺序
转移数据时会将当前链表拆分成俩个链表 , 一个存放到旧索引位置 , 一个存放到(原位置 + 旧容量)索引位置(扩容规律)
在并发下触发扩容不会造成死循环
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix