JDK 8的HashMap源码解析
感谢Neal Gafter、Arthur van Hoff、Josh Bloch、Doug Lea为我们提供了HashMap这一工具类,方便了诸多Java开发者的开发工作。
看再多的源码解析,自己不亲身去阅读源码,都是徒劳无功。
注释分析
为了我们能轻松地阅读源码,JDK的开发者们已经为我们准备了许多详尽的注释,阅读这些注释对我们理解代码起着事半功倍的效果,那么我们就先来看看HashMap在JDK8中的注释吧!
Hash table based implementation of the Map interface. This implementation provides all of the optional map
operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable,
except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the
map; in particular, it does not guarantee that the order will remain constant over time.
挑上述这段注释的几个重点:
(1)HashMap里允许存放null键和null值,HashTable不允许存放null键和null值。
(2)HashMap不是线程安全的容器,HashTable是线程安全的容器。
(3)HashMap不保证存放的键值对的顺序,甚至该顺序随着HashMap中键值对的增加/删除可能会发生变化。
This implementation provides constant-time performance for the basic operations (get and put), assuming the
hash function disperses the elements properly among the buckets. Iteration over collection views requires
time proportional to the "capacity" of the HashMap instance (the number of buckets) plus its size (the number
of key-value mappings). Thus, it's very important not to set the initial capacity too high (or the load factor too
low) if iteration performance is important.
挑上述这段注释的几个重点:
(1)HashMap利用哈希函数把键值对元素合理地分布在各个哈希桶中,保证了get和put操作的时间复杂度是O(1)。
(2)迭代操作取决于哈希桶的数量和HashMap中键值对的数量。因此,为了良好的迭代性能,不要把哈希桶的初始容量设得太高,也不要把HashMap的负载率设得太低。
An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The
capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time
the hash table is created. The load factor is a measure of how full the hash table is allowed to get before its
capacity is automatically increased. When the number of entries in the hash table exceeds the product of the
load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so
that the hash table has approximately twice the number of buckets.
挑上述这段注释的几个重点:
(1)初始容量(初始哈希桶数量)和负载率是影响HashMap性能的两个重要参数。
(2)当HashMap中的键值对数量超过负载率和当前哈希桶数量的乘积时,HashMap会发生扩容操作(哈希桶数量翻倍)。
As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher
values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the
HashMap class, including get and put). The expected number of entries in the map and its load factor should
be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If
the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash
operations will ever occur.
挑上述这段注释的几个重点:
(1)将默认负载率设为0.75是时间性能和空间性能上的权衡结果,过高的负载率虽然可以提高空间性能,但会降低时间性能。
(2)当设置初始哈希桶数量时,需要考虑HashMap中准备存放的键值对数目和HashMap的负载率,以尽量减少扩容操作的发生频率。
If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will
allow the mappings to be stored more efficiently than letting it perform automatic rehashing as needed to
grow the table. Note that using many keys with the same hashCode() is a sure way to slow down
performance of any hash table. To ameliorate impact, when keys are Comparable, this class may use
comparison order among keys to help break ties.
挑上述这段注释的几个重点:
(1)当太多键的hashCode()值相同时,会影响HashMap的性能。
(2)当出现太多键有相同的hashCode()值的情况时,如果此时键实现了Comparable接口,那么HashMap会利用这点做一些措施,来提高性能。
Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at
least one of the threads modifies the map structurally, it must be synchronized externally. (A structural
modification is any operation that adds or deletes one or more mappings; merely changing the value
associated with a key that an instance already contains is not a structural modification.) This is typically
accomplished by synchronizing on some object that naturally encapsulates the map.
挑上述这段注释的一个重点:
当多线程同时访问HashMap,且至少有一个线程对HashMap做出了结构性的修改(如新增/删除键值对),必须在外部加同步措施。
If no such object exists, the map should be "wrapped" using the Collections.synchronizedMap method. This
is best done at creation time, to prevent accidental unsynchronized access to the map:
Map m = Collections.synchronizedMap(new HashMap(...));
挑上述这段注释的一个重点:
可以利用Collections.synchronizedMap()方法来将HashMap包装成为线程安全的容器。
The iterators returned by all of this class's "collection view methods" are fail-fast: if the map is structurally
modified at any time after the iterator is created, in any way except through the iterator's own remove method,
the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the
iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined
time in the future.
挑上述这段注释的一个重点:
Fail-Fast机制:当用迭代器遍历HashMap时,如果该HashMap发生了结构性的修改,会抛出ConcurrentModificationException异常。
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to
make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators
throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a
program that depended on this exception for its correctness: the fail-fast behavior of iterators should be
used only to detect bugs.
挑上述这段注释的一个重点:
Fail-Fast机制抛出ConcurrentModificationException异常不是一种可靠的机制,不要让你的程序的运行依赖于该机制。
This map usually acts as a binned (bucketed) hash table, but when bins get too large, they are transformed
into bins of TreeNodes, each structured similarly to those in java.util.TreeMap. Most methods try to use
normal bins, but relay to TreeNode methods when applicable (simply by checking instanceof a node). Bins of
TreeNodes may be traversed and used like any others, but additionally support faster lookup when
overpopulated. However, since the vast majority of bins in normal use are not overpopulated, checking for
existence of tree bins may be delayed in the course of table methods.
挑上述这段注释的一个重点:
当某个哈希桶中的键值对过多时,HashMap会将该桶中原来的链式存储的节点转化为红黑树的树节点。
Tree bins (i.e., bins whose elements are all TreeNodes) are ordered primarily by hashCode, but in the case of
ties, if two elements are of the same "class C implements Comparable<C>", type then their compareTo
method is used for ordering. (We conservatively check generic types via reflection to validate this -- see
method comparableClassFor). The added complexity of tree bins is worthwhile in providing worst-case O(log
n) operations when keys either have distinct hashes or are orderable, Thus, performance degrades gracefully
under accidental or malicious usages in which hashCode() methods return values that are poorly distributed,
as well as those in which many keys share a hashCode, so long as they are also Comparable. (If neither of
these apply, we may waste about a factor of two in time and space compared to taking no precautions. But
the only known cases stem from poor user programming practices that are already so slow that this makes
little difference.)
挑上述这段注释的几个重点:
(1)同一个哈希桶中的树节点根据hashCode值排序,但当hashCode值相同且键所属的类实现了Comparable接口,会根据Comparable接口定义的compareTo方法来排序。
(2)HashMap中提供了comparableClassFor()方法来对泛型信息(即键所属的类)进行检查。
(3)在红黑树中查找节点的时间复杂度是O(logn)。
Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough
nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or
resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are
rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution
(http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default
resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance,
the expected occurrences of list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million
挑上述这段注释的几个重点:
(1)树节点所占的空间是普通节点的两倍,因此只有在同一个哈希桶中的节点数量大于等于TREEIFY_THRESHOLD时,才会将该桶中原来的链式存储的节点转化为红黑树的树节点。
(2)当一个哈希桶中的节点数量过少时,原来的红黑树树节点又会转化为链式存储的普通节点。
(3)理论上,在随机生成hashCode的情况下,一个哈希桶中的节点数概率满足泊松分布,其概率如上所示,其中我们可以看到一个桶中有8个节点(转化为红黑树的树节点)的概率只有0.00000006(亿分之六)。
The root of a tree bin is normally its first node. However, sometimes (currently only upon Iterator.remove),
the root might be elsewhere, but can be recovered following parent links (method TreeNode.root()).
12
挑上述这段注释的一个重点:
一般来说,树节点的根节点是该哈希桶中的第一个节点(当调用Iterator.remove()方法时不满足该条件),但可通过TreeNode.root()方法恢复。
All applicable internal methods accept a hash code as an argument (as normally supplied from a public
method), allowing them to call each other without recomputing user hashCodes. Most internal methods also
accept a "tab" argument, that is normally the current table, but may be a new or old one when resizing or
converting.
When bin lists are treeified, split, or untreeified, we keep them in the same relative access/traversal order (i.e., field Node.next) to better preserve locality, and to slightly simplify handling of splits and traversals that invoke iterator.remove. When using comparators on insertion, to keep a total ordering (or as close as is required here) across rebalancings, we compare classes and identityHashCodes as tie-breakers.
12345
挑上述这段注释的一个重点:
当节点发生树化、分摊化、去树化等操作时,我们保持节点在利用Node.next方式遍历时的相对顺序不变。
The use and transitions among plain vs tree modes is complicated by the existence of subclass
LinkedHashMap. See below for hook methods defined to be invoked upon insertion, removal and access that
allow LinkedHashMap internals to otherwise remain independent of these mechanics. (This also requires that
a map instance be passed to some utility methods that may create new nodes.)
The concurrent-programming-like SSA-based coding style helps avoid aliasing errors amid all of the twisty pointer operations.
12345
注释分析总结
1 线程安全方面:
(1)HashMap是线程不安全的。
(2)当多线程同时访问HashMap,且至少有一个线程对HashMap做出了结构性的修改(如新增/删除键值对),必须在外部加同步措施。
(3)可以利用Collections.synchronizedMap()方法来将HashMap包装成为线程安全的容器。
2 键值特性方面:
(1)HashMap允许存放null键和null值。
(2)HashMap不保证存放的键值对的顺序,甚至该顺序随着HashMap中键值对的增加/删除可能会发生变化。
(3)HashMap利用哈希函数把键值对元素合理地分布在各个哈希桶中,保证了get和put操作的时间复杂度是O(1)。
(4)当太多键的hashCode()值相同时,会影响HashMap的性能。
(5)当出现太多键有相同的hashCode()值的情况时,如果此时键实现了Comparable接口,那么HashMap会利用这点做一些措施,来提高性能。
3 初始容量和负载率方面:
(1)初始哈希桶数量和负载率是影响HashMap性能的两个重要参数。
(2)迭代操作取决于哈希桶的数量和HashMap中键值对的数量。因此,为了良好的迭代性能,不要把哈希桶的初始容量设得太高,也不要把HashMap的负载率设得太低。
(3)当HashMap中的键值对数量超过负载率和当前哈希桶数量的乘积时,HashMap会发生扩容操作(哈希桶数量翻倍)。
(4)将默认负载率设为0.75是时间性能和空间性能上的权衡结果,过高的负载率虽然可以提高空间性能,但会降低时间性能。
(5)当设置初始哈希桶数量时,需要考虑HashMap中准备存放的键值对数目和负载率,以尽量减少扩容操作的发生频率。
4 Fail-Fast机制方面:
(1)当用迭代器遍历HashMap时,如果该HashMap发生了结构性的修改,会抛出ConcurrentModificationException异常。
(2)Fail-Fast抛出ConcurrentModificationException异常不是一种可靠的机制,不要让你的程序的运行依赖于该机制。
5 节点转化方面:
(1)当某个哈希桶中的键值对过多时,HashMap会将该桶中原来的链式存储的节点转化为红黑树的树节点。
(2)当一个哈希桶中的节点数量过少时,原来的红黑树树节点又会转化为链式存储的普通节点。
(3)同一个哈希桶中的树节点根据hashCode值排序,但当hashCode值相同且键所属的类实现了Comparable接口,会根据Comparable接口定义的compareTo方法来排序。
(4)树节点所占的空间是普通节点的两倍,因此只有在同一个哈希桶中的节点数量大于等于TREEIFY_THRESHOLD时,才会将该桶中原来的链式存储的节点转化为红黑树的树节点。
(5)理论上,在随机生成hashCode的情况下,一个哈希桶中的节点数概率满足泊松分布,其概率如上所示,其中我们可以看到一个桶中有8个节点(转化为红黑树的树节点)的概率只有0.00000006(亿分之六)。
(6)一般来说,树节点的根节点是该哈希桶中的第一个节点(当调用Iterator.remove()方法时不满足该条件),但可通过TreeNode.root()方法恢复。
(7)当节点发生树化、均摊、去树化等操作时,我们保持节点在利用Node.next方式遍历时的相对顺序不变。
HashMap继承的类和实现的接口
看完了大段注释之后,想必大家对HashMap的许多特性都有了初步的印象,这么多的特性是怎么实现的呢?一起来看看代码吧!先从HashMap所继承的类和实现的接口看起。
如上图所示,HashMap继承了AbstractMap类,实现了接口Map、Serializable和Cloneable。
这里有一个疑点,从上图我们也可以看出:HashMap直接实现Map接口,而又间接地通过继承AbstractMap类实现了Map接口,实现了两次Map接口,这是为什么呢?
主要有两种说法:
(1)添加Map接口声明是为了Class类的getInterfaces方法能够直接获取到Map接口。
Class类的getInterfaces()方法不能获取到父类实现的接口,如果不写上实现Map接口,这个方法返回的数组中就没有Map.class。
(2)这就是一个写法上的错误,并没有深意。
HashMap中的常量
//序列化与反序列化用
private static final long serialVersionUID = 362498820763181265L;
//初始哈希桶数量,必须是2的幂,通过移位运算提高运算效率
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大哈希桶数量,必须是2的幂,通过移位运算提高效率
static final int MAXIMUM_CAPACITY = 1 << 30;
//初始负载率
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当某个哈希桶中节点数量大于等于该值时,会转化为树节点
static final int TREEIFY_THRESHOLD = 8;
//当某个哈希桶中节点数量小于等于该值时,会转化为普通节点
static final int UNTREEIFY_THRESHOLD = 6;
//只有当哈希桶的数量大于等于64时,某个哈希桶中的节点才可能因为节点过多而转化为树节点
//treeifyBin函数中会用到该变量,后续会提及
static final int MIN_TREEIFY_CAPACITY = 64;
这里有几个关键的问题需要解释一下:
为什么哈希桶的数量必须是2的幂?
(1)设置为2的幂我们可以通过移位运算提高运算效率。
(2)在介绍putVal()函数时会解释这一点。
某个哈希桶中的普通节点转化为树节点的界限为什么是8?树节点转化为普通节点界限为什么是6?
(1)之前讲的“泊松分布”,树节点的开销比普通节点要大,而根据泊松分布,大于等于8的概率仅有亿分之六,因此选择8作为普通节点转化为树节点的界限。
(2)普通节点转化为树节点的界限是8而树节点转化为普通节点的界限是6,中间差了2,主要是为了防抖动。如果普通节点转化为树节点的界限和树节点转化为普通节点的界限相同,那么一旦插入一个节点达到了这个界限,就需要从普通节点转化为树节点;删除这个新插入的节点后,又要从树节点转化为普通节点,这样频繁的抖动的开销是很大的。
为什么只有当哈希桶的数量大于等于64时,某个哈希桶中的节点才可能因为节点过多而转化为树节点?
按JDK中的说法,树化的桶数量阈值至少是4倍的TREEIFY_THRESHOLD,因此至少因是32,否则在树化操作和扩容操作间会产生冲突。
HashMap中的节点类型
Node
static class Node<K,V> implements Map.Entry<K,V> {
//hash值定义为final,不可变,在构造函数中初始化该值
final int hash;
//key值定义为final,不可变的引用,但引用的对象可变,在构造函数中初始化该值
final K key;
//value值是可变的
V value;
//next指针,可变
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() {
//将key的hashCode值和value的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;
}
}
划重点:
(1)Node节点实现了Map.Entry接口,有4个属性,hash值、key值、value值、next指针。
(2)Node节点的hash值是不可变的。
(3)Node节点的key引用是不可变的,但key指向的对象是可变的。
(4)Node节点的value值、next指针都是可变的。
TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
//前一个节点
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red; //说明这是一颗红黑树
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
//返回红黑树的根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
//调整红黑树的结构,使指定的节点root成为对应哈希桶中的的第一个节点
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash; //找到桶位
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) { //当根节点不是桶中第一个元素时
Node<K,V> rn;
tab[index] = root; //根节点放在桶的第一位
TreeNode<K,V> rp = root.prev; //根的前一个节点
//将根节点从双链表中抽出,原来的位置前后链接
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
//将根节点放在双链表的首位
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
//从this树节点查找hash值为h,Key为k的节点
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q; // 当前节点的左右孩子
if ((ph = p.hash) > h) // hash值小的从左子树迭代查找
p = pl;
else if (ph < h) // hash值大的从右子树迭代查找
p = pr;
// hash值相等,且键地址相同或都为空时,查找成功
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// hash值相等,但键不相同,且节点没有左子树,就从右子树查找
else if (pl == null)
p = pr;
// hash值相等,但键不相同,且节点没有右子树,就从左子树查找
else if (pr == null)
p = pl;
// 比较两个Key
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
// Key不可比较或比较结果为0时,先在右子树中查找
else if ((q = pr.find(h, k, kc)) != null)
return q;
// 右子树查找不到时
else
p = pl;
} while (p != null);
return null;
}
// 从根节点查找hash值为h,Key为k的节点
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
// 强行比较两个对象,结果为-1或1
static int tieBreakOrder(Object a, Object b) {
int d;
// a和b都不为空时比较它们的类名
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
// a为null,或b为null,或类名也相等时,比较它们的内存地址
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
/**
* Returns a list of non-TreeNodes replacing those linked from
* this node.
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
}
划重点:
(1)TreeNode继承了LinkedHashMap.Entry类,而LinkedHashMap.Entry类实际上继承了HashMap.Node类,就是前面提及的Node节点,因此TreeNode是Node的子类。
(2)TreeNode包含4个指针,parent、left、right、prev,其中parent、left和right指针的作用很好理解,分别指向其父节点、左孩子和右孩子,prev指针在删除节点时有用。
HashMap中的非静态字段
//哈希桶数组table
transient Node<K,V>[] table;
//用于实现抽象类AbstractMap的keySet()和values()方法,存储的是HashMap中的键值对集合。
transient Set<Map.Entry<K,V>> entrySet;
//HashMap中的键值对数目
transient int size;
//HashMap发生结构性修改(如新增/删除键值对、扩容等操作)的次数,用于前文提到的迭代操作时的Fail-Fast机制的实现
transient int modCount;
//扩容阈值,当HashMap中的键值对数目超过该值时,会发生扩容操作
int threshold;
//负载率
final float loadFactor;
123456789101112
HashMap中的构造函数
HashMap一共提供了4个构造函数,我们一一来分析。
设定初始桶数量和负载率
public HashMap(int initialCapacity, float loadFactor) {
//如果初始桶数量小于0,抛出参数异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始桶数量不能大于最大桶数量,即前文提及的常量MAXIMUM_CAPACITY = 1 << 30,
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果负载率小于等于0,或者说负载率不是一个Float型的数,抛出参数异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//设定负载率
this.loadFactor = loadFactor;
//设定threshold的值,此时table数组还没有被初始化,故此时threshold是初始哈希桶数量
//tableSizeFor方法会在后续分析,这里说一下它的作用:
//将传进的初始哈希桶数量调整为2的幂
this.threshold = tableSizeFor(initialCapacity);
}
12345678910111213141516171819
设定初始桶数量
public HashMap(int initialCapacity) {
//调用构造函数1,负载率为默认负载率DEFAULT_LOAD_FACTOR,其值为0.75
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
1234
空参构造函数
public HashMap() {
//负载率为默认负载率DEFAULT_LOAD_FACTOR,其值为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
1234
传一个Map
public HashMap(Map<? extends K, ? extends V> m) {
//负载率为默认负载率DEFAULT_LOAD_FACTOR,其值为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
//将原Map里的键值对存放进新的HashMap里,putMapEntries方法会在后面提及
putMapEntries(m, false);
}
123456
HashMap中的关键函数
hash
该函数的作用是,计算key的哈希值。注意,这个hash不是HashMap中的哈希运算,在后面我们会看到,HashMap的哈希运算是在该函数的基础上进行的。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1234
key的哈希值的计算方式如下:
(1)如果key为null,则返回0。
(2)否则,返回key的哈希值和其无符号右移16位的值异或位运算的结果。
为什么采取上述计算方式呢?为此,JDK给出了一段详尽的说明。
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.
12345678
挑上述这段注释的一个重点:
因为HashMap中哈希桶的数量是2的幂,其哈希运算是对哈希桶数量的取模运算,即相当于计算机网络中的子网掩码,因此如果不将key的哈希值右移以此来考虑高位的影响,那么哈希运算就仅仅与key值的hashCode函数的低位相关(举个例子,假如key的hashCode值是00000000 10101010 00000000 10010011,哈希桶的数量是16,将key的hashCode对16取模,相当于00000000 10101010 00000000 10010011和00000000 00000000 00000000 00001111做位与运算,我们可以看到,无论高位的值是多少,都会成为0,即高位不会对哈希运算的结果产生任何影响)。
讲得这么复杂,其实简单地理解就是为了降低哈希冲突的概率。
tableSizeFor
这个函数在HashMap的第1个构造函数里出现过了,其作用是将传进的初始哈希桶数量调整为2的幂。该函数巧妙地利用了位运算实现了这样的功能,提高了效率。
static final int tableSizeFor(int cap) {
//假设传进cap = 34,那么n = cap - 1 = 33
int n = cap - 1;
// n 00000000 00000000 00000000 00100001
// n >>> 1 00000000 00000000 00000000 00010000
// 异或运算结果 00000000 00000000 00000000 00110001
n |= n >>> 1;
// n 00000000 00000000 00000000 00110001
// n >>> 2 00000000 00000000 00000000 00001100
// 异或运算结果 00000000 00000000 00000000 00111101
n |= n >>> 2;
// n 00000000 00000000 00000000 00111101
// n >>> 4 00000000 00000000 00000000 00000011
// 异或运算结果 00000000 00000000 00000000 00111111
n |= n >>> 4;
// n 00000000 00000000 00000000 00111111
// n >>> 8 00000000 00000000 00000000 00000000
// 异或运算结果 00000000 00000000 00000000 00111111
n |= n >>> 8;
// n 00000000 00000000 00000000 00111111
// n >>> 8 00000000 00000000 00000000 00000000
// 异或运算结果 00000000 00000000 00000000 00111111
n |= n >>> 16;
// 最后返回的是n + 1,即64
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
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; //新的哈希桶数目和新的扩容阈值
if (oldCap > 0) {
//旧哈希桶数目已经大于等于哈希桶的最大数目MAXIMUM_CAPACITY,显然不可能再扩容了
if (oldCap >= MAXIMUM_CAPACITY) {
//将扩容界限值设置为Integer.MAX_VALUE,防止下次再触发扩容操作
threshold = Integer.MAX_VALUE;
//返回原table数组
return oldTab;
}
//将新哈希桶数目newCap设置为旧哈希桶数目oldCap的两倍,移位运算实现,提高效率
//如果此时新哈希桶数目newCap小于MAXIMUM_CAPACITY
//且旧哈希桶数目oldCap大于等于默认初始哈希桶数目DEFAULT_INITIAL_CAPACITY
//为什么这里还需要满足旧哈希桶数目oldCap大于等于默认初始哈希桶数目
//DEFAULT_INITIAL_CAPACITY这个条件呢?
//在分析HashMap中的非静态字段时有分析,如果table数组还没有被初始化,这个字段存
//储的不是扩容阈值,而是初始哈希桶数目或0。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//将新的扩容阈值设为旧扩容阈值的两倍,移位运算实现,提高效率
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//此时oldCap为0,说明table数组还没有被初始化,此时的新哈希桶数目就设置为旧的扩
//容阈值,其值为16。
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//此时oldCap为0,oldThr也为0,设置新哈希桶数目为DEFAULT_INITIAL_CAPACITY
newCap = DEFAULT_INITIAL_CAPACITY;
//设置新的扩容阈值
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果还没有设置新的阈值,这对应于oldCap > 0中的else-if语句不满足的情况
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;
//加下来我们要做的事情,就是把oldTab中的键值对移动到newTab里来
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) { //遍历oldTab中的每一个桶
Node<K,V> e;
if ((e = oldTab[j]) != null) { //e执行桶中的第一个节点
oldTab[j] = null; //将原桶中的节点置为null
if (e.next == null) //如果桶中只有e一个节点
//用新的哈希桶容量计算e在新表中的位置
//这里是真正的哈希运算出现处,HashMap中的哈希运算是和哈希桶数目减
//1做位与运算得出的,举个例子,哈希桶数目是16,则和15的二进制表示
//00000000 00000000 00000000 00001111做位与运算,相当于对16
//取模。结合前文hash的计算方法这就将键值对均匀地分配到了各个哈希桶
//中。
newTab[e.hash & (newCap - 1)] = e;
//如果桶中不仅只有e一个节点,且节点类型是树节点
else if (e instanceof TreeNode)
//进行树节点的转移
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果桶中不仅只有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;
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;
}
resize()操作用于第一次给table数组初始化,或是table数组的扩容,其树形节点的转移见后面对树形节点操作的分析,其普通节点的转移过程分析如图所示:
(1)假设初始哈希桶数目为16,在编号为3的哈希桶里有3个普通节点(其余节点未画出),其key的hash函数返回值分别是3、19、35。
(2)开始处理桶中的第一个节点
此时e指针指向3号桶的第一个节点,next指针指向3号桶的第二个节点。
将此时e的哈希值3与oldCap的值即16作位与运算,00000000 00000000 00000000 00000011 & 00000000 00000000 00000000 00010000,得到值0,此时hiTail为null,因此令hiHead指向e,同时hiTail也指向e,之后令e = e.next,开始处理3号桶中的第二个节点。
(3)开始处理桶中的第二个节点
此时e指针指向3号桶的第二个节点,next指针指向3号桶的第三个节点。
将此时e的哈希值19与oldCap的值即16作位与运算,00000000 00000000 00000000 00010011 & 00000000 00000000 00000000 00010000,得到值16,此时loTail为null,因此令loHead指向e,同时loTail也指向e,之后令e = e.next,开始处理3号桶中的第三个节点。
(4)开始处理桶中的第三个节点
此时e指针指向3号桶的第三个节点,next指针指向NULL。
将此时e的哈希值35与oldCap的值即16作位与运算,00000000 00000000 00000000 00100011 & 00000000 00000000 00000000 00010000,得到值0,此时loTail不为null,因此令loTail的next节点指向e,再更新loTail指向e,之后令e = e.next,循环结束。
(4)将loTail和hiTail的next指针都置为null,就分离出了两个处理低位链表和高位链表的指向,loHead在新table中的索引和旧table中的索引相同,hiHead在新table中的索引比旧table中的索引大16(即原table的大小),原来在3号桶中的节点转移至新table后,其存放位置如下图所示。
为什么loHead在新table中的索引和旧table中的索引相同?hiHead在新table中的索引比旧table中的索引大16(即原table的大小)?
(1)令loHead在新table中的索引和旧table中的索引相同,仅仅是为了避免对其他桶产生影响。理想情况下,如本例,原链表中的节点其key的hash值相差16,因此原链表中的节点分成两半,一半在loHead中,另一半在hiHead中。因此原来在[0, 15]范围内的节点有一半会转移到新区域[16, 31]。
(2)区分loHead中的节点和hiHead中节点的标准是其key的hash值与原table大小做位与运算的结果是否为0,而原table大小必定是2的幂,即其二进制表示中,仅有1个二进制位是1,其余二进制位均是0,因此hiHead在新table中的索引比旧table中的索引大16(即原table的大小)。
putVal
该函数是往HashMap中新增节点的函数,输入的参数有5个,onlyAbsent如果为true,表示仅新增节点而不更新节点,evit用于LinkedHashMap的回调方法afterNodeInsertion,该函数会返回旧值(如果没有旧值,则返回null)。
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数组为null或table数组的长度为0,扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果经过哈希运算后对应的桶上没有任何节点
//p是某个桶的第一个节点,其哈希运算结果与key的哈希运算结果相同
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果p的哈希值与hash相同,且p节点的key与key相同,则令e指向p
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果p是树节点,调用树节点的新增节点方法,后序会说明这个方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//p是普通节点
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//将新节点添加在链表尾部
p.next = newNode(hash, key, value, null);
//这里binCount没有把新增的节点计算进去,因此这里实际的意义是:
//如果哈希桶中的节点数目大于等于TREEIFY_THRESHOLD,树化节点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果p的哈希值与hash相同,且p节点的key与key相同,break跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//这里e已经等于了p.next,因此这里是令p = p.next,即遍历下一个节点
p = e;
}
}
if (e != null) { // existing mapping for key
//如果找到了e,说明这是一个更新value的操作,不是新增键值对的操作
V oldValue = e.value;
//如果onlyIfAbsent为false,说明可以更新value值
//当然原value值为null时,无论onlyIsAbsent是否为false都可以更新value值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); //这是为LinkedHashMap准备的回调方法
return oldValue;
}
}
++modCount; //HashMap的结构性修改次数加1,++modCount的运算速度比modCount++要快一些
if (++size > threshold) //如果新增键值对后HashMap中的键值对数量大于了扩容阈值,进行扩容操作
resize();
afterNodeInsertion(evict); //这是为LinkedHashMap准备的回调方法
return null; //如果是新增节点,该函数返回null。
}
这里可以解答一下在前文——HashMap中的常量留下的一个小问题(其实在resize方法的注释里已经有了说明):
为什么哈希桶的数量必须是2的幂?
HashMap中的哈希运算是和哈希桶数目减1做位与运算得出的,举个例子,哈希桶数目是16,则和15的二进制表示00000000 00000000 00000000 00001111做位与运算,相当于对16取模。结合前文hash的计算方法这就将键值对均匀地分配到了各个哈希桶中。
getNode
该函数的作用是,根据hash值和key值在HashMap中寻找相应的Node节点。
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//确保table已经被初始化而且不是一个空数组,令first指向用hash值进行哈希运算得到的哈希桶中的第一个节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果first节点就是我们所要寻找的节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//令e指向first节点的下一个节点
if ((e = first.next) != null) {
//如果是树节点,做特殊处理
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//循环遍历链表,寻找key
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//没找到,返回null
return null;
}
removeNode
该函数的作用是删除某节点,会返回被删除的节点。如果matchValue为true,代表只有node节点的value值和传入的value值相等时才删除node。如果movable为true,代表可以移动其他节点(意思是可以发生去树化操作)。注意:移除节点并不会发生缩容操作。
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//确保table已经被初始化而且不是一个空数组,令p指向用hash值进行哈希运算得到的哈希桶中的第一个节点
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;
//接下来的任务就是寻找待删除的节点node
//如果p节点就是待删除节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//令e指向p节点的下一个节点
else if ((e = p.next) != null) {
//如果p是树节点,做特殊处理
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
//循环遍历链表,寻找key
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break; //一旦找到了key就跳出循环
}
p = e; //令p指向node的前一个节点
} while ((e = e.next) != null);
}
}
//现在我们找到了待删除的节点node,接下来我们的任务就是删除该节点
//删除条件有以下几个:
//(1)node不能为null。
//(2)matchValue为false(如果matchValue为true,代表只有node节点的value值和传入的value值相等时才删除node)或
//node节点的value值和传入的value值相等(这时无论matchValue是否为true都无所谓)
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);
//如果待删除的node是哈希桶中的的第一个节点,直接修改相应哈希桶的指针即可
else if (node == p)
tab[index] = node.next;
//如果待删除节点不是哈希桶中的第一个节点,此时p指向的是node的前一个结点,只需改变p的next域的指向为node的next域即可
else
p.next = node.next;
++modCount; //结构性修改次数加1
--size; //HashMap中的键值对数目减1
afterNodeRemoval(node); //LinkHashMap的回调方法
return node; //返回被删除的节点值
}
}
//如果没有需要删除的节点,返回null
return null;
}
HashMap中的非关键函数
在HashMap中的关键函数这一小节,我们分析了6个关键函数,分别涉及hash值的获取,增、删、改、查4个方面,以及扩容操作。这一小节,再来看看一些非关键函数,我们会发现许多非关键函数都是对上述6个关键函数的简单调用。
comparableClassFor
这个函数的作用是:如果x实现了Comparable接口,那么返回x的实际类型,否则返回null。这个函数在后续涉及树节点的代码会用到。
static Class<?> comparableClassFor(Object x) {
//检查x是否实现了Comparable接口
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
//如果x是String类型
if ((c = x.getClass()) == String.class) // bypass checks
return c;
//获取x的类c所实现的所有接口的集合
if ((ts = c.getGenericInterfaces()) != null) {
//遍历接口集合
for (int i = 0; i < ts.length; ++i) {
//如果寻找到了Comparable接口,则返回c(x所属的类)
if (((t = ts[i]) instanceof ParameterizedType) &&
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
return c;
}
}
}
return null;
}
compareComparables
该函数的作用是返回k和x的比较结果。这个函数在后续涉及树节点的代码会用到。
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
putMapEntries
这个函数在HashMap的第4个构造函数里出现了。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
//如果table还没有被初始化
if (table == null) {
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
//此时table还没有被初始化,threshold中的值是初始哈希桶数量
threshold = tableSizeFor(t);
}
//此时table已经被初始化,如果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);
}
}
}
keySet、values、entrySet
这三个函数的作用分别是返回HashMap中的key值集合、value值集合和键值对集合。这三个函数的作用很类似,仅对keySet方法进行分析。对于keySet函数,JDK给出了一段详尽的注释说明。
Returns a Set view of the keys contained in this map. The set is backed by the map, so changes to the map
are reflected in the set, and vice-versa. If the map is modified while an iteration over the set is in progress
(except through the iterator's own remove operation), the results of the iteration are undefined. The set
supports element removal, which removes the corresponding mapping from the map, via the Iterator.remove,
Set.remove, removeAll, retainAll, and clear operations. It does not support the add or addAll operations.
挑上述这段注释的几个重点:
(1)keySet返回的Set集合和此HashMap是关联在一起的,修改HashMap会导致该Set集合的改变,修改该Set集合也会导致HashMap的改变。
(2)如果在迭代该Set集合的时候修改HashMap的值,迭代结果是不确定的。
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount; //在遍历前记录一下修改次数
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc) //如果在遍历过程中HashMap发生了结构性修改,抛出ConcurrentModificationException
throw new ConcurrentModificationException();
}
}
}
HashMap中的内部迭代器类
HashIterator
这是一个抽象类,KeyIterator、ValueIterator和EntryIterator均继承了这个抽象类
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0; //从第一个桶开始遍历
if (t != null && size > 0) { // advance to first entry
//跳过空桶,使index指向table数组中从索引0开始的第一个非空的哈希桶的索引加1处
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount) //如果在迭代期间HashMap发生了结构性修改,抛出ConcurrentModificationException
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//如果e节点已经是当前桶的最后一个节点
if ((next = (current = e).next) == null && (t = table) != null) {
//跳过空桶,使index指向table数组中的下一个非空的哈希桶的索引加1处
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException(); //如果在迭代期间HashMap发生了结构性修改,抛出ConcurrentModificationException
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false); //LinkedHashMap的回调方法
//为什么用迭代器的remove操作在迭代时删除元素不会抛出ConcurrentModificationException,
//就是因为在remove函数里重新设置了expectedModCount的值为modCount
expectedModCount = modCount;
}
}
KeyIterator、ValueIterator和EntryIterator
如前文所述,这三个类都继承了HashIterator类,并实现了Iterator接口,分别返回下一个键、值或键值对。
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
HashMapSpliterator
为了适应时代的变化,现在并行计算越来越需要。HashMapSpliterator的诞生正是为了并行迭代而出现的,可以在多线程的情况下迭代同一个 HashMap,但是官方建议一个线程只和一个迭代器配合使用。
static class HashMapSpliterator<K,V> {
//需要遍历的HashMap对象
final HashMap<K,V> map;
Node<K,V> current; // current node
int index; // current index, modified on advance/split
//当前迭代器遍历上限的桶索引
int fence; // one past last index
//需要遍历的元素个数
int est; // size estimate
int expectedModCount; // for comodification checks
HashMapSpliterator(HashMap<K,V> m, int origin,
int fence, int est,
int expectedModCount) {
this.map = m;
this.index = origin;
this.fence = fence;
this.est = est;
this.expectedModCount = expectedModCount;
}
//这个方法的作用是获取一个当前迭代器的一个迭代范围,例如返回的值是4,那么遍历到第四个桶就会结束
final int getFence() { // initialize fence and size on first use
int hi;
if ((hi = fence) < 0) {
HashMap<K,V> m = map;
est = m.size; //需要遍历的元素个数为
expectedModCount = m.modCount;
Node<K,V>[] tab = m.table;
hi = fence = (tab == null) ? 0 : tab.length; //如果tab已经被初始化了那么返回tab数组的长度
}
return hi;
}
public final long estimateSize() {
getFence(); // force init
return (long) est;
}
}
KeySpliterator、ValueSpliterator、EntrySpliterator
这三个类均继承了HashMapSpliterator,并实现了Spliterator接口,由于其实现方式几乎相同,而代码量又偏大,这里仅以KeySpliterator为例进行分析。
static final class KeySpliterator<K,V>
extends HashMapSpliterator<K,V>
implements Spliterator<K> {
KeySpliterator(HashMap<K,V> m, int origin, int fence, int est,
int expectedModCount) {
super(m, origin, fence, est, expectedModCount);
}
//对当前迭代器进行分割
public KeySpliterator<K,V> trySplit() {
//这里的分割方法只是把当前迭代器的开始索引和最后索引除以二而已
int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
return (lo >= mid || current != null) ? null :
//需要遍历的元素个数est也需要除以二
new KeySpliterator<>(map, lo, index = mid, est >>>= 1,
expectedModCount);
}
//在当前迭代器遍历范围遍历一遍
public void forEachRemaining(Consumer<? super K> action) {
int i, hi, mc;
if (action == null)
throw new NullPointerException();
HashMap<K,V> m = map;
Node<K,V>[] tab = m.table;
if ((hi = fence) < 0) {
mc = expectedModCount = m.modCount;
hi = fence = (tab == null) ? 0 : tab.length;
}
else
mc = expectedModCount;
if (tab != null && tab.length >= hi &&
(i = index) >= 0 && (i < (index = hi) || current != null)) {
Node<K,V> p = current;
current = null;
do {
if (p == null)
p = tab[i++];
else {
action.accept(p.key);
p = p.next;
}
} while (p != null || i < hi);
if (m.modCount != mc)
throw new ConcurrentModificationException();
}
}
//会遍历迭代器遍历的范围之内的元素,当找到第一个非空元素的时候就会停止遍历
public boolean tryAdvance(Consumer<? super K> action) {
int hi;
if (action == null)
throw new NullPointerException();
Node<K,V>[] tab = map.table;
if (tab != null && tab.length >= (hi = getFence()) && index >= 0) {
while (current != null || index < hi) {
if (current == null)
current = tab[index++];
else {
K k = current.key;
current = current.next;
action.accept(k);
if (map.modCount != expectedModCount)
throw new ConcurrentModificationException();
return true;
}
}
}
return false;
}
public int characteristics() {
return (fence < 0 || est == map.size ? Spliterator.SIZED : 0) |
Spliterator.DISTINCT;
}
}
附上一段测试代码:
public class SpliteratorTest {
static class MyThread implements Runnable {
Spliterator<Integer> spliterator;
String threadName;
MyThread(Spliterator<Integer> spliterator, String threadName) {
this.spliterator = spliterator;
this.threadName = threadName;
}
@Override
public void run() {
spliterator.forEachRemaining(s -> System.out.println(threadName + "=" + s));
}
}
public static void main(String[] args) {
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < 23; i++) {
map.put(i, i);
}
Spliterator<Integer> s1 = map.keySet().spliterator();
Spliterator<Integer> s2 = s1.trySplit();
Spliterator<Integer> s3 = s2.trySplit();
Thread t1 = new Thread(new MyThread(s1, "线程1"));
Thread t2 = new Thread(new MyThread(s2, "线程2"));
Thread t3 = new Thread(new MyThread(s3, "线程3"));
t1.start();
t2.start();
t3.start();
}
}
上述代码的测试结果为:
线程2=8
线程2=9
线程2=10
线程2=11
线程2=12
线程2=13
线程2=14
线程2=15
线程3=0
线程3=1
线程3=2
线程3=3
线程3=4
线程3=5
线程3=6
线程3=7
线程1=16
线程1=17
线程1=18
线程1=19
线程1=20
线程1=21
线程1=22
HashMap中与树节点相关的关键方法
介绍树节点相关的方法前,先介绍一下红黑树的相关概念。什么是红黑树?满足下述5个性质的就是红黑树。
(1)树中的节点非黑即红。
(2)根节点是黑色的。
(3)叶子节点(null)是黑色的。
(4)红色节点的孩子都是黑色的。
(5)从根节点到任一叶子节点的路径上,黑色节点的数量是相同的。
treeifyBin
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果tab未初始化,或者说哈希桶数目小于MIN_TREEIFY_CAPACITY,执行扩容操作
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// e指向哈希桶的第一个节点
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
//在替换为树节点的过程中,保持next指针的相对顺序不变
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
//如果该哈希桶中第一个节点不为null,树化该哈希桶中的节点
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
putTreeVal
HashMap的putVal方法会调用树方法putTreeVal来插入新的键值对。
该方法首先会查找树中是否有这个键,有的话就返回这个节点的引用,但注意,这里没有马上更新Value。查找的过程中,首先比较hash值,其次再比较Key,应该是考虑到比较int类型的速度更快。没有找到的话,会新建一个树的叶子节点,再调用树方法balanceInsertion插入新节点到红黑树中。
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
//找到树根
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) { //p为当前和新元素比较的节点
int dir, ph; K pk;
//根据hash值比较大小
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
//hash值相等时,如果键指向同一地址,查找结束
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//hash值相等,比较两个键
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//键不可比较,或者比较结果为0时,在节点的左右子树查找该Key,找到就结束
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
//比较两个对象的内存地址
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
//找到叶子节点还没找到时
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
//将当前节点插入到p节点之后,注意这里的前后是双链表的前后
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//当前节点和p节点建立父子关系,这里是在树结构中
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
//将树结构中刚刚插入的元素向上红黑化,然后将树的根节点排到桶的第一位
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
treeify
HashMap的treeifyBin方法会调用该方法,来将链表构建为红黑树。这个方法的主体和putTreeVal方法类似(不同的是,它不会遇到重复Key值),向只含有一个元素的红黑树上不断添加新的节点。
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null; // 初始化树节点
// 初始化根节点,根节点为黑链接
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
// 将每一个节点插入树中
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
// 根据hash值比较
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
// hash值相等时,比较键
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
// 如果节点的键不是Comparable类,
// 或者两个节点键比较的结果为相等,就强行让比较结果不为0
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
// 根据比较的结果判断在左子树还是右子树
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 子树为null,查找结束,将节点作为树叶子节点
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 从新添加的元素往上更新红黑树结构,见红黑树操作
root = balanceInsertion(root, x);
break;
}
}
}
}
// 确保红黑树的根节点,同时也是桶位的第一个节点
moveRootToFront(tab, root);
}
balanceInsertion
该方法用于往红黑树中平衡插入节点x。
插入操作主要处理以下三种情况的迭代:
第一种,上浮颜色,上浮后迭代插入红色的xpp节点(图中的2)即可。
第二种和第三种通过旋转转化成了稳定结构,不需要再迭代。
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
x.red = true; // 新节点为红色
// 注意这是一个循环体,将节点一直上浮
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// x为根节点
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// xp为黑节点,或者xp为根节点时
// 根节点也是黑节点
// 黑节点的孩子可以是红,所以更新结束
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 下面的情况中父节点都为红
// xp在xpp的左子树时
if (xp == (xppl = xpp.left)) {
// xp和xppr都为红
// 直接上浮颜色即可,见示意图1,x = xpp继续迭代
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp; // 上浮节点为xpp
}
// xp是红,xppr是黑时
// 见示意图2,最终转化为父节点为黑,所以下一轮就结束
else {
// 当前节点为右孩子时,左旋父节点
// 转变为xppl、xppll都为红的模式
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 转变为xp的左右孩子都为红的模式
if (xp != null) {
xp.red = false; // xp改为黑色
if (xpp != null) {
xpp.red = true; // xpp改为红色
root = rotateRight(root, xpp); // 右旋xpp
}
}
}
}
// xp在xpp的右子树时
else {
// xp和xppl都为红
// 直接上浮颜色即可,类似示意图1,x = xpp继续迭代
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp; // x上浮到xpp
}
// xp是红,xppl是黑时
// 见示意图3,最终转化为父节点为黑,所以下一轮就结束
else {
// 当前节点为左孩子时,右旋父节点
// 转变为xppr、xpprr都为红的模式
if (x == xp.left) {
root = rotateRight(root, x = xp); // 右旋父节点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 转变为xp的左右孩子都为红的模式
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true; // xpp改成红色
root = rotateLeft(root, xpp); // 左旋xpp
}
}
}
}
}
}
rotateLeft
// 节点p左旋转,注意这里没有改变颜色
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
// 当前节点以及右孩子不为空时
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
rotateRight
// 节点p右旋转,没有改变颜色
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
// 当前节点以及左孩子不为空时
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
参考文献
为什么HashMap继承了AbstractMap还要实现Map接口
深入扒 HashMap 源码 - 6.5 HashMap 内部类分割迭代器 HashMapSpliterator、KeySpliterator、ValueSpliterat、EntrySplitera
Jdk1.8集合框架之HashMap源码解析(详细解析红黑树)
转载自:https://blog.csdn.net/qq_41231926/article/details/95791180