ConcurrentHashMap是一个支持检索操作完全并发、更新操作高并发的哈希表。该类遵循与 java.util.Hashtable 相同的功能规范,并包含与

每个方法对应的版本。不过,尽管所有操作都是线程安全的,检索操作不需要加锁,也不支持锁定整个表以阻止所有访问。此类可与依赖其线程安全但不依赖其同步细节的程序中的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{@code Hashtable} 完全互操作。

### get
这次先从get方法开始看起,get是读操作,读操作是无锁的。
```java
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code key.equals(k)},
* then this method returns {@code v}; otherwise it returns
* {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not necessarily
* indicate that the map contains no mapping for the key;
* it's also possible that the map explicitly maps the key to
* {@code null}. The {@link #containsKey containsKey} method
* may be used to distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
// 1. 计算hash值,并定位到桶数组中的位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 2. 遍历链表,查找key对应的节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}

接下来看一看它和HashMap在内部实现上的一些区别。

spread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Spreads (XORs) higher bits of hash to lower and also forces top
* bit to 0. 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.
*
* 将哈希值的高位通过异或运算(XOR)传播到低位,并强制最高位为0。
* 由于哈希表的容量总是2的幂次方,只有低位参与桶索引的计算,
* 如果哈希值只在高位不同,则会发生碰撞。通过这种方式将高位信息混合到低位,
* 可以减少碰撞,提高哈希分布的均匀性。
* 这种做法在速度、实用性和位扩散质量之间做了权衡。
* 由于很多哈希集合本身分布较好,且对于大量碰撞会用红黑树处理,
* 所以这里只做了简单的异或操作,既能降低碰撞,也能让高位信息参与索引计算。
*/
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}

前面运算这里不解释,跟hashMap一样,主要看后面的 & HASH_BITS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
作用是消除负数。
为什么要消除负数呢?因为Java中的不存在无符号整数,int是有符号的,最高位是符号位,1是负数,0是正数。
正数的原码、反码、补码都是一样的。
负数的原码:是对应的正数的二进制,但是符号位是1。
符号位是区分正负数用的,1是负数,0是整数(符号位就是最高位的意思,左边第一位)
负数的反码:是除符号位,其余的取反
负数的补码:是反码+1
并且,在计算机系统中,数值一律用补码来存储。这是因为补码在处理加减运算和表示零时具有显著的优势。
举个例子:
-5的原码是:10000000000000000000000000000101
反码是:11111111111111111111111111111010
补码是:11111111111111111111111111111011
```HASH_BITS```换算下来的二进制是:```011111111111111111111111111111111```
而与运算的运算规则就是对两个二进制数的每一位进行比较。只有当两个对应位都为1时,结果才为1,否则为0。
因此```hash & HASH_BITS```,不管hash是正还是负,结果都会转成正数,因为```HASH_BITS```的符号位是0,& 下来的最高位肯定是0,也就是正数。
spread(int h) 的作用其实就是避免hash值是负数,大概是因为```ConcurrentHashMap```内置了```MOVED```、```TREEBIN```、```RESERVED```
这3个hash是负数,为了避免冲突吧。

#### tabAt
```java
/*
* Atomic access methods are used for table elements as well as
* elements of in-progress next table while resizing. All uses of
* the tab arguments must be null checked by callers. All callers
* also paranoically precheck that tab's length is not zero (or an
* equivalent check), thus ensuring that any index argument taking
* the form of a hash value anded with (length - 1) is a valid
* index. Note that, to be correct wrt arbitrary concurrency
* errors by users, these checks must operate on local variables,
* which accounts for some odd-looking inline assignments below.
* Note that calls to setTabAt always occur within locked regions,
* and so require only release ordering.
*
* 原子访问方法用于table元素以及扩容时的next table元素。
* 所有tab参数的使用都需要调用方进行null检查。
* 调用方还会偏执地预先检查tab的长度不为0,确保(hash & (length-1))形式的索引是有效的。
* 这些检查必须基于局部变量,以防止并发错误。
* setTabAt方法的调用总是在加锁区域内,因此只需要release语义。
*/
@SuppressWarnings("unchecked")
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
// 通过Unsafe的getReferenceAcquire方法原子性地获取tab数组指定位置的元素
return (Node<K,V>)U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE);
}

这段代码的作用是原子性地读取哈希表(table)指定槽位(bin)上的节点,用于并发场景下安全地获取 ConcurrentHashMap 的桶元素。
tabAt 方法通过 Unsafe.getReferenceAcquire,以原子方式读取数组 tab 下标 i 处的元素,保证可见性和并发安全。
主要用于高并发下的哈希表读操作,比如 get、put、resize 等,避免数据竞争。
((long)i << ASHIFT) + ABASE 计算出数组元素的真实内存偏移量。

find

在 public V get(Object key) 方法中,eh < 0 表示当前桶的第一个节点(e)的 hash 字段是负数。这种情况有三种特殊节点:

  1. MOVED(-1):表示该桶是一个转发节点(ForwardingNode),说明哈希表正在扩容,当前桶的数据已经迁移到新的表,需要在新表中继续查找。
  2. TREEBIN(-2):表示该桶是树形结构的根节点(TreeBin),需要在红黑树中查找目标 key。
  3. RESERVED(-3):表示该节点是占位节点(ReservationNode),用于并发计算时的占位。
    因此,eh < 0 时,get 方法会通过 e.find(h, key) 进入特殊的查找逻辑(如树查找或迁移查找),而不是普通链表查找。

