java集合---HashMap源码分析
HashMap简介(1.8)
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
HashMap 默认的初始化容器大小为16,负载因子为0.75。
/**
* 基于哈希表的Map接口实现。此实现提供所有可选的映射操作,并允许 空值和空键。(HashMap 类大致相当于Hashtable,除了它是不同步的并且允许空值。)这个类不保证映射的顺序; 特别是,它不保证顺序会随着时间的推移保持恒定。
* <p>
* 假设散列函数在桶之间正确地分散元素,该实现为基本操作(get和put)提供了恒定时间的性能。对集合视图迭代的时间需要与HashMap实例的“容量” (桶的数量)加上其大小(键 - 值映射的数量)成正比 。因此,如果迭代性能很重要,则不要将初始容量设置得太高(或负载因子太低)这一点非常重要。
* <p>
* HashMap的一个实例有两个影响其性能的参数:初始容量和负载因子。capacity是哈希表中存储桶的数量,初始容量只是创建哈希表时的容量。负载因子是衡量哈希表在自动增加其容量之前的填充程度的度量。当哈希表中的条目数超过加载因子和当前容量的乘积时,哈希表将被重新哈希(即,重建内部数据结构),以便哈希表具有大约两倍的桶数。
* <p>
* 作为一般规则,默认加载因子(.75)在时间和空间成本之间提供了良好的折衷方案。较高的值会减少空间开销,但会增加查找成本(反映在HashMap类的大多数操作中,包括 get和put)。在设置其初始容量时,应考虑映射中的预期条目数及其负载因子,以最大程度减少重新哈希的次数。如果初始容量大于最大条目数除以加载因子,则不会进行任何哈希操作。
* <p>
* 如果要将多个映射存储在HashMap 实例中,则使用足够大的容量创建映射将允许映射更有效地存储,而不是根据需要执行自动重新散列来扩展表。请注意,使用许多具有相同的键是降低任何哈希表性能的可靠方法。为了改善影响,当键出现时Comparable,这个类可以使用键之间的比较顺序来帮助打破平局。
* <p>
* 请注意,此实现不同步。 如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了映射,则必须在外部进行同步。(结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键相关联的值不是结构修改。)这通常通过同步自然封装映射的某个对象来完成。 。如果不存在此类对象,则应使用该Collections.synchronizedMap 方法“包装”地图 。这最好在创建时完成,以防止意外地不同步访问地图:
* Map m = Collections.synchronizedMap(new HashMap(...));
* <p>
* 所有这个类的“集合视图方法”返回的迭代器都是快速失败的:如果在创建迭代器之后的任何时候对映射进行结构修改,除了通过迭代器自己的 remove方法之外,迭代器将抛出一个 ConcurrentModificationException。因此,面对并发修改,迭代器快速而干净地失败,而不是在将来某个未确定的时间冒着任意的,不确定性的行为风险。
* <p>
* 请注意,迭代器的故障快速行为无法得到保证,因为一般来说,在存在非同步并发修改的情况下不可能做出任何严格的保证。失败快速迭代器会尽最大努力抛出ConcurrentModificationException。因此,编写依赖于此异常的程序以确保其正确性是错误的:迭代器的快速失败行为应该仅用于检测错误。
* <p>
* 此类是 Java Collections Framework的成员 。
*/
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的n次幂
* 1<<4 左移时不管正负,低位补0
* 1的二进制为1 左移4位为 10000 转换成十进制 1×2^4+0×2^3+0×2^2+0×2^2+0×2^0=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.
* 表示容器最大值 1×2^30= 1 073 741 824
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
* 加载因子 扩容时使用
* 当初始容量为16时 16×0.75=12 当集合容量使用到12时就会开始扩容为原来2倍为32
*/
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.
* 单个hash槽内元素个数大于等于8时 bin转换成trees
* 通过泊松分布可知单个hash槽内元素个数为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.
* 单个hash槽内元素个数小于等于6时 trees转换成bin
*/
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.
* 最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树)否则,若桶内元素太多时,则直接扩容,而不是树形化,为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
*/
static final int MIN_TREEIFY_CAPACITY = 64;
HashMap实施说明
/*
实现注意事项。
链表结构(这里叫 bin ,箱子)
Map通常充当一个binned(桶)的哈希表,但是当箱子变得太大时,它们就会被转换成TreeNodes的箱子,每个箱子的结构都类似于java.util.TreeMap。
大多数方法都尝试使用普通的垃圾箱,但是在适用的情况下(只要检查一个节点的实例)就可以传递到TreeNode方法。
可以像其他的一样遍历和使用TreeNodes,但是在过度填充的时候支持更快的查找
然而,由于大多数正常使用的箱子没有过多的填充,所以在表方法的过程中,检查树箱的存在可能会被延迟。
树箱(bins即所有的元素都是TreeNodes)主要是由hashCode来排序的,但是在特定的情况下,
如果两个元素是相同的“实现了Comparable接口”,那么使用它们的比较方法排序。
(我们通过反射来保守地检查泛型类型,以验证这一点——参见方法comparableClassFor)。
使用树带来的额外复杂,是非常有价值的,因为能提供了最坏只有O(log n)的时间复杂度当键有不同的散列或可排序。
因此,性能降低优雅地在意外或恶意使用hashCode()返回值的分布很差,以及许多key共享一个hashCode,只要他们是可比较的。
(如果这两种方法都不适用,同时不采取任何预防措施,我们可能会在时间和空间上浪费大约两倍的时间。
但是,唯一已知的案例源于糟糕的用户编程实践,这些实践已经非常缓慢,这几乎没有什么区别。)
因为TreeNodes大小大约是普通节点的两倍,所以只有当容器包含足够的节点来保证使用时才使用它们(见treeifythreshold)。
当它们变得太小(由于移除或调整大小),它们就会被转换回普通bins。
在使用良好的用户hashcode的用法中,很少使用树箱。
理想情况下,在随机的hashcode中,箱子中节点的频率遵循泊松分布(http://en.wikipedia.org/wiki/Poisson_distribution),
默认大小调整阈值为0.75,但由于调整粒度的大小有很大的差异。
忽略方差,list的长度 k=(exp(-0.5) * pow(0.5, k) / factorial(k))
第一个值是:
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
树箱(tree bin很难翻译啊!)的根通常是它的第一个节点。
然而,有时(目前只在Iterator.remove)中,根可能在其他地方,但是可以通过父链接(方法TreeNode.root())恢复。
所有适用的内部方法都接受散列码作为参数(通常由公共方法提供),允许它们在不重新计算用户hashcode的情况下调用彼此。
大多数内部方法也接受一个“标签”参数,通常是当前表,但在调整或转换时可能是新的或旧的。
当bin列表被树化、分割或取消时( treeified, split, or untreeified),我们将它们保持在相同的相对存取/遍历顺序(例如
字段Node.next)为了更好地保存位置,并稍微简化对调用迭代器的拆分和traversals的处理(splits and traversals that invoke iterator.remove)。
当在插入中使用比较器时,为了在重新平衡中保持一个总排序(或者在这里需要的接近),我们将类和标识符码作为连接开关。
由于子类LinkedHashMap的存在,普通(plain)与树模型(tree modes)之间的使用和转换变得复杂起来。
请参阅下面的hook方法,这些方法在插入、删除和访问时被调用,允许LinkedHashMap内部结构保持独立于这些机制。
(这还要求将Map实例传递给一些可能创建新节点的实用方法。)
concurrent-programming-like SSA-based编码风格有助于避免在所有扭曲的指针操作中出现混叠错误。
*/
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加
Node简介
Node类是HashMap的一个静态内部类,实现了 Map.Entry<K,V>接口。在调用put方法创建一个新的键值对时,会调用newNode方法来创建Node对象
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;
}
}
hashMap中静态方法介绍
hash计算存放数据到数组中的下标
/**
*计算key.hashCode()并将散列的(XOR)较高的位散布到较低的位。因为该表使用2的幂次掩码,所以仅在当前掩码上方的位中发生变化的哈希集将始终发生冲突
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1.h >>> 16
h是hashcode。 >>>是无符号右移,h >>> 16是用来取出h的高16 具体展示如下
1. 0010 0100 1011 0011 1101 1111 1110 0001
2.
3. h >>> 16
4.
5. 0000 0000 0000 0000 0010 0100 1011 0011
2.为什么 h = key.hashCode()) 与 (h >>> 16) 异或
在1.7时获取数组下标的方法是indexFor(int h, int length)方法,1.8中用tab[(n - 1) & hash]代替,但二者原理相同。
static int indexFor(int h, int length) {
return h & (length-1);
}
&在 java 中做与运算,& 是所有的2进制位数“与”出的最终结果,“与”的规则是两者都为1时才得1,否则就得0
132 & 15 =4 ?
阿拉伯数字(十进制):132 二进制:10000100
阿拉伯数字(十进制):15 二进制:0000 1111(计算器转换应该是1111,因为两个二进制进行运算时,需要在位数少的前面补零-补码操作)
10000100 & 0000 1111 = 0100
由于和(length-1)运算,length 绝大多数情况小于2的16次方。所以始终是hashcode 的低16位(甚至更低)参与运算。。hashCode方法源码:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
}
所以这样高16位是用不到的,如何让高16也参与运算呢。所以才有hash(Object key)方法。让他的hashCode()和自己的高16位^运算。所以(h >>> 16)得到他的高16位与hashCode()进行^运算,为什么不用&和|,因为&和|都会使得结果偏向0或者1 ,并不是均匀的概念,所以用^,会让得到的下标更加散列。
comparableClassFor(Object x)方法解读
comparableClassFor(Object x)方法,当x的类型为X,且X直接实现了Comparable接口(比较类型必须为X类本身)时,返回x的运行时类型;否则返回null。
/**
* Returns x's Class if it is of the form "class C implements
* Comparable<C>", else null.
*/
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) { // 判断是否实现了Comparable接口
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
if ((c = x.getClass()) == String.class)
return c; // 如果是String类型,直接返回String.class
if ((ts = c.getGenericInterfaces()) != null) { // 判断是否有直接实现的接口
for (int i = 0; i < ts.length; ++i) { // 遍历直接实现的接口
if (((t = ts[i]) instanceof ParameterizedType) && // 该接口实现了泛型
((p = (ParameterizedType)t).getRawType() == // 获取接口不带参数部分的类型对象
Comparable.class) && // 该类型是Comparable
(as = p.getActualTypeArguments()) != null && // 获取泛型参数数组
as.length == 1 && as[0] == c) // 只有一个泛型参数,且该实现类型是该类型本身
return c; // 返回该类型
}
}
}
return null;
}
1.instanceof
insanceof可以理解为是某种类型的实例,无论是运行时类型,还是它的父类,它实现的接口,他父类实现的接口,甚至它父类的父类的父类实现的接口的父类的父类,总之,只要在继承链上有这个类型就可以了。x instanceof Comparable表示x类是否属于Comparable实现该接口的类或者子类或者在继承链上有这个类型。
2.getClass()
与instanceof相应对的是getClass()方法,无论该对象如何转型,getClass()返回的只会是它的运行时类型,也就是new一个对象时的类型。
3.getGenericInterfaces()
getGenericInterfaces()方法返回的是该对象的运行时类型“直接实现”的接口。
- 返回的一定是接口。
- 必然是该类型自己实现的接口,继承过来的不算。
4.ParameterizedType
ParameterizedType是Type接口的子接口,表示参数化的类型,即实现了泛型参数的类型。需要注意:
3. 如果直接用bean对象instanceof ParameterizedType,结果都是false。
4. Class对象不能instanceof ParameterizedType,编译会报错。
5. 只有用Type对象instanceof ParameterizedType才能得到想要的比较结果。可以这么理解:一个Bean类不会是ParameterizedType,只有代表这个Bean类的类型(Type)才可能是ParameterizedType。
6. 实现泛型参数,可以是给泛型传入了一个真实的类型,或者传入另一个新声明的泛型参数;只声明泛型而不实现,instanceof ParameterizedType为false。
5.getRawType()
getRawType()方法返回声明了这个类型的类或接口,也就是去掉了泛型参数部分的类型对象。
6.getActualTypeArguments()
与getRawType()相对应,getActualTypeArguments()以数组的形式返回泛型参数列表。
注意,这里返回的是实现该泛型时传入的参数
- 当传入的是真实类型时,打印的是全类名。
- 当传入的是另一个新声明的泛型参数时 ,打印的是代表该泛型参数的符号。
compareComparables方法解析
/**
* Returns k.compareTo(x) if x matches kc (k's screened comparable
* class), else 0.
* 如果x的类型是kc,返回k.compareTo(x)的比较结果
* 如果x为空,或者类型不是kc,返回0
*/
@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
tableSizeFor方法解析
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这个方法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数)。
HashMap重要方法解析
初始化方法
/**
* Constructs an empty {@code HashMap} 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;
this.threshold = tableSizeFor(initialCapacity);
}
/**
* Constructs an empty {@code HashMap} with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
* 自定义初始化容量和使用默认的加载因子
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty {@code HashMap} with the default initial capacity
* (16) and the default load factor (0.75).
* 使用默认的初始化容量和默认的加载因子
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Constructs a new {@code HashMap} with the same mappings as the
* specified {@code Map}. The {@code HashMap} is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified {@code 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) {//m的类型参数是? extends,所以只能使用泛型代码的出口,比如get函数
int s = m.size();
if (s > 0) {//前提是传入map的大小不为0,
if (table == null) { // 说明是拷贝构造函数来调用的putMapEntries,或者构造后还没放过任何元素
//先不考虑容量必须为2的幂,那么下面括号里会算出来一个容量,使得size刚好不大于阈值。
//但这样会算出小数来,但作为容量就必须向上取整,所以这里要加1
float ft = ((float)s / loadFactor) + 1.0F;
//如果小于最大容量,就进行截断;否则就赋值为最大容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//虽然上面一顿操作猛如虎,但只有在算出来的容量t > 当前暂存的容量(容量可能会暂放到阈值上的)时,才会用t计算出新容量,再暂时放到阈值上
if (t > threshold) threshold表示需要扩容的阈值
threshold = tableSizeFor(t);
}
//说明table已经初始化过了;判断传入map的size是否大于当前map的threshold,如果是,必须要resize
//这种情况属于预先扩大容量,再put元素
else if (s > threshold)
resize();
//循环里的putVal可能也会触发resize
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {//下面的Entry泛型类对象,只能使用get类型的函数
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
get()获取方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final HashMap.Node<K,V> getNode(int hash, Object key) {
//定义变量
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//查看数据需要满足一下条件
//1)数组不为空
//2)数组长度>0
//3)通过hash计算出该元素在数组中存放位置的索引,而且该索引处数据不为空null
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))))
//如果第一个就是我们要找的,直接返回即可
return first;
//如果第一个不是,我们需要循环遍历,然后找数据
if ((e = first.next) != null) {
//如果第1个的元素是红黑树类型的节点
if (first instanceof HashMap.TreeNode)
//那我们需要调用红黑树的方法查找节点
return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
//如果不是,则该为链表,需要遍历查找
do {
//循环判断下一个节点的hash和key是否相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
//更新e为下一个
} while ((e = e.next) != null);
}
}
//没找到返回Null
return null;
/**
* Calls find for root node.
* 在红黑树中获取值
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
//root()获取根节点
//find遍历树结构,查找对象
return ((parent != null) ? root() : this).find(h, k, null);
}
}
/**
* Finds the node starting at root p with the given hash and key.
* The kc argument caches comparableClassFor(key) upon first use
* comparing keys.
*/
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;
//根据hash值判断,p=左子节点,或右子节点
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
//p的key与之key对比,如果相同,则返回当前对象
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//如果左子节点为空,则p=右子节点
else if (pl == null)
p = pr;
//如果右子节点为空,则p=左子节点
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
//嵌套查询,如果找到,则返回该对象
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
//循环对象,直到找到,或者循环结束
} while (p != null);
return null;
}
put()获取方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 参数onlyIfAbsent表示是否替换原值 if true, don't change existing value 如果为true不会改变已经存在的值
// 参数evict我们可以忽略它,它主要用来区别通过put添加还是创建时初始化数据的
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 空表,需要初始化
if ((tab = table) == null || (n = tab.length) == 0)
// resize()不仅用来调整大小,还用来进行初始化配置
n = (tab = resize()).length;
// (n - 1) & hash 计算出数组下标
//这里就是看下在hash位置有没有元素,实际位置是hash % (length-1)
if ((p = tab[i = (n - 1) & hash]) == null)
// 将元素直接插进去
tab[i] = newNode(hash, key, value, null);
else {
//这时就需要链表或红黑树了
// e是用来查看是不是待插入的元素已经有了,有就替换
Node<K,V> e; K k;
// p是存储在当前位置的元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //要插入的元素就是p,这说明目的是修改值
// p是一个树节点 红黑树
else if (p instanceof TreeNode)
// 把节点添加到树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 这时候就是链表结构了,要把待插入元素挂在链尾 1.7 是头插法 1.8以后都是尾插法
for (int binCount = 0; ; ++binCount) {
//向后循环 一直找到链表最后的位置
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表比较长,需要树化,
// 由于初始即为p.next,所以当插入第8个元素才会树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 找到了对应元素,就可以停止了 这说明目的是修改值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 继续向后
p = e;
}
}
// e就是被替换出来的元素,这时候就是修改元素值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 默认为空实现,允许我们修改完成后做一些操作
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// size太大,达到了capacity的0.75,需要扩容
if (++size > threshold)
resize();
// 默认也是空实现,允许我们插入完成后做一些操作
afterNodeInsertion(evict);
return null;
}
put方法中putTreeVal源码分析
/**
* 当存在hash碰撞的时候,且元素数量大于8个时候,就会以红黑树的方式将这些元素组织起来
* map 当前节点所在的HashMap对象
* tab 当前HashMap对象的元素数组
* h 指定key的hash值
* k 指定key
* v 指定key上要写入的值
* 返回:指定key所匹配到的节点对象,针对这个对象去修改V(返回空说明创建了一个新节点)
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null; // 定义k的Class对象
boolean searched = false; // 标识是否已经遍历过一次树,未必是从根节点遍历的,但是遍历路径上一定已经包含了后续需要比对的所有节点。
TreeNode<K,V> root = (parent != null) ? root() : this; // 父节点不为空那么查找根节点,为空那么自身就是根节点
for (TreeNode<K,V> p = root;;) { // 从根节点开始遍历,没有终止条件,只能从内部退出
int dir, ph; K pk; // 声明方向、当前节点hash值、当前节点的键对象
if ((ph = p.hash) > h) // 如果当前节点hash 大于 指定key的hash值
dir = -1; // 要添加的元素应该放置在当前节点的左侧
else if (ph < h) // 如果当前节点hash 小于 指定key的hash值
dir = 1; // 要添加的元素应该放置在当前节点的右侧
else if ((pk = p.key) == k || (k != null && k.equals(pk))) // 如果当前节点的键对象 和 指定key对象相同
return p; // 那么就返回当前节点对象,在外层方法会对v进行覆盖写入
// 走到这一步说明 当前节点的 hash值 和 指定key的hash值 是相等的,但是equals不等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// 走到这里说明:指定key没有实现comparable接口 或者 实现了comparable接口并且和当前节点的键对象比较之后相等(仅限第一次循环)
/*
* searched 标识是否已经对比过当前节点的左右子节点了
* 如果还没有遍历过,那么就递归遍历对比,看是否能够得到那个键对象equals相等的的节点
* 如果得到了键的equals相等的的节点就返回
* 如果还是没有键的equals相等的节点,那说明应该创建一个新节点了
*/
if (!searched) { // 如果还没有比对过当前节点的所有子节点
TreeNode<K,V> q, ch; // 定义要返回的节点、和子节点
searched = true; // 标识已经遍历过一次了
/*
* 红黑树也是二叉树,所以只要沿着左右两侧遍历寻找就可以了
* 这是个短路运算,如果先从左侧就已经找到了,右侧就不需要遍历了
* find 方法内部还会有递归调用。参见:find方法解析 在get方法讲解中包含
*/
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; // 找到了指定key键对应的
}
// 走到这里就说明,遍历了所有子节点也没有找到和当前键equals相等的节点
dir = tieBreakOrder(k, pk); // 再比较一下当前节点键和指定key键的大小
}
TreeNode<K,V> xp = p; // 定义xp指向当前节点
/*
* 如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就可以把要添加的元素作为当前节点的左节点,如果不为空,还需要下一轮继续比较
* 如果dir大于等于0,那么看当前节点的右节点是否为空,如果为空,就可以把要添加的元素作为当前节点的右节点,如果不为空,还需要下一轮继续比较
* 如果以上两条当中有一个子节点不为空,这个if中还做了一件事,那就是把p已经指向了对应的不为空的子节点,开始下一轮的比较
*/
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 如果恰好要添加的方向上的子节点为空,此时节点p已经指向了这个空的子节点
Node<K,V> xpn = xp.next; // 获取当前节点的next节点
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); // 创建一个新的树节点
if (dir <= 0)
xp.left = x; // 左孩子指向到这个新的树节点
else
xp.right = x; // 右孩子指向到这个新的树节点
xp.next = x; // 链表中的next节点指向到这个新的树节点
x.parent = x.prev = xp; // 这个新的树节点的父节点、前节点均设置为 当前的树节点
if (xpn != null) // 如果原来的next节点不为空
((TreeNode<K,V>)xpn).prev = x; // 那么原来的next节点的前节点指向到新的树节点
moveRootToFront(tab, balanceInsertion(root, x));// 重新平衡,以及新的根节点置顶
return null; // 返回空,意味着产生了一个新节点
}
}
}
/**
* 红黑树插入节点后,需要重新平衡
* root 当前根节点
* x 新插入的节点
* 返回重新平衡后的根节点
*/
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
x.red = true; // 新插入的节点标为红色
/*
* 这一步即定义了变量,又开起了循环,循环没有控制条件,只能从内部跳出
* xp:当前节点的父节点、xpp:爷爷节点、xppl:左叔叔节点、xppr:右叔叔节点
*/
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// 如果父节点为空、说明当前节点就是根节点,那么把当前节点标为黑色,返回当前节点
if ((xp = x.parent) == null) { // L1
x.red = false;
return x;
}
// 父节点不为空
// 如果父节点为黑色 或者 【(父节点为红色 但是 爷爷节点为空) -> 这种情况何时出现?】
else if (!xp.red || (xpp = xp.parent) == null) // L2
return root;
if (xp == (xppl = xpp.left)) { // 如果父节点是爷爷节点的左孩子 // L3
if ((xppr = xpp.right) != null && xppr.red) { // 如果右叔叔不为空 并且 为红色 // L3_1
xppr.red = false; // 右叔叔置为黑色
xp.red = false; // 父节点置为黑色
xpp.red = true; // 爷爷节点置为红色
x = xpp; // 运行到这里之后,就又会进行下一轮的循环了,将爷爷节点当做处理的起始节点
}
else { // 如果右叔叔为空 或者 为黑色 // L3_2
if (x == xp.right) { // 如果当前节点是父节点的右孩子 // L3_2_1
root = rotateLeft(root, x = xp); // 父节点左旋,见下文左旋方法解析
xpp = (xp = x.parent) == null ? null : xp.parent; // 获取爷爷节点
}
if (xp != null) { // 如果父节点不为空 // L3_2_2
xp.red = false; // 父节点 置为黑色
if (xpp != null) { // 爷爷节点不为空
xpp.red = true; // 爷爷节点置为 红色
root = rotateRight(root, xpp); //爷爷节点右旋,见下文右旋方法解析
}
}
}
}
else { // 如果父节点是爷爷节点的右孩子 // L4
if (xppl != null && xppl.red) { // 如果左叔叔是红色 // L4_1
xppl.red = false; // 左叔叔置为 黑色
xp.red = false; // 父节点置为黑色
xpp.red = true; // 爷爷置为红色
x = xpp; // 运行到这里之后,就又会进行下一轮的循环了,将爷爷节点当做处理的起始节点
}
else { // 如果左叔叔为空或者是黑色 // L4_2
if (x == xp.left) { // 如果当前节点是个左孩子 // L4_2_1
root = rotateRight(root, x = xp); // 针对父节点做右旋,见下文右旋方法解析
xpp = (xp = x.parent) == null ? null : xp.parent; // 获取爷爷节点
}
if (xp != null) { // 如果父节点不为空 // L4_2_4
xp.red = false; // 父节点置为黑色
if (xpp != null) { //如果爷爷节点不为空
xpp.red = true; // 爷爷节点置为红色
root = rotateLeft(root, xpp); // 针对爷爷节点做左旋
}
}
}
}
}
}
/**
* 节点左旋
* root 根节点
* 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
rl.parent = p; // 设置rl和要左旋的节点的父子关系【之前只是爹认了孩子,孩子还没有答应,这一步孩子也认了爹】
// 将要左旋的节点的右孩子的父节点 指向 要左旋的节点的父节点,相当于右孩子提升了一层,
// 此时如果父节点为空, 说明r 已经是顶层节点了,应该作为root 并且标为黑色
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p) // 如果父节点不为空 并且 要左旋的节点是个左孩子
pp.left = r; // 设置r和父节点的父子关系【之前只是孩子认了爹,爹还没有答应,这一步爹也认了孩子】
else // 要左旋的节点是个右孩子
pp.right = r;
r.left = p; // 要左旋的节点 作为 他的右孩子的左节点
p.parent = r; // 要左旋的节点的右孩子 作为 他的父节点
}
return root; // 返回根节点
}
/**
* 节点右旋
* root 根节点
* 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
lr.parent = p; // 设置lr和要右旋的节点的父子关系【之前只是爹认了孩子,孩子还没有答应,这一步孩子也认了爹】
// 将要右旋的节点的左孩子的父节点 指向 要右旋的节点的父节点,相当于左孩子提升了一层,
// 此时如果父节点为空, 说明l 已经是顶层节点了,应该作为root 并且标为黑色
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p) // 如果父节点不为空 并且 要右旋的节点是个右孩子
pp.right = l; // 设置l和父节点的父子关系【之前只是孩子认了爹,爹还没有答应,这一步爹也认了孩子】
else // 要右旋的节点是个左孩子
pp.left = l; // 同上
l.right = p; // 要右旋的节点 作为 他左孩子的右节点
p.parent = l; // 要右旋的节点的父节点 指向 他的左孩子
}
return root;
}
/**
* 把红黑树的根节点设为 其所在的数组槽 的第一个元素
* 首先明确:TreeNode既是一个红黑树结构,也是一个双链表结构
* 这个方法里做的事情,就是保证树的根节点一定也要成为链表的首节点
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) { // 根节点不为空 并且 HashMap的元素数组不为空
int index = (n - 1) & root.hash; // 根据根节点的Hash值 和 HashMap的元素数组长度 取得根节点在数组中的位置
TreeNode<K,V> first = (TreeNode<K,V>)tab[index]; // 首先取得该位置上的第一个节点对象
if (root != first) { // 如果该节点对象 与 根节点对象 不同
Node<K,V> rn; // 定义根节点的后一个节点
tab[index] = root; // 把元素数组index位置的元素替换为根节点对象
TreeNode<K,V> rp = root.prev; // 获取根节点对象的前一个节点
if ((rn = root.next) != null) // 如果后节点不为空
((TreeNode<K,V>)rn).prev = rp; // root后节点的前节点 指向到 root的前节点,相当于把root从链表中摘除
if (rp != null) // 如果root的前节点不为空
rp.next = rn; // root前节点的后节点 指向到 root的后节点
if (first != null) // 如果数组该位置上原来的元素不为空
first.prev = root; // 这个原有的元素的 前节点 指向到 root,相当于root目前位于链表的首位
root.next = first; // 原来的第一个节点现在作为root的下一个节点,变成了第二个节点
root.prev = null; // 首节点没有前节点
}
/*
* 这一步是防御性的编程
* 校验TreeNode对象是否满足红黑树和双链表的特性
* 如果这个方法校验不通过:可能是因为用户编程失误,破坏了结构(例如:并发场景下);也可能是TreeNode的实现有问题(这个是理论上的以防万一);
*/
assert checkInvariants(root);
}
}
put方法中resize源码分析
final Node<K,V>[] resize() {
//数组扩容是新建一个长度为原数组长度两倍的数组,再将旧数组的内容移植到新数组中,以此完成扩容操作
//我们先定义一个旧数组的变量来接受HashMap内置table对象
Node<K,V>[] oldTab = table;
//定义旧容量获取旧数组容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//定义旧阈值来获取旧数组阈值
int oldThr = threshold;
//定义新容量和新阈值
int newCap, newThr = 0;
//如果旧容量大于0的情况下
//注:这说明之前已经存在一个table数组了,我们对它进行的是单纯的扩容操作而无需新键一个table
if (oldCap > 0) {
//判断:如果说旧容量已经大于等于HashMap本身所能允许的最大容量时
//这个时候我们已经不能为它进行扩容操作了,因为已经扩无可扩了
if (oldCap >= MAXIMUM_CAPACITY) {
//那这个时候怎么办呢,由我在文章开头申明的第二点,我们可以将阈值给扩大,
//这样在不改变数组容量的情况下,我们依然可以放进更多的键值对
threshold = Integer.MAX_VALUE;
//扩无可扩那就不需要进行后面的操作了,直接把该数组返回即可
return oldTab;
}
//判断:如果我们的旧数组容量大于hashMap的初始容量时,且我们将旧数组容量的大小乘2后赋给新容量
//这里我们知道:扩容是将数组容量*2的操作
//如果新数组容量的范围在HashMap中的最大容量范围之内
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//那我们就直接将阈值扩大两倍
//冷知识:阈值=容量*负载因子
//所以这一步和newThr = newCap*DEFAULT_LOAD_FACTOR是等价的
newThr = oldThr << 1; // double threshold
}
//如果阈值大于0的情况下
//注意:此时数组容量是不大与0的,那什么情况下会出现数组容量不大于0而阈值大于0的情况呢
//HashMap有两个带参构造器,可以指定初始容量,
// 若你调用了这两个可以指定初始容量的构造器,
// 这两个构造器就会将阈值记录为第一个大于等于你指定容量,且满足2^n的数(可以看看这两个构造器)
else if (oldThr > 0) // initial capacity was placed in threshold
//那这个时候有啥办法呢,没容量有阈值,我看了一下构造器的代码,貌似这个阈值定义出来的话值也会是2^n,
//那我们可以直接把旧阈值赋给新容量。后续在通过新容量来获得新阈值不就可以了吗
newCap = oldThr;
//这个时候我们的容量和阈值都不大于0了
//那就说明我们是使用默认构造器创建的HashMap了,此时我们还没有初始化table数组。
else { // zero initial threshold signifies using defaults
//新容量等于初始化容量,新阈值等于初始阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新阈值等于0,只可能出现在上述第二种判断当中,别忘了新阈值我们还没有进行赋值呢
if (newThr == 0) {
//一系列的取值赋值语句,不做过多解释
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//经过上面的操作,新阈值一定有值啦,将它赋给全局变量threshould方便其他操作
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建一个以新容量大小为长度的新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//全局变量table接受
table = newTab;
//下面的操作是一个将旧数组中的数据移植到新数组的操作。
//有数据才移植嘛!没数据还移啥
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
//把数组中第一个位置的元素拿出来
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//属于一个位置一个位置抽取的过程
oldTab[j] = null;
//如果它的next节点为空说明这个位置只有一个节点
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
//定义四个指针:低位头、低位尾
// 高位头、高位尾
//低位:0~旧容量长
//高位:旧容量长~新容量长
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
//其实这句话我觉得写在下面会更好理解
//这个主要是为了让指向链表的指针下移,以读取整个链表
next = e.next;
//我们知道让数据的哈希值和数组容量-1做与操作是为了定位,大家其实都有知道了
//但是这里直接和数组容量做与操作什么意思呢
//比如一个数组容量为8,那二进制为:1000
//那么8-1,它的二进制为: 0111
//看出来没有,最高位不同
//而现在我们要对数组扩容,在新的数组上,我们要把原有的数据区分开来,我们都知道新数组容量是原数组容量的2倍
//那么只要利用起这个最高位的不同,就能恰好将原有数据区分起来
//所以我们只要对它进行与操作就可以啦
//例:1 &7 = 1,9 & 7 = 1
// 1 & 8 = 0,9 & 8 = 1
//这其实就是一个区分高低位的过程
//为0则是低位
if ((e.hash & oldCap) == 0) {
//如果低位尾为空,说明这是第一个节点
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//为1则是高位
//以下同理
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//指针指向链表下一个数据,对它进行操作
} while ((e = next) != null);
//所有操作进行完以后,检查以下低位尾是不是不为空
//因为这里可能会出现只有高位或地位有数据的情况
if (loTail != null) {
//不为空则将它的尾巴置空
loTail.next = null;
//把它的头放入新table对应位置上,则该位置的新链表操作就完成了。
newTab[j] = loHead;
}
//高位同理
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//把新table返回,大功告成
return newTab;
}
put方法中treeifyBin源码分析
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//1,如果table数组为空,或者大小未超过64,则重置table大小
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//2,如果table大小超过64,把当前链表转换成红黑树
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//2.1,循环链表,把每一个对象转换成红黑树,并绑定上下级关系
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);
//2.2 重置红黑树相关信息
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}