HashMap类源码详解
一、简介
1.存储结构及特点
HashMap 底层的数据结构:
底层是利用邻接表(散列表)的形式进行存储的:数组 + 链表/红黑树。
HashMap 中每个节点 的存储方式为<key,value>形式,
其中,key 无序不可重复,value可以重复。
2.存储结构的由来
首先我们需要明白:
· 当hash值不同时,对象一定不同。
原因:因为用同样的计算方法计算同样的东西,计算结果一定相同。结果不同,就说明他俩根本不是同一个东西。
· 当hash值相同时,对象可能相同,也可能不同。
原因:因为不同的东西经过相同的计算方式,结果可能会相同。
所以,此时还需要借助equals()方法进行内容的具体判断。
也就是说,用hashCode()方法计算对象的存储位置时,不同的对象是可以产生相同的hash值的。
因此,当同一个hash值对应的对象不止有一个时,就将相同地址的对象用链表结构连接起来。
3.邻接表的存储方式
HashMap中底层的数组部分,是可以通过key计算出来的hash值直接进行快速访问的。
数组内存储的内容是Entry类对象,这个类有三个组成部分:key+value(键值对),next(指向下一个Entry类对象的指针)。
后面链表中的每个节点,存的也是一个 由 key+value(键值对),以及指向下一个节点地址的next指针包装成的一个Entry对象。
4.HashMap的存储过程
在往HashMap中put节点(键值对)时,首先通过底层重写的散列算法hashCode()方法计算出key的hash值,从而确定要将当前节点插入到数组中的哪个位置。要将当前节点定位到散列表的相应位置,所以需要重写hashcode()方法。
但是,当两个key计算出来的hash值相同时,则后来的元素就要添加到先来的元素的后面(JDK1.8采用尾插法),从而形成链表。所以,同一个链表上的Hash值是相同的(在jdk1.8中,如果链表的长度>8,则此链表就会转化成红黑树,从而提高查找效率)。然后依次遍历该位置后面链表中的所有元素,此时就需要调用该类重写的equals()方法进行内容的具体比较了。相等则修改key对应的value值,不等则添加该节点。
实际上,HashMap中判断元素是否重复是有两个过程的,首先生成即将插入的新对象其key的hash值。当我们发现没有相同的散列值时,就可以判断这是一个不可能重复的元素,就可以直接插入了。当我们发现table中已经有相同的散列值时,并不能直接判断此对象就是个重复元素(因为不同的对象也可能产生相同的hash值),此时还需要调用equals()方法对具体内容再判断一次,如果判断的结果还是真,就表示这真的是个重复元素,就不做存入处理,只做值value的更新处理;如果equals判断为假,那么就重新比较其他的节点,再进行插入判断。
5.HashMap中,需要重写equals()和hashCode()的原因
Object中的原生方法:
-
hashCode() :返回的是对象的地址引用。
所以,在这种情况下,不同对象的hash值肯定不同,即使两个对象的内容完全一致,但由于地址引用不同,所以hash值也就不同。
-
equals() :底层默认的比较方式是"=="号。
所以,对于引用数据类型来说,比较的是对象的地址(8个基本数据类型+String类型进行值比较)。
equals()方法和hashCode()方法都属于Object类,在Java中,所有的类都是Object类的子类,也就是说,任何Java对象都可调用Object类的方法。
很明显,Object类原生的equals()方法就是用来判断,两个对象是否是同一个地址引用的。在Object类源码中,其底层是使用了“==”来实现比较的。也就是说,通过比较两个对象的内存地址是否相同,来判断这两个对象是否是同一个对象。但实际上,该equals()方法通常没有什么太大的实用价值。我们往往需要用equals()方法来判断, 2个对象在逻辑上是否等价,而非验证它们地址内存的唯一性。这样我们在实现一些自己的类时,就需要重写equals()方法。
而在HashMap中,需要通过重写hashCode()方法,重新定义计算key的hash值的计算方式(根据key的实际内容进行计算,而不是返回对象的地址引用)。由于内容不同的对象也可能被计算出相同的hash值,所以此时就需要通过重写equals()方法进行对象的内容判断(内容相同的对象被认为是同一个对象,返回true。而不是equals()利用原来的"=="号,来比较引用数据类型的地址是否相同。)。
6.JDK1.7和1.8的区别
1>.存储结构:
JDK1.7中,底层的存储结构是:数组+链表。
而JDK1.8中:数组+链表/红黑树(链表长度>8时,会由链表转换为红黑树以提高效率)。
思考:
红黑树是为了解决链表查询出现O(n)情况,那么为什么不用其他树呢?
如平衡二叉树等,我们通过以下二方面分析:
平均插入效率:链表>红黑树>平衡二叉树
平均查询效率:平衡二叉树>红黑树>链表
可以看出红黑树介于二者之间,hashMap作为各种操作频繁的容器,自然选择综合性能较好的红黑树
2>.新节点插入链表的方式:
JDK1.7中用的是头插法(扩容后与原位置相反,因为resize()会导致环形链表)。
JDK1.8用的是尾插法(因为1.8中引入了红黑树结构,要通过遍历链表,将链表转换为红黑树),扩容后位置与原链表相同。
3>.hash算法简化:
key的Hash值的计算区别
JDK1.7:
h^ =(h>>>20)^(h>>>12)
return h ^(h>>>7) ^(h>>>4);
JDK1.8:
return (key==null)?0:(h=key.hashCode())^(h>>>16);
JDK1.7中因为要保持hash函数的散列性,所以进行了多次的异或等位运算。
JDK1.8因为链表长度超过8会转为红黑树,所以我们可以稍微减少元素的散列性,从而避免多次进行异或等位运算。
4>.扩容机制:
JDK1.7扩容条件:元素个数 > 容量(16) * 加载因子 (0.75) && 插入的数组位置有元素存在。
JDK1.8扩容条件 :元素个数 > 容量 (16) * 加载因子(0.75)。
虽然都是进行2倍扩容,但是JDK1.7在扩容的时候,会重新计算位置。
JDk1.8则不会,只需关注新增的参与hash运算最高的那个bit位是1还是0就好了。
是0的话,扩容前后索引下标位置不变,新索引 = 原索引。
是1的话,新索引 = 原索引+ oldCap(旧数组长度)。
7.方法简介
1>.基本方法
1.增加( put(key,value) )
2.删除( remove(key) )
3.更改(replace(key,value))
(其实,先put(key,value1),后又put(key,value2),value2覆盖了value1,就是发生了更改。)
4.查找(get(key))
2>.重要方法
1.keySet()方法:
可以通过 keySet()方法,获取 HashMap中所有的key,此方法的返回值是Set类型。
例:Set<key.type> keys = hashMap.keySet();
2.entrySet()方法:
可以通过 entrySet()方法,获取HashMap中所有的Entry对象,每个Entry对象的信息包括(key + value + nextNode)。
例:Set< Map.Entry <key.type,value.type> > entrys = hashMap.entrySet()
3>.特殊方法
1.getOrDefault(key , defaultValue)方法:
· 如果key存在,就返回其对应的value值;
· 如果key不存在,就返回默认值defaultValue(以前返回的是null)。
2.putIfAbsent(key , value)方法:
· 当key不存在时,向HashMap中添加此键值对;
· 如果此key存在,则不发生任何更改(以前用put()方法存放时,value值会发生改变)。
二、基础信息
1.继承的父类&实现的接口
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
2.重要属性
//允许序列化和反序列化
private static final long serialVersionUID = 362498820763181265L;
/**
* The default initial capacity - MUST be a power of two.
* 默认的初始容量-必须是2的整数次幂。
*
* 00001 << 4 = 10000 = 2^4 = 16,
* 所以,table的默认初始容量 DEFAULT_INITIAL_CAPACITY = 1 << 4 = 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的幂。
* 即table的最大容量为2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
* 在构造函数中未指定时使用的默认负载系数。
*
* 加载因子用于表示Hash表中,元素的填满程度。
* 加载因子越大,表内填满的元素就越多,空间利用率就越高,但冲突的机会就会加大。
* 加载因子越小,填满的元素越少,冲突的机会就会减小,但空间的浪费就会增多。
* 而此处,加载因子的默认值是0.75
*/
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.
*
* 使用树而不是列表的容器计数阈值。
* 当向至少有这么多节点的容器中添加元素时,容器被转换为树。
* 该值必须大于2,并且应该至少为8,
* 以与关于在收缩时转换回普通bins的树移除假设相匹配。
*
* 桶的树化阈值:即 链表转成红黑树的临界值。
* 在存储数据时,当链表长度 > 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.
*
* 在调整大小操作期间取消(分割)存储库的存储库计数阈值。
* 应小于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.
*
* 可以树形化容器的最小表容量。
* 即 :当哈希表中的容量 > 64时,才允许树形化链表 .
* (否则,如果一个容器中有太多节点,则会调整表的大小,而不是树形化链表。)
* 为了避免 进行扩容 和 树形化 之间的选择冲突,
* 这个值应该至少为4 * TREEIFY_THRESHOLD。
*
* 最小树形化容量阈值:
* 当整个hashMap容器中的节点数>64时,才允许链表树化。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*
* 该表在第一次使用时初始化,并根据需要调整大小。
* 当分配时,长度总是2的幂。
* (在某些操作中,我们也允许长度为0,以允许当前不需要的引导机制。)
*
* 这是邻接表的数组部分。
* 同hash值的key被放在同一链表里。
* 链表头是最新加入的数据元素,数组中的每个元素就是同hash值的链表头。
*/
transient Node<K,V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
* 保存缓存entrySet()。
* 注意,keySet()和values()使用了AbstractMap字段。
*
* 存放具体数据节点的集合
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* The number of key-value mappings contained in this map.
* 此映射中包含的键-值映射的数量。
* 用于记录元素个数
*/
transient int size;
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*
* 当前 哈希表结构的修改次数(例如添加或删除元素时会被记录,但修改不会)
*/
transient int modCount;
/**
* The next size value at which to resize (capacity * load factor).
*
* 要调整大小的下一个尺寸值(容量*负载系数)
*
* 扩容阈值:
* 当哈希表中的元素个数size > 此阈值threshold时,会触发扩容操作。
* 会重新设置长度(扩容阈值threshold = capacity(表长) * loadFactor(负载系数) ),
* 表示符合装载因子的最大使用容量。
*
* @serial
*/
int threshold;
/**
* The load factor for the hash table.
* 哈希表的负载因子。
*
* @serial
*/
final float loadFactor;
3.Node节点类
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*
* 基本散列 bin节点,用于大多数条目。
* (参见下面的treenode子类和LinkedHashMap的Entry子类。)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//此节点的hash值
final K key;//此节点的 键key
V value; //此节点的 值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; }
//计算当前节点hash值的hashCode()方法,重写自父类Object。
//(借助父类Object 本地native修饰的hashCode()方法进行计算)
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//修改当前节点的 value值,并将原来的旧值返回
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
/**
* 重写了父类Object的equals()方法:
* 重新定义了比较规则:
* 不再是Object中默认的,通过“==”来比较两个对象的地址引用了,
* 而是比较属性的具体内容。
*/
public final boolean equals(Object o) {
//由于是引用数据类型,所以 "==" 比较的是变量的地址引用
//如果 参数o 和 当前节点对象 的地址相同,表示是同一个对象
if (o == this)
return true;
//首先判断 参数o的数据类型,如果数据类型和当前节点的数据类型相同
if (o instanceof Map.Entry) {
//将 形参o 类型转换为e
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
//如果当前的 key 和 形参e.getKey()的地址相同,
//并且,当前的 value 和 形参e.getValue()的地址相同,则认为是同一个对象
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
三、重要方法
1.构造函数
注意: 3个有参构造方法会给扩容阈值threshold赋值,而无参构造方法不会。
1>.无参构造
/**
* 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; // all other fields defaulted
}
2>.有参构造
1.HashMap(int initialCapacity, float loadFactor)方法:
//构造一个空的HashMap,具有指定的初始容量和负载因子。
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);
//如果参数合法,分别给 负载因子loadFactor 和 扩容阈值threshold 赋值。
this.loadFactor = loadFactor;
//注意 扩容阈值threshold, 需要通过tableSizeFor()方法计算后给出(因为表的长度必须是2的整数幂)
this.threshold = tableSizeFor(initialCapacity);
}
/**
* Returns a power of two size for the given target capacity.
*
* 作用:返回一个大于等于参数cap的数,
* 并且,这个数一定是2的整数幂。
*
* 位运算 >>> :表示无符号右移,左边补0
* 逻辑运算 | : 逻辑"或"
*/
static final int tableSizeFor(int cap) {//例:cap = 10
int n = cap - 1;//n = 10-1 = 9 = 1001
n |= n >>> 1;//n = 1001 | 0100 = 1101
n |= n >>> 2;//n = 1101 | 0011 = 1111
n |= n >>> 4;//n = 1111 | 0000 = 1111
n |= n >>> 8;//n = 1111 | 0000 = 1111
n |= n >>> 16;//n = 1111 = 15
//n = 15 时,执行return n+1,最后返回10
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
2.HashMap(int initialCapacity)方法:
//构造一个空的HashMap,具有指定的初始容量和默认负载因子(0.75)。
public HashMap(int initialCapacity) {
//只传入初始容量时,第二个负载系数将传入默认值DEFAULT_LOAD_FACTOR = 0.75
//然后利用上面的构造函数HashMap(int initialCapacity, float loadFactor)进行构造
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
3>.利用Map构造HashMap
HashMap(Map<? extends K, ? extends V> m) 方法:
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* 使用与指定的Map相同的映射构造一个新的HashMap。
* 创建HashMap时,默认负载因子(0.75)和初始容量足以容纳指定的Map中的映射。
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();//记录形参m内的元素个数
if (s > 0) {
//如果 table 没有被初始化过
if (table == null) { // pre-size
//由于 扩容阈值threshold = capacity(表长) * loadFactor(负载系数)
//所以先用ft大概算一下所需表长
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果 计算得到的所需表长t > 扩容阈值
if (t > threshold)
//则更新扩容阈值,通过tableSizeFor(t)得到一个不小于t的2的整数幂。
threshold = tableSizeFor(t);
}
//如果已初始化过,并且m的元素个数>扩容阈值
else if (s > threshold)
//则进行扩容处理
resize();
//复制数据,将m中的所有元素添加到hashMap中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
2.增加put(K key, V value)方法
//增加:存入 键值对型 节点
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(如果此处是true,已存在的key其对应的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) {
//tab:用于存储当前散列表table的地址引用
//p : 用于表示当前散列表中的节点元素
//n : 用于表示散列表数组的长度
//i : 用于表示计算后的最终下标。
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果 table为null,或者长度为0
if ((tab = table) == null || (n = tab.length) == 0)
//为哈希表做初始化处理
//先为table扩容(扩容就是做了初始化),并用n记录扩容后表的长度
n = (tab = resize()).length;
//如果 利用表长n和hash值计算出来的下标i,在哈希表中对应的存储单元为null
if ((p = tab[i = (n - 1) & hash]) == null)
//就将此节点放到表table的对应位置
tab[i] = newNode(hash, key, value, null);
//否则表示,此hash值对应的节点不止一个
else {
//e : 用于记录与待插节点的hash值和key都完全一致的节点。
Node<K,V> e; K k;
//如果,数组中的节点p与新加节点的hash值相等
//并且,键key也相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//此时表示已经找到了重复元素,用e记录旧节点的地址引用,后续需要进行值替换
e = p;
//如果新加节点的key与数组中节点的key不同,
//第一种可能是,去后面的树中去找(涉及红黑树)
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果不去树中查询,就去链表中遍历。(至此已知,链表的头元素与待插元素的key不一致)
else {
//开始循环遍历链表
for (int binCount = 0; ; ++binCount) {
//如果已经到达链表尾部,则表示此hash值对应的链表中没有与待插元素相同的key
if ((e = p.next) == null) {
//就把新节点添加到此链表的尾部,作为尾节点。
p.next = newNode(hash, key, value, null);
//如果添加完新节点后的链表长度>8,则由链表转换成红黑树。
// 为什么是长度>8?因为触发下面if语句的条件是:binCount >= 7。
// 由于binCount的初始值是0,
// 当binCount=7时,表示现在正在执行的是循环的第8次,
// 而且刚才又在链表的尾部添加了一个新节点,故此时链表长度是9,而9>8。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//将链表结构进行树化
break;
}
//如果在遍历链表的过程中,遇到与待插节点的hash值和key都相同的节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//p = e = p.next,循环继续往后走的条件
}
}
//若 e != null,则表示此hashMap中,存在与待插元素同hash且同key的重复元素,
// 需执行value的更新操作。
if (e != null) { // existing mapping for key
V oldValue = e.value;//保存旧值
if (!onlyIfAbsent || oldValue == null)
e.value = value;//更新value值
afterNodeAccess(e);
return oldValue;//并将旧值返回
}
}
++modCount;
//如果 节点个数size > 扩容阈值threshold,则需执行扩容操作
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3.删除remove()
1>. remove(Object key, Object value)方法:
//删除节点:当key和value同时比对成功,才执行删除操作,否则不删。
@Override
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
2>. remove(Object key, Object value)方法:
//删除:根据键key ,删除对应的节点对象
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
//删除节点操作的具体实现
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab:保存当前table数组的地址引用
//n : 记录当前table数组的长度
Node<K,V>[] tab; Node<K,V> p; int n, index;
//如果 此hash值对应的桶中存在节点p
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node:用于记录查找结果
Node<K,V> node = null, e; K k; V v;
//如果 传入的参数与桶中头节点的hash值和key比对成功。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;//用node记录已经找到的节点
//如果 此桶中头节点的后面还有节点。
else if ((e = p.next) != null) {
//如果 头节点的后面是树结构
if (p instanceof TreeNode)
//则 在树中返回查询结果
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
//否则表示,头节点后面是链表结构
else {
//利用循环向后遍历链表中的节点,挨个比对它们的hash值和key
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//至此,已经用node记录了最终的查找结果
//若 node != null,则表示找到了此key对应的节点
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//如果 这是个树节点
if (node instanceof TreeNode)
//则在树中删除
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//如果 这是桶中的那个头节点
else if (node == p)
//则把头节点的后继节点更新为新的头节点
tab[index] = node.next;
//如果 这是链表中的一个普通节点
else
p.next = node.next;//将当前node节点从链表中移除
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
4.查找get(Object key)方法
//查找:根据 键key,返回 值value
public V get(Object key) {
Node<K,V> e;
//getNode()方法:根据 key的hash值 和 键key,返回对应Node节点
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//根据 key的hash值 和 键key,返回对应Node节点
final Node<K,V> getNode(int hash, Object key) {
//tab:保存当前数组的地址引用
//first:某个桶中的头节点
//n :记录table数组的长度
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//如果 数组表table此hash值对应的桶中有节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果 传入的参数与数组对应桶中,头节点的hash值相等,key也相同
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//表示此hash桶中的头节点,就是我们要找的节点
return first;
//如果 此hash值对应桶中的节点不只是这一个头节点
if ((e = first.next) != null) {
//如果 头节点后面连的是个树
if (first instanceof TreeNode)
//在树中返回节点
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//否则表示,此hash值对应桶中的节点后面是个链表结构
//则依次向后遍历每个节点,比较其hash值和key
do {
//如果hash值和key比对成功
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;//则返回此节点
} while ((e = e.next) != null);
}
}
return null;
}
5.修改replace()
1>.replace(K key, V value)方法:
//修改:把key的值更新为value
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;//修改value
afterNodeAccess(e);
return oldValue;
}
return null;
}
2>.replace(K key, V oldValue, V newValue)方法:
//修改:当传入的key和oldValue都比对成功后,才将值更新为newValue
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
四、核心方法
1.hash(Object key)方法:
/**
* 作用:为了让 key.hashCode()的高16位 ,也参与 异或运算(相同为0,不同为1)。
*
* 例: h = 0010 0101 1010 1100 0011 1111 0010 1110
* 则:h >>> 16 = 0000 0000 0000 0000 0010 0101 1010 1100
*
* 0010 0101 1010 1100 0011 1111 0010 1110
* ^ 0000 0000 0000 0000 0010 0101 1010 1100
* = 0010 0101 1010 1100 0001 1010 1000 0010
* 返回结果:高16位不变,低16位和高16位做了异或运算。
*
* 当哈希表的长度较小时,比如最开始默认的16,即表示可用索引index的范围是[0,15],此时非常容易产生冲突。
* 为了在计算索引值index时,降低冲突的可能性,
* 索引的计算方式为:index = (table.length - 1) & (高16位^低16位后得到的 hash值),
*
* 当多个key.hashCode()值的高16位不同,而低16位完全相同时,
* 通过这样的方式计算索引值,由于影响索引值的因素包括高16位,得到的索引值index也就不易重复了,
* 从而可以在一定程度上减少地址冲突的可能性。
*/
//经过计算,返回 key的hash值
static final int hash(Object key) {
int h;
//如果key!=null,借助hashCode()方法,计算key的hash值。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2.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.
*
* 扩容方法:
* 初始化或加倍表的大小。
* 如果为空,则按照字段阈值中默认的初始容量目标进行分配。
* 否则,将使用2的整数次幂进行扩容,
* 每个容器中的元素必须保持相同的索引,或者在新表中以2的幂移动。
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//oldTab:用于保存扩容前旧表的地址引用
//oldCap:用于记录扩容前table数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;//扩容之前的扩容阈值
//newCap:扩容之后的table数组的长度
//newThr:扩容之后,下次再触发的扩容阈值
int newCap, newThr = 0;
//如果旧表被初始化过,表示这是一次正常的扩容
if (oldCap > 0) {
//如果旧表数组长度已经超过最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
//则不再继续扩充,且设置下次扩容条件为 threshold = Integer.MAX_VALUE.
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果旧表数组长度没超过最大容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//则新数组长度扩容为旧数组长度的2倍.
newThr = oldThr << 1; // double threshold
}
//此时 oldCap == 0,表示hashMap中的旧数组还没有被初始化过.
//如果 旧的扩容阈值oldThr > 0.
//注意:3个有参构造方法会给扩容阈值threshold赋值,无参构造方法不会
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//此时 oldCap == 0,表示hashMap中的旧数组还没有被初始化过.
//如果 旧的扩容阈值oldThr == 0.
//则表示之前的hashMap是通过无参构造函数创建的,扩容阈值没有被赋过值.
else { // zero initial threshold signifies using defaults
//给 新数组长度newCap 赋默认值16
newCap = DEFAULT_INITIAL_CAPACITY;
//给 新的扩容阈值newThr 赋默认值0.75*16 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果 新扩容阈值newThr == 0
if (newThr == 0) {
//就计算一个 新扩容阈值newThr
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;//把 新的扩容阈值 赋给 全局变量
//至此,计算出了 扩容后新数组的长度newCap 和 下次触发扩容函数的新阈值newThr
//接下来,开始进行扩容操作.
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建了一个长度更长的新数组.
table = newTab;//将扩容后的数组newTab的地址引用返回给table
//如果 oldTab != null,则表示旧数组被初始化过(即旧表中有数据).
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)
//根据新数组长度重新计算索引,然后将此节点元素e存入到新表newTab中.
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用来做链表的动态拼接.
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//若 e.hash = 15 = 0 1111 ,oldCap = 16 = 1 0000
//则 e.hash & oldCap = 0 1111 & 1 0000 = 0 0000 = 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;//用于标记尾节点,给尾节点的后继节点赋成null.
//由于扩容前后,低位链表内节点的索引值不需要发生改变.
//所以直接通过低位链表的表头,把整个链表添加到新数组的对应桶中.
newTab[j] = loHead;
}
//如果高位链表中有数据
if (hiTail != null) {
hiTail.next = null;//用于标记尾节点,给尾节点的后继节点赋成null.
//由于扩容前后,高位链表内节点的索引值会发生改变.
//所以通过高位链表的表头,把整个链表添加到新数组的对应桶中.
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
3.扩容机制
解析:
当桶内节点个数不止一个,且后面的结构是链表时,其扩容图示如上:
以索引值为15的桶为例,在图示中有5个节点元素。
我们已知,索引的计算方式 index = (key.hash) &(table.length - 1).
当扩容前的数组长度为16时,table.length - 1 = 16 - 1 = 15 = 1111 B;
由索引值index = 15 = 1111 B = (key.hash) &(table.length - 1) = (key.hash) & 1111 B,
得知,key的hash值的后4位 key.hash = 1111 B。
当数组长度扩容为32时,table.length - 1 = 32 - 1 = 31 = 0001 1111 B;
在旧表中,索引 index = 15 位置处的节点,由上述可知,其key的hash值的后四位是 1111 B。
对应到长度为32的新表中计算其索引值,index = (key.hash) &(table.length - 1)。
此时,由于key.hash高位数值的不同,最终结果存在两种可能。
1.若 key.hash = xxx0 1111 B,
则index = (key.hash) &(table.length - 1) = xxx0 1111 & 0001 1111 =0000 1111 = 15。
所以,存在原数组索引值为15桶中的元素,存到了新数组中索引值为15的桶中(索引值没变)。
2.若 key.hash = xxx1 1111 B,
则index = (key.hash) &(table.length - 1) = xxx1 1111 & 0001 1111 =0001 1111 = 31。
所以,存在原数组索引值为15桶中的元素,存到了新数组中索引值为31的桶中(索引值发生变化)。
综上所述,扩容前后的索引值存在两种对应关系。
一种是,索引值不发生变化:新索引 = 旧索引。
另一种是,索引值发生变化:新索引 = 旧索引 + 旧表长度。