put

put内部依然是putVal,这是高并发下安全插入/更新元素的主流程,兼顾性能和线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}

主要作用和流程如下:

  1. 参数校验:key 或 value 为 null 时抛出异常。
  2. 计算 hash:对 key 的 hashCode 做扰动处理并去除负数,减少哈希冲突。
  3. 定位桶:通过 (n - 1) & hash 计算桶下标。
  4. 初始化表:如果 table 未初始化或长度为 0,则初始化。
  5. 无锁插入:如果目标桶为空,CAS 方式直接插入新节点,无需加锁。
  6. 扩容迁移:如果桶头节点 hash 为 MOVED,说明正在扩容,协助迁移。
  7. 只插入不存在的 key:如果 onlyIfAbsent 且 key 已存在,直接返回旧值。
  8. 加锁插入/更新:否则对桶头节点加锁,链表或树结构中查找/插入/更新节点。
    • 链表:遍历查找,存在则更新,不存在则尾插新节点。
    • 树:委托 TreeBin 处理。
    • ReservationNode:递归更新异常。
  9. 链表转树:如果桶内节点数超过阈值,链表转红黑树。
  10. 计数:插入成功后更新元素计数。
  11. 返回:返回旧值或 null。

成员变量

HashMap中基本的成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/* ---------------- Fields -------------- */

/**
* 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,以支持目前不需要的引导机制。)
*/
transient Node<K,V>[] table;

/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*
* 缓存entrySet(),注意AbstractMap的字段用于keySet()和values()。
*/
transient Set<Map.Entry<K,V>> entrySet;

/**
* The number of key-value mappings contained in this map.
*
* 当前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).
*
* HashMap结构性修改的次数。结构性修改指改变映射数量或内部结构(如rehash)。
* 该字段用于让Collection视图的迭代器快速失败(fail-fast)。
* (参见ConcurrentModificationException)
*/
transient int modCount;

/**
* The next size value at which to resize (capacity * load factor).
*
* 每次需要扩容的阈值(容量*负载因子)。
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
// (Javadoc描述在序列化时有效。如果table数组未分配,该字段保存初始容量,0表示DEFAULT_INITIAL_CAPACITY。)
int threshold;

/**
* The load factor for the hash table.
*
* 哈希表的负载因子。
*
* @serial
*/
final float loadFactor;

构造器

无参构造器

这个也是我们最常用的一个构造器,仅仅指定了负载因子,其他的字段都使用默认值。

1
2
3
4
5
6
7
/**
* 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
}

而负载因子是0.75,这个值在时间和空间的权衡之下的一个不错的选择。

1
2
3
4
5
6

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

公共方法

put

put自身的功能很简单,就是插入或更新一个键值对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* 将指定的值与此映射中的指定键关联。
* 如果该映射先前包含该键的映射,则旧值将被替换。
*
* @param key key with which the specified value is to be associated
* 要关联指定值的键
* @param value value to be associated with the specified key
* 要与指定键关联的值
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}.
* (A {@code null} return can also indicate that the map
* previously associated {@code null} with {@code key}.)
* 返回与key关联的前一个值,如果没有映射则返回null。
* (返回null也可能表示该映射之前将null与key关联。)
*/
public V put(K key, V value) {
// 调用putVal方法,插入或更新键值对
return putVal(hash(key), key, value, false, true);
}

代码很简单,通过hash(key)方法计算哈希值,然后调用putVal方法插入或更新键值对。

hash

而hash函数是怎么实现的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 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.
*
* 计算key的hashCode,并将高位的hash通过异或运算混合到低位。
* 因为哈希表的长度总是2的幂,只有低位参与寻址,如果高位不同而低位相同会导致哈希冲突。
* 通过将高位与低位异或,可以让高位的信息参与寻址,减少哈希冲突。
*/
static final int hash(Object key) {
int h;
// 如果key为null,返回0,否则返回hashCode与其高16位右移后异或的结果
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在hashmap不是null键的情况下,调用键对象的hashCode()方法获取哈希值,然后将该哈希值与其高16位右移后的值进行异或运算,得到最终的哈希值。
这样做的目的是为了进行一个hash扰动的过程,将高位的信息也混合到低位中,从而减少哈希冲突。

为什么这么做?主要是考虑了hash寻址的一个实现方式:

  • HashMap 通过如下方式计算数组的索引:
1
int index = (table.length - 1) & hash;

这里 & 是按位与运算,参加运算的两个数,按二进制位进行“与”运算。
运算规则:只有两个数的二进制同时为1,结果才为1,否则为0。(负数按补码形式参加按位与运算)
翻译一下来说,就是在当 table.length 是 2 的幂时,这个算法等价于取模运算,但效率更高,因为位运算比除法快。
但这也会带来一个问题在通常使用场景下,table数量有限的情况下,table.length-1 的二进制表示为低位全是 1。
例如,table.length=16(二进制 10000),n-1=15(二进制 01111)。 在table.length=16 时,只有 hash 的最低 4 位参与索引计算。
这样,按位与实现(table.length - 1) & hash 实际上就是取 hash 的低 log₂(table.length) 位作为哈希表的索引。也就是只保留
hash 的低位部分作为索引, 高位被舍弃。因此,只有哈希值的低位部分决定元素放在哈希表的哪个位置。

这样的话,即使 hashCode 的高位分布再好,但低位分布不好,仍然会导致哈希冲突。
所以 HashMap 还会对 hashCode 做扰动(让高16位与低16位异或),让高位的信息也混合到低位,提升分布均匀性。

具体的实现细节在putVal方法中,我们看看putVal的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* Implements Map.put and related methods.
* 实现了Map.put及相关方法。
*
* @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,不覆盖已有值
* @param evict if false, the table is in creation mode.
* 如果为false,表示表处于创建模式
* @return previous value, or null if none
* 返回旧值,如果没有则返回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为空或长度为0,则进行初始化或扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算哈希表下标,如果该位置为空,直接插入新节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

尝试逐行解析一下,一开始的四个环境变量,通读整段代码,可以知道分别代表了

  1. tab - 存放hashmap所有元素的哈希表
  2. n - 哈希表的长度

resize

因此,第一次添加元素肯定会走resize ,

整个代码很长,精简一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* 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.
*
* 初始化或扩容哈希表(容量加倍)。如果table为null,则根据threshold字段分配初始容量。
* 否则,由于采用2的幂次扩容,原哈希表中的元素要么保持原索引,要么以2的幂为偏移移动到新表中。
*
* @return the table
* 返回哈希表数组
*/
final Node<K,V>[] resize() {
// 记录原来的哈希表、哈希表容量与阈值
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
// 初始化新的容量与阈值
int newCap, newThr = 0;
// 从这里开始就是关于容量的核心计算方式
// 情况一:容量大于0,哈希表数组已经初始化,进行正常扩容
if (oldCap > 0) {
// 情况一:容量大于0,哈希表数组已经初始化,进行正常扩容
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果已达最大容量2^30,阈值设为最大,直接返回,不进行容量调整
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 情况二:容量在默认值16和最大值2^30之间,正常扩容,让容量翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 情况二:数组未初始化,但构造时指定了初始容量,这种情况下,初始容量值被保存在 threshold 变量里
newCap = oldThr; // initial capacity was placed in threshold
else { // 情况三:数组未初始化,且构造时未指定容量(调用 new HashMap())
newCap = DEFAULT_INITIAL_CAPACITY; // 默认初始容量
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 默认阈值
}
// 处理情况二中的 newThr 未被赋值的问题
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE); // 计算新阈值
}
threshold = newThr; // 更新阈值
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 新建扩容后的哈希表
table = newTab; // 更新存储哈希表
// 原数据不为空,进行数据迁移逻辑
if (oldTab != null) {
// 遍历旧的哈希表数组的每一个桶
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 置空旧哈希表的该桶,帮助GC
oldTab[j] = null;
if (e.next == null)
// 情况A: 桶中只有一个节点,直接迁移
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 情况B: 桶中是红黑树节点,调用红黑树的split方法进行迁移
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 情况 C: 桶中是链表(最核心的优化点)
// 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;
}

有个疑问就是int oldThr = threshold;中threshold一开始就是4了

这部分代码比较复杂,整体分为两部分,扩容和数据迁移,
扩容主要处理了三种情况:

  1. 情况一(oldCap > 0): 这是最常见的扩容场景,数组已经存在,直接将容量(newCap)和阈值(newThr)都扩大为原来的 2 倍
  2. 情况二(oldCap == 0 && oldThr > 0): 数组还未初始化,但构造函数中指定了初始容量,这时直接使用该初始容量作为新的容量
  3. 情况三(oldCap == 0 && oldThr == 0): 数组未初始化,且构造函数中也未指定初始容量,这时使用默认的初始容量(16)和默认的负载因子(0.75)计算
    阈值(newThr)。
    数据迁移部分主要处理了三种情况:
  4. 情况A(桶中只有一个节点): 直接将该节点迁移,因为无链表,也能直接确定元素存放位置,
    之所以是e.hash & (newCap - 1),也是一个优化点,避免了取模运算,通过位运算提高效率,并获得了与e.hash % newCap 相同的效果。
    具体来说,假设旧容量是16(2^4),新容量是32(2^5),那么对于一个节点e:
  • 旧索引计算:旧容量 - 1 = 15(二进制 01111),索引 = e.hash & 15
  • 新索引计算:新容量 - 1 = 31(二进制 11111),索引 = e.hash & 31
    由于新容量是旧容量的两倍,索引计算时多了一位,因此节点要么保持原索引不变(如果该位是0),
    要么索引增加旧容量(如果该位是1)。这就是为什么在链表迁移时会用到 e.hash & oldCap 来判断节点的新位置。
  1. 情况B(桶中是红黑树节点): 调用红黑树的 split 方法进行迁移
  2. 情况C(桶中是链表): 由于容量是 2 的幂次方,扩容后,一个桶中的元素只可能去往两个位置:要么留在原索引位置,要么移动到
    原索引 + 老容量 的位置,通过位运算将链表节点分为两部分,一部分索引不变,另一部分索引加上旧容量(oldCap),然后分别插入到新表中,保持链表顺序。

核心优化点在于通过(e.hash & oldCap) == 0 决定元素去向。
oldCap 是旧数组的容量,它一定是 2 的幂(例如 16,二进制为 10000),e.hash & oldCap 这个位运算,实际上是在检查 e.hash 在 oldCap
的最高位上是 0 还是 1
如果结果为 0,说明该元素的哈希值在该位上是 0扩容后,它的新索引与旧索引完全相同这些元素被串在一起,形成低位链表(lo-list)
如果结果不为 0,说明该元素的哈希值在该位上是 1扩容后,它的新索引就是 旧索引 + oldCap这些元素被串在一起,形成高位链表(hi-list)
通过一次遍历,源码就巧妙地将一个长链表拆分成了两个子链表(loHead 和 hiHead),然后将这两个子链表分别放置到新数组的对应位置,极大地提高了扩容效率。

putVal

回到putVal方法中,这样前面就很清晰了。哈希表不存在,resize方法初始化哈希表,resize完成后,n就有值了,然后计算索引i,如果该位置为空,直接插入新节点。

1
2
3
4
5
6
7
8
9
10
11
12
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 已经有值,存在哈希冲突
// 下面分析
...
}

而该位置不为空的话,那就是发生了哈希冲突,需要处理冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}

如果该位置不为空,说明发生了哈希冲突,需要处理冲突:

  1. 如果该位置的节点hash值与新节点相同,并且key也相同,说明是更新操作,直接更新值
  2. 如果该位置的节点是红黑树节点,调用红黑树的插入方法
  3. 否则,说明是链表节点,遍历链表:
    • 如果找到相同hash和key的节点,说明是更新操作,直接更新值
    • 如果遍历到链表末尾,插入新节点
    • 如果链表长度超过阈值,调用treeifyBin方法,将链表转换为红黑树
      处理完hash冲突,剩下的就是几行代码了:
1
2
3
4
5
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;

最后,更新modCount和size,如果size超过阈值,调用resize进行扩容,以及提供了一个afterNodeInsertion的回调。用来给子类LinkedHashMap使用的
afterNodeInsertion
,方便LinkedHashMap中被覆盖的afterNodeInsertion方法,用来回调移除最早放入Map的对象。可以看到源码中留有对应的钩子:

1
2
3
4
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

而如何判断链表是否需要树化成红黑树呢,是通过binCount >= TREEIFY_THRESHOLD - 1来确定的。
简单来说,如果当前桶(bin)中的节点数量已经达到或超过 TREEIFY_THRESHOLD - 1,则在插入新节点后,桶中的节点数将达到或超过
TREEIFY_THRESHOLD,此时需要将链表结构转换为红黑树结构(treeify)。

  • 1 是因为计数是从0开始的,插入新节点后才会达到阈值。例如,TREEIFY_THRESHOLD 默认是8,当已有7个节点(binCount ==
    7)时,再插入一个节点就达到8,需要树化。

treeify

关于树化,是通过以下代码实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);

TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}

具体流程如下:

  1. 遍历以当前节点为头的链表,将每个节点插入到红黑树中。
  2. 第一个节点直接作为根节点,标记为黑色。
  3. 后续节点根据 hash 值、可比较性和类名等规则插入到红黑树的合适位置,并保持红黑树的平衡。
  4. 最后将红黑树的根节点放到哈希表的桶头。

get

同样的,get本身的逻辑很简单,是委托给内部的一个getNode方法实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*
* 返回指定key对应的value,如果没有该key的映射则返回null。
* 注意,返回null不一定表示没有该key的映射,也可能是该key被显式映射为null。
* 可以通过containsKey方法区分这两种情况。
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}

getNode

getNode的实现如下,很简单直接在关键源码处添加注释了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Implements Map.get and related methods.
*
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
// 哈希表不为空且长度大于0且计算哈希表下标后获取该位置的第一个节点也不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
// 检查第一个节点是否匹配
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 遍历链表或红黑树
if ((e = first.next) != null) {
if (first instanceof TreeNode)
// 如果是红黑树节点,调用红黑树的查找方法
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 否则遍历链表
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

成员变量和静态常量

不考虑从AbstractList中继承成的modCount之类的字段,ArrayList具有以下成员变量:

字段名 类型 说明
elementData Object[] 存储元素的数组缓冲区
size int 当前元素数量

包含静态常量的完整定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

@java.io.Serial
private static final long serialVersionUID = 8683452581122892189L;

/**
* Default initial capacity.
* 默认初始容量。
*/
private static final int DEFAULT_CAPACITY = 10;

/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
* 用于默认容量空实例的共享空数组实例。
* 我们将其与 EMPTY_ELEMENTDATA 区分开来,以便在添加第一个元素时知道应该扩容多少。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
* ArrayList用于存储元素的数组缓冲区。ArrayList的容量即为该数组的长度。
* 任何elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空ArrayList,
* 在添加第一个元素时会扩容到DEFAULT_CAPACITY。
*/
transient Object[] elementData; // non-private to simplify nested class access

/**
* The size of the ArrayList (the number of elements it contains).
* ArrayList的实际元素个数。
*
* @serial
*/
private int size;

serialVersionUID,DEFAULT_CAPACITY,elementData,size这四个属性整体中规中矩,一看就知道什么意思。
具有迷惑性的还是如何区分EMPTY_ELEMENTDATA```和```DEFAULTCAPACITY_EMPTY_ELEMENTDATA
简单来说,就是 DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA 虽然都是空数组,但用途不同。
前者用于默认构造的 ArrayList,更多的是标识此List是通过无参构造器构造的;后者用于容量为0的特殊情况。

构造器

ArrayList有三个构造器

传入一个初始容量的构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

代码整体比较简单,就三种情况。传入一个初始容量:

  • 如果大于0就创建一个指定大小的数组
  • 如果等于0就使用共享的表示空List的变量EMPTY_ELEMENTDATA
  • 如果小于0就抛出异常,因为数组的索引不能为零。

此时,ArrayList的初始size已经变为了对应传入大小。注意与下文无参构造器的对比。

无参构造器

1
2
3
4
5
6
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

无参构造器就是简单的讲数组元素赋值为默认的表示无参构造器构造的DEFAULTCAPACITY_EMPTY_ELEMENTDATA
注意,尽管源码上的注释都写着初始化一个初始容量为10的空数组,但其实此时ArrayList的size为0,
目的是为了保持延迟初始化。只有在添加第一个元素时才会扩容到默认的容量10。

Collection的构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}

通过传入的 Collection 创建一个新的 ArrayList,并按集合的迭代器顺序保存元素。
具体步骤很简单:

  1. 首先将集合 c 转为数组 a。
  2. 如果集合不为空,会判断集合的类型,如果本身就是 ArrayList 类型,直接修改引用指向复用其内部数组(提升效率)。
    否则,复制一份新数组,防止外部修改影响内部数据。
    如果集合为空,则使用共享的空数组 EMPTY_ELEMENTDATA。

此时,ArrayList若是不为空,则size已经调整为传入集合的大小。

具体用于复制数组的Arrays类的copyOf源码也很简单:

  • 如果 newLength 大于原数组长度,则新数组多余部分用 null 填充。
  • 如果 newLength 小于原数组长度,则只复制前 newLength 个元素。
  • 新数组的类型由 newType 决定,支持任意数组类型。
  • 对于 Object[] 类型,直接 new;否则用反射创建。
  • 用 System.arraycopy 进行高效复制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* Copies the specified array, truncating or padding with nulls (if necessary)
* so the copy has the specified length. For all indices that are
* valid in both the original array and the copy, the two arrays will
* contain identical values. For any indices that are valid in the
* copy but not the original, the copy will contain {@code null}.
* Such indices will exist if and only if the specified length
* is greater than that of the original array.
* The resulting array is of the class {@code newType}.
*
* 复制指定的数组,如有必要会截断或用 null 填充,使复制后的数组具有指定长度。
* 原数组和新数组中索引都有效的位置,值完全相同。
* 新数组中多出来的部分(如果有)会用 null 填充。
* 只有当 newLength 大于原数组长度时才会出现这种情况。
* 返回的新数组类型为 newType。
*
* @param <T> the class of the objects in the returned array
* 返回数组中对象的类型
* @param <U> the class of the objects in the original array
* 原数组中对象的类型
* @param original the array to be copied
* 要复制的原数组
* @param newLength the length of the copy to be returned
* 返回的新数组长度
* @param newType the class of the copy to be returned
* 返回的新数组类型
* @return a copy of the original array, truncated or padded with nulls
* to obtain the specified length
* 复制后的新数组,长度为 newLength
* @throws NegativeArraySizeException if {@code newLength} is negative
* 如果 newLength 为负数,抛出异常
* @throws NullPointerException if {@code original} is null
* 如果 original 为 null,抛出异常
* @throws ArrayStoreException if an element copied from
* {@code original} is not of a runtime type that can be stored in
* an array of class {@code newType}
* 如果原数组元素类型与新数组类型不兼容,抛出异常
* @since 1.6
*/
@IntrinsicCandidate
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
// 判断目标类型是否为 Object[],是则直接 new,否则用反射创建
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
// 复制原数组内容到新数组,长度为原数组和新数组长度的较小值
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

公共方法

trimToSize

该方法主要是将内部数组容量缩减为当前元素数量以释放多余内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Trims the capacity of this {@code ArrayList} instance to be the
* list's current size. An application can use this operation to minimize
* the storage of an {@code ArrayList} instance.
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}

  1. 首先递增来自于父类的 modCount 来标记结构性修改(主要是用于 fail-fast 检测以便尽可能快速地抛出ConcurrentModificationException)。

  2. 仅在当前容量大于实际大小时进行操作:

    • 若 size == 0,把 elementData 指向共享的 EMPTY_ELEMENTDATA 空数组
    • 否则用 Arrays.copyOf 按当前大小复制数组。
      该方法在需要复制时,时间复杂度为 O(n)

    该方法与 ArrayList 的其它方法一致,线程不安全,在并发环境需外部同步,否则可能抛出 ConcurrentModificationException 或产生不确定行为。

ensureCapacity

这个方法主要的作用就是在我们明确知道要添加多少元素时,可以提前扩容以减少多次扩容带来的性能损耗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Increases the capacity of this {@code ArrayList} instance, if
* necessary, to ensure that it can hold at least the number of elements
* specified by the minimum capacity argument.
*
* 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳minCapacity指定数量的元素。
*
* @param minCapacity the desired minimum capacity
* 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
grow(minCapacity);
}
}

首先递增来自于父类的 modCount 来标记结构性修改。
仅在当前容量大于实际大小时进行操作:
只有当请求容量大于当前数组长度时才会扩容;并且当当前数组是用于默认构造器的DEFAULTCAPACITY_EMPTY_ELEMENTDATA且请求容量不超过默认容量 DEFAULT_CAPACITY 时,(也就是通过无参构造器构造的时候)保持延迟初始化(不提前分配),以便首次添加元素时按默认策略扩容

grow

grow是ArrayList的私有方法,主要用于实际执行扩容逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* 增加容量以确保至少能容纳minCapacity指定数量的元素。
*
* @param minCapacity the desired minimum capacity
* 所需的最小容量
* @throws OutOfMemoryError if minCapacity is less than zero
* 如果minCapacity小于0会抛出内存溢出错误
*/
private Object[] grow(int minCapacity) {
// 获取老容量,也就是当前容量
int oldCapacity = elementData.length;
// 如果当前容量大于0 或者 元素不是默认构造器的占位符DEFAULTCAPACITY_EMPTY_ELEMENTDATA
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth 最小增长量 */
oldCapacity >> 1 /* preferred growth 推荐增长量 */);
// 复制原数组到新容量
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
// 如果原数组为空,则分配默认容量或minCapacity中较大的值
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}

这个方法是ArrayList中相当重要的的一个方法,主要是实现了扩容的功能。因此,我们要重点分析一下它的实现。

首先,它是记录了一下当前容量的大小,然后再进行下面的操作。

如果当前容量大于0,或者当前数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,前面说明过,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个被final修饰的空数组,在三个构造方法中,只有无参构造方法中elementData被赋予了DEFAULTCAPACITY_EMPTY_ELEMENTDATA。

首先先看else分支,它只会处理的唯一的一种情况——用默认无参构造方法创建的数组的初始扩容情况
此时的容量为0,添加一个元素时会创建一个新的数组,其大小为max(DEFAULT_CAPACITY, minCapacity)。
从上面的源码变量信息中可得知DEFAULT_CAPACITY是一个常量,其值为10,而minCapacity的值为(0+1) 。
所以添加一个元素时,max(DEFAULT_CAPACITY, minCapacity)的值必为10。也就是说,当我们用默认无参构造方法创建的数组在添加元素前,ArrayList的容量为0,添加一个元素后,ArrayList的容量就变为10了。

看完了else,我们在看看if,来看看能处理其余的扩容情况的if语句是怎么做的。

我们来看一下if里面的操作,先创建一个新的数组,然后将旧数组拷贝到新数组并赋给elementData返回。ArraysSupport.newLength函数的作用是创建一个大小为oldCapacity + max(minimum growth, preferred growth)的数组。

minCapacity是传入的参数,我们上面看过,它的值是当前容量(老容量)+1,那么minCapacity - oldCapacity的值就恒为1,minimum growth的值也就恒为1。

oldCapacity >> 1的功能是将oldCapacity 进行位运算,右移一位,也就是减半,preferred growth的值即为oldCapacity大小的一半。

也就是说,在用户没有干预,没有添加大量元素的情况下,自动扩容的规则是:第一次容量增长为10,后续每一次扩容都是当前容量的1.5倍(只是近似,因为右移碰上奇数会舍弃小数部分)

grow扩容的一点题外话

当oldCapacity为0时,右移后还是0,也就是说此时扩容的大小为0+max(1,0)=1,容量从0扩展到1。那么什么时候是这种情况呢?

  1. 用户调用了传容量的构造方法,并且传入的是0,此时elementData被赋予的是EMPTY_ELEMENTDATA,此时数组容量为0,添加元素时,符合if的条件,会进入此扩容情况,容量从0扩展到1。

  2. 传Collection元素列表的构造方法被传入空列表时,elementData被赋予的是EMPTY_ELEMENTDATA,数组容量为0,此时添加元素时,符合if的条件,会进入此扩容情况,容量从0扩展到1。

1
2
3
4
# 重置 Dock 图标数据库
rm ~/Library/Application\ Support/Dock/*.db && killall Dock
# 重置 Launchpad 图标数据库
defaults write com.apple.dock ResetLaunchPad -bool true && killall Dock

完成以上操作后,Launchpad 图标布局已经恢复默认设置,苹果官方提供的 App 都被重新排列到 Launchpad 第一屏幕中,然后根据自己的需要来进行重新排列 App 即可。

Mac版Docker旧版本(稳定版)的链接,时间跨度为2017年11月至2020年9月。

列表中的最后一个:2.4.0.0版本(48506)适用于macOS Mojave(10.14.6)

https://download.docker.com/mac/stable/20182/Docker.dmg
https://download.docker.com/mac/stable/20233/Docker.dmg
https://download.docker.com/mac/stable/20242/Docker.dmg
https://download.docker.com/mac/stable/20256/Docker.dmg
https://download.docker.com/mac/stable/20257/Docker.dmg
https://download.docker.com/mac/stable/20268/Docker.dmg
https://download.docker.com/mac/stable/20462/Docker.dmg
https://download.docker.com/mac/stable/20633/Docker.dmg
https://download.docker.com/mac/stable/20744/Docker.dmg
https://download.docker.com/mac/stable/20931/Docker.dmg
https://download.docker.com/mac/stable/21090/Docker.dmg
https://download.docker.com/mac/stable/21326/Docker.dmg
https://download.docker.com/mac/stable/21335/Docker.dmg
https://download.docker.com/mac/stable/21498/Docker.dmg
https://download.docker.com/mac/stable/21581/Docker.dmg
https://download.docker.com/mac/stable/21598/Docker.dmg
https://download.docker.com/mac/stable/21624/Docker.dmg
https://download.docker.com/mac/stable/21636/Docker.dmg
https://download.docker.com/mac/stable/21639/Docker.dmg
https://download.docker.com/mac/stable/21648/Docker.dmg
https://download.docker.com/mac/stable/21662/Docker.dmg
https://download.docker.com/mac/stable/21679/Docker.dmg
https://download.docker.com/mac/stable/21682/Docker.dmg
https://download.docker.com/mac/stable/21698/Docker.dmg
https://download.docker.com/mac/stable/21707/Docker.dmg
https://download.docker.com/mac/stable/21748/Docker.dmg
https://download.docker.com/mac/stable/21751/Docker.dmg
https://download.docker.com/mac/stable/21752/Docker.dmg
https://download.docker.com/mac/stable/21755/Docker.dmg
https://download.docker.com/mac/stable/21763/Docker.dmg
https://download.docker.com/mac/stable/21788/Docker.dmg
https://download.docker.com/mac/stable/21792/Docker.dmg
https://download.docker.com/mac/stable/21794/Docker.dmg
https://download.docker.com/mac/stable/21801/Docker.dmg
https://download.docker.com/mac/stable/21805/Docker.dmg
https://download.docker.com/mac/stable/21811/Docker.dmg
https://download.docker.com/mac/stable/21816/Docker.dmg
https://download.docker.com/mac/stable/21865/Docker.dmg
https://download.docker.com/mac/stable/21960/Docker.dmg
https://download.docker.com/mac/stable/21967/Docker.dmg
https://download.docker.com/mac/stable/21972/Docker.dmg
https://download.docker.com/mac/stable/21992/Docker.dmg
https://download.docker.com/mac/stable/21995/Docker.dmg
https://download.docker.com/mac/stable/22049/Docker.dmg
https://download.docker.com/mac/stable/22140/Docker.dmg
https://download.docker.com/mac/stable/22171/Docker.dmg
https://download.docker.com/mac/stable/22927/Docker.dmg
https://download.docker.com/mac/stable/22958/Docker.dmg
https://download.docker.com/mac/stable/22968/Docker.dmg
https://download.docker.com/mac/stable/22998/Docker.dmg
https://download.docker.com/mac/stable/23002/Docker.dmg
https://download.docker.com/mac/stable/23011/Docker.dmg
https://download.docker.com/mac/stable/23091/Docker.dmg
https://download.docker.com/mac/stable/23093/Docker.dmg
https://download.docker.com/mac/stable/23237/Docker.dmg
https://download.docker.com/mac/stable/23293/Docker.dmg
https://download.docker.com/mac/stable/23417/Docker.dmg
https://download.docker.com/mac/stable/23469/Docker.dmg
https://download.docker.com/mac/stable/23519/Docker.dmg
https://download.docker.com/mac/stable/23544/Docker.dmg
https://download.docker.com/mac/stable/23559/Docker.dmg
https://download.docker.com/mac/stable/23608/Docker.dmg
https://download.docker.com/mac/stable/23739/Docker.dmg
https://download.docker.com/mac/stable/23751/Docker.dmg
https://download.docker.com/mac/stable/23770/Docker.dmg
https://download.docker.com/mac/stable/24012/Docker.dmg
https://download.docker.com/mac/stable/24150/Docker.dmg
https://download.docker.com/mac/stable/24166/Docker.dmg
https://download.docker.com/mac/stable/24198/Docker.dmg
https://download.docker.com/mac/stable/24214/Docker.dmg
https://download.docker.com/mac/stable/24219/Docker.dmg
https://download.docker.com/mac/stable/24221/Docker.dmg
https://download.docker.com/mac/stable/24226/Docker.dmg
https://download.docker.com/mac/stable/24245/Docker.dmg
https://download.docker.com/mac/stable/24312/Docker.dmg
https://download.docker.com/mac/stable/24813/Docker.dmg
https://download.docker.com/mac/stable/26199/Docker.dmg
https://download.docker.com/mac/stable/26348/Docker.dmg
https://download.docker.com/mac/stable/26357/Docker.dmg
https://download.docker.com/mac/stable/26360/Docker.dmg
https://download.docker.com/mac/stable/26369/Docker.dmg
https://download.docker.com/mac/stable/26380/Docker.dmg
https://download.docker.com/mac/stable/26383/Docker.dmg
https://download.docker.com/mac/stable/26385/Docker.dmg
https://download.docker.com/mac/stable/26386/Docker.dmg
https://download.docker.com/mac/stable/26388/Docker.dmg
https://download.docker.com/mac/stable/26389/Docker.dmg
https://download.docker.com/mac/stable/26392/Docker.dmg
https://download.docker.com/mac/stable/26395/Docker.dmg
https://download.docker.com/mac/stable/26399/Docker.dmg
https://download.docker.com/mac/stable/26401/Docker.dmg
https://download.docker.com/mac/stable/26413/Docker.dmg
https://download.docker.com/mac/stable/26414/Docker.dmg
https://download.docker.com/mac/stable/26511/Docker.dmg
https://download.docker.com/mac/stable/26540/Docker.dmg
https://download.docker.com/mac/stable/26630/Docker.dmg
https://download.docker.com/mac/stable/26672/Docker.dmg
https://download.docker.com/mac/stable/26709/Docker.dmg
https://download.docker.com/mac/stable/26710/Docker.dmg
https://download.docker.com/mac/stable/26711/Docker.dmg
https://download.docker.com/mac/stable/26734/Docker.dmg
https://download.docker.com/mac/stable/26751/Docker.dmg
https://download.docker.com/mac/stable/26753/Docker.dmg
https://download.docker.com/mac/stable/26764/Docker.dmg
https://download.docker.com/mac/stable/28832/Docker.dmg
https://download.docker.com/mac/stable/28848/Docker.dmg
https://download.docker.com/mac/stable/28862/Docker.dmg
https://download.docker.com/mac/stable/28864/Docker.dmg
https://download.docker.com/mac/stable/28878/Docker.dmg
https://download.docker.com/mac/stable/28880/Docker.dmg
https://download.docker.com/mac/stable/28899/Docker.dmg
https://download.docker.com/mac/stable/28905/Docker.dmg
https://download.docker.com/mac/stable/29203/Docker.dmg
https://download.docker.com/mac/stable/29211/Docker.dmg
https://download.docker.com/mac/stable/29348/Docker.dmg
https://download.docker.com/mac/stable/29389/Docker.dmg
https://download.docker.com/mac/stable/29520/Docker.dmg
https://download.docker.com/mac/stable/29878/Docker.dmg
https://download.docker.com/mac/stable/29930/Docker.dmg
https://download.docker.com/mac/stable/30127/Docker.dmg
https://download.docker.com/mac/stable/30134/Docker.dmg
https://download.docker.com/mac/stable/30161/Docker.dmg
https://download.docker.com/mac/stable/30176/Docker.dmg
https://download.docker.com/mac/stable/30215/Docker.dmg
https://download.docker.com/mac/stable/30293/Docker.dmg
https://download.docker.com/mac/stable/36664/Docker.dmg
https://download.docker.com/mac/stable/36874/Docker.dmg
https://download.docker.com/mac/stable/37199/Docker.dmg
https://download.docker.com/mac/stable/37877/Docker.dmg
https://download.docker.com/mac/stable/38240/Docker.dmg
https://download.docker.com/mac/stable/39773/Docker.dmg
https://download.docker.com/mac/stable/40693/Docker.dmg
https://download.docker.com/mac/stable/42247/Docker.dmg
https://download.docker.com/mac/stable/42716/Docker.dmg
https://download.docker.com/mac/stable/43472/Docker.dmg
https://download.docker.com/mac/stable/43829/Docker.dmg
https://download.docker.com/mac/stable/43832/Docker.dmg
https://download.docker.com/mac/stable/43849/Docker.dmg
https://download.docker.com/mac/stable/43884/Docker.dmg
https://download.docker.com/mac/stable/45183/Docker.dmg
https://download.docker.com/mac/stable/45519/Docker.dmg
https://download.docker.com/mac/stable/46911/Docker.dmg
https://download.docker.com/mac/stable/47997/Docker.dmg
https://download.docker.com/mac/stable/48029/Docker.dmg
https://download.docker.com/mac/stable/48506/Docker.dmg

Catalina mac os 10.15 最后一个支持版本 v4.15.0

big sur mac os 11 最后一个支持版本为 v4.24.2

截止目前最新版本:4.28.0

就在刚才,键盘被猫碰掉到地上,敲出了一行神秘字符,神奇的是它还真能用,特此记录。

1
2
3
4
5
// Topaz Impression2 License
// 适用于Topaz Impression2的注册码,谁还会想要使用破解版呢。

000069-141028-408825-414022-482506

就在刚才,键盘被猫碰掉到地上,敲出了一行神秘字符,神奇的是它还真能用,特此记录。

1
2
3
4
5
// Topaz Glow2 License
// 适用于Topaz Glow2的注册码,谁还会想要使用破解版呢。

000059-150201-904563-911451-273019

就在刚才,键盘被猫碰掉到地上,敲出了一行神秘字符,神奇的是它还真能用,特此记录。

1
2
3
4
5
6
7
8
9
10
// Topaz Remask5 License
// 适用于Topaz Remask5的注册码,谁还会想要使用破解版呢。

// 五选一
396729-110613-671365-611287-805052
345539-110613-827603-575889-338686
396729-110613-671365-611287-805052
767919-110613-887726-599742-487926
486389-110613-858235-885706-086593

就在刚才,键盘被猫碰掉到地上,敲出了一行神秘字符,神奇的是它还真能用,特此记录。

1
2
3
4
5
6
7
8
// install4j License
// 适用于install4j最新版的注册码,谁还会想要使用破解版呢。

// v9
S-NEO_PENG#890808-wm1fa21hg0qqz#82622

// v10
S-NEO_PENG#890808-3gkd3i1jyekke#262a69

午后小憩的时候又梦到了我的猫,梦中的它给我表演了一系列神秘的操作后,醒来以后复刻了一下猫咪的操作,神奇的是它还真能用,特此记录。
就在刚才,键盘被猫碰掉到地上,敲出了一行神秘字符,神奇的是它还真能用,特此记录。

windows/mac安装

通过下方官网地址下载软件并进行安装即可。

1
https://typora.io/

windows/mac激活

安装完成后,如果是window系统,进入typora的安装目录(默认是C:\Program Files\Typora)下的 \resources\page-dist\static\js 目录,找到 LicenseIndex开头的文件。

如果是mac系统,默认情况下,直接执行以下命令定位:

1
open /Applications/Typora.app/Contents/Resources/TypeMark/page-dist/static/js

同样在目录下找到 LicenseIndex 开头的 Js文件。

用文本编辑器打开该文件,搜索

1
e.hasActivated="true"==e.hasActivated

并将其替换为

1
e.hasActivated="true"=="true"

实际上只要让e.hasActivated返回一个为真的布尔值即可,因此可完全可以替换成这样:

1
e.hasActivated=true

(修改前,可将该文件备份一下,万一操作失误,可以进行恢复)

经过以上修改后,重启Typora后,点击左下角未激活,直接提示已激活。

macos可能会提示软件损坏,该开启任何来源开启任何来源,该移除com.apple.quarantine移除属性。

0%