diff --git a/notes/Java 容器.md b/notes/Java 容器.md
index 99d88f89..3513492c 100644
--- a/notes/Java 容器.md
+++ b/notes/Java 容器.md
@@ -16,6 +16,9 @@
* [ConcurrentHashMap](#concurrenthashmap)
* [LinkedHashMap](#linkedhashmap)
* [WeakHashMap](#weakhashmap)
+ * [附录:实测](#附录实测)
+ * [A1 ArrayList 的扩容与常见陷阱](#a1-arraylist-的扩容与常见陷阱)
+ * [A2 HashMap 的并发写入](#a2-hashmap-的并发写入)
* [参考资料](#参考资料)
@@ -26,40 +29,32 @@
### Collection
-
+
#### 1. Set
-- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
-
-- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
-
+- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN);
+- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的;
- LinkedHashSet:具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。
#### 2. List
-- ArrayList:基于动态数组实现,支持随机访问。
-
-- Vector:和 ArrayList 类似,但它是线程安全的。
-
+- ArrayList:基于动态数组实现,支持随机访问;
+- Vector:和 ArrayList 类似,但它是线程安全的;
- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
#### 3. Queue
-- LinkedList:可以用它来实现双向队列。
-
+- LinkedList:可以用它来实现双向队列;
- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
### Map
-
-
-- TreeMap:基于红黑树实现。
-
-- HashMap:基于哈希表实现。
-
-- Hashtable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 Hashtable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
+
+- TreeMap:基于红黑树实现;
+- HashMap:基于哈希表实现;
+- Hashtable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 Hashtable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全。ConcurrentHashMap 的效率更高,因为它不会锁住整个表:JDK 1.7 使用分段锁,JDK 1.8 只锁住一个桶;
- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
@@ -67,7 +62,7 @@
### 迭代器模式
-
+
Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。
@@ -104,11 +99,17 @@ List list = Arrays.asList(arr);
List list = Arrays.asList(1, 2, 3);
```
+asList() 返回的是数组的一个视图,而不是一个新的 ArrayList,使用时需要注意(JDK 21 实测,见[附录 A1](#a1-arraylist-的扩容与常见陷阱)):
+
+- 长度固定:可以 set(),但调用 add()、remove() 会抛出 UnsupportedOperationException;
+- 与数组共用存储:修改原数组后,List 中的元素也跟着改变;
+- 传入 int[] 这样的基本类型数组时,整个数组被当成一个元素,得到的 List 长度为 1。
+
+需要一个可以修改的列表时,用 new ArrayList<>(Arrays.asList(arr)) 复制一份。
+
## 三、源码分析
-如果没有特别说明,以下源码分析基于 JDK 1.8。
-
-在 IDEA 中 double shift 调出 Search EveryWhere,查找源码文件,找到之后就可以阅读源码。
+如果没有特别说明,以下源码分析基于 JDK 1.8。JDK 21 中这些类的主要逻辑没有变化,有差异的地方单独说明。
### ArrayList
@@ -122,17 +123,17 @@ public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
```
-数组的默认大小为 10。
+数组的默认大小为 10。用无参构造函数创建时,elementData 先指向一个长度为 0 的共享空数组,第一次添加元素时才分配长度为 10 的数组。
```java
private static final int DEFAULT_CAPACITY = 10;
```
-
+
#### 2. 扩容
-添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,即 oldCapacity+oldCapacity/2。其中 oldCapacity >> 1 需要取整,所以新容量大约是旧容量的 1.5 倍左右。(oldCapacity 为偶数就是 1.5 倍,为奇数就是 1.5 倍-0.5)
+添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,即 oldCapacity+oldCapacity/2。其中 oldCapacity >> 1 需要取整,所以新容量大约是旧容量的 1.5 倍(oldCapacity 为偶数就是 1.5 倍,为奇数就是 1.5 倍减 0.5)。例如依次添加元素时,容量的变化为 0、10、15、22、33(JDK 21 实测,见[附录 A1](#a1-arraylist-的扩容与常见陷阱))。JDK 21 的 grow() 改为调用 ArraysSupport.newLength() 计算新容量,增长的规则相同。
扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
@@ -258,6 +259,21 @@ modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。代码参考上节序列化中的 writeObject() 方法。
+最常见的情况是在 foreach 循环中删除元素。foreach 编译后使用迭代器,list.remove() 改变了 modCount,迭代器下一次调用 next() 时就会抛出异常。应该使用迭代器自己的 remove(),或者 removeIf():
+
+```java
+for (String s : list) {
+ if (s.equals("a")) list.remove(s); // ConcurrentModificationException
+}
+
+Iterator it = list.iterator();
+while (it.hasNext()) {
+ if (it.next().equals("a")) it.remove();
+}
+
+list.removeIf(s -> s.equals("a"));
+```
+
### Vector
@@ -350,7 +366,7 @@ List list = new CopyOnWriteArrayList<>();
写操作需要加锁,防止并发写入时导致写入数据丢失。
-写操作结束之后需要把原始数组指向新的复制数组。
+写操作结束之后需要把原始数组指向新的复制数组。JDK 21 中写操作改为用 synchronized 加锁,过程相同。
```java
public boolean add(E e) {
@@ -412,7 +428,7 @@ transient Node first;
transient Node last;
```
-
+
#### 2. 与 ArrayList 的比较
@@ -421,15 +437,22 @@ ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。Array
- 数组支持随机访问,但插入删除的代价很高,需要移动大量元素;
- 链表不支持随机访问,但插入删除只需要改变指针。
+需要注意的是,链表在中间插入前要先遍历到插入位置,这一步也是 O(N) 的,而且要逐个访问分散在内存中的节点;数组移动元素用的是 System.arraycopy(),一次复制一整块连续内存。例如在 10 万个元素的中间插入 1 万次,ArrayList 约 50 ms,LinkedList 约 0.8~1 秒;只有在头部插入时 LinkedList 才明显更快(JDK 21 实测,见[附录 A1](#a1-arraylist-的扩容与常见陷阱))。所以大多数场景下应该使用 ArrayList,需要在头尾频繁插入删除时使用 ArrayDeque 或 LinkedList。
+
### HashMap
-为了便于理解,以下源码分析以 JDK 1.7 为主。
+为了便于理解,以下源码分析以 JDK 1.7 为主。JDK 1.8 主要有以下变化,JDK 21 与 JDK 1.8 相同:
+
+- 数组元素由 Entry 改为 Node,链表长度达到 8 时转为红黑树,见第 8 节;
+- 新的键值对插入链表尾部,而不是头部;
+- 计算哈希值时只做一次扰动:(h = key.hashCode()) ^ (h >>> 16),把高 16 位异或到低 16 位,让高位也参与决定桶下标;
+- 扩容时不再逐个重新计算桶下标,而是按哈希值新增的那一位把链表拆成两条,分别放在原位置和原位置 + 旧容量,见第 6 节。
#### 1. 存储结构
-内部包含了一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry。
+内部包含了一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了四个字段,从 next 字段可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值对桶的数量取模结果相同的 Entry。
-
+
```java
transient Entry[] table;
@@ -498,8 +521,8 @@ map.put("K3", "V3");
```
- 新建一个 HashMap,默认大小为 16;
-- 插入 <K1,V1\> 键值对,先计算 K1 的 hashCode,为了便于说明,假设为 115(实际上 "K1".hashCode() 是 2374),使用除留余数法得到所在的桶下标 115%16=3。
-- 插入 <K2,V2\> 键值对,假设 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
+- 插入 <K1,V1\> 键值对,先计算 K1 的 hashCode,为了便于说明,假设为 115(实际上 "K1".hashCode() 是 2374),使用除留余数法得到所在的桶下标 115%16=3;
+- 插入 <K2,V2\> 键值对,假设 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6;
- 插入 <K3,V3\> 键值对,假设 K3 的 hashCode 也为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2\> 前面。
应该注意到链表的插入是以头插法方式进行的,例如上面的 <K3,V3\> 不是插在 <K2,V2\> 后面,而是插入在链表头部。JDK 1.8 改成了尾插法。
@@ -509,7 +532,7 @@ map.put("K3", "V3");
- 计算键值对所在的桶;
- 在链表上顺序查找,时间复杂度显然和链表的长度成正比。
-
+
#### 3. put 操作
@@ -599,7 +622,7 @@ int hash = hash(key);
int i = indexFor(hash, table.length);
```
-**4.1 计算 hash 值**
+##### 4.1 计算哈希值
```java
final int hash(Object k) {
@@ -618,13 +641,16 @@ final int hash(Object k) {
}
```
+JDK 1.8 简化为一次扰动:
+
```java
-public final int hashCode() {
- return Objects.hashCode(key) ^ Objects.hashCode(value);
+static final int hash(Object key) {
+ int h;
+ return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
```
-**4.2 取模**
+##### 4.2 取模
令 x = 1\<\<4,即 x 为 2 的 4 次方,它具有以下性质:
@@ -649,9 +675,9 @@ x : 00010000
y%x : 00000010
```
-我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时用位运算的话能带来更高的性能。
+位运算的代价比求模运算小得多,因此在进行这种计算时用位运算的话能带来更高的性能。
-确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运算。
+确定桶下标的最后一步是将 key 的哈希值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运算。
```java
static int indexFor(int h, int length) {
@@ -671,7 +697,7 @@ static int indexFor(int h, int length) {
| :--: | :-- |
| capacity | table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。|
| size | 键值对数量。 |
-| threshold | size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。 |
+| threshold | size 的临界值。JDK 1.7 中 size 大于等于 threshold、并且要插入的桶不为空时扩容;JDK 1.8 中插入后 size 大于 threshold 时扩容。 |
| loadFactor | 装载因子,table 能够使用的比例,threshold = (int)(capacity* loadFactor)。 |
```java
@@ -744,7 +770,7 @@ void transfer(Entry[] newTable) {
假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32:
-```html
+```text
capacity : 00010000
new capacity : 00100000
```
@@ -754,6 +780,8 @@ new capacity : 00100000
- 为 0,那么 hash%00010000 = hash%00100000,桶位置和原来一致;
- 为 1,hash%00100000 = hash%00010000 + 16,桶位置是原位置 + 16。
+JDK 1.8 的 resize() 就利用了这个性质:遍历旧桶中的链表,按 (hash & oldCap) 是否为 0 把节点分到 lo、hi 两条链表,分别放到新数组的 j 和 j + oldCap 位置,链表中节点的相对顺序保持不变。
+
#### 7. 计算数组容量
@@ -795,22 +823,24 @@ static final int tableSizeFor(int cap) {
#### 9. 与 Hashtable 的比较
-- Hashtable 使用 synchronized 来进行同步。
-- HashMap 可以插入键为 null 的 Entry。
-- HashMap 的迭代器是 fail-fast 迭代器。
+- Hashtable 使用 synchronized 来进行同步;
+- HashMap 可以插入键为 null 的 Entry;
+- HashMap 的迭代器是 fail-fast 迭代器;
- HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
#### 10. 线程安全
-HashMap 不是线程安全的。例如两个线程同时向同一个空桶插入,都判断桶为空,后写入的会覆盖先写入的;size++ 也不是原子操作。在 JDK 21 上用 8 个线程各插入 10 万个不同的键,HashMap 的 size 每次都不同,例如 281854、463269,远少于 80 万;同样的操作 ConcurrentHashMap 每次都是 800000。
+HashMap 不是线程安全的。例如两个线程同时向同一个空桶插入,都判断桶为空,后写入的会覆盖先写入的;size++ 也不是原子操作。用 8 个线程各插入 10 万个不同的键,HashMap 的 size 每次都不同,例如 111068、323870,远少于 80 万;同样的操作 ConcurrentHashMap 每次都是 800000(JDK 21 实测,见[附录 A2](#a2-hashmap-的并发写入))。
-JDK 1.7 的问题更严重:两个线程同时扩容时,头插法会把链表的顺序反转,可能让链表形成环,之后在这个桶上的 get 会陷入死循环。JDK 1.8 改为尾插法,扩容时保持链表原来的顺序,不会再成环,但并发写入仍然会丢失数据。多线程环境应该使用 ConcurrentHashMap。
+JDK 1.7 的问题更严重:两个线程同时扩容时,头插法会把链表的顺序反转,可能让链表形成环,之后在这个桶上的 get 会陷入死循环。JDK 1.8 改为尾插法,扩容时保持链表原来的顺序,不会再因为头插法成环,但并发写入仍然会丢失数据;链表转为红黑树时还可能破坏树的结构,实测中出现过线程抛出 ClassCastException,以及线程在 TreeNode.treeify() 中一直循环不返回的情况(见[附录 A2](#a2-hashmap-的并发写入))。多线程环境应该使用 ConcurrentHashMap。
### ConcurrentHashMap
+第 1、2 节是 JDK 1.7 的实现,JDK 1.8 的改动见第 3 节。
+
#### 1. 存储结构
-
+
```java
static final class HashEntry {
@@ -876,7 +906,6 @@ ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两
如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
```java
-
/**
* Number of unsynchronized retries in size and containsValue
* methods before resorting to locking. This is used to avoid
@@ -937,7 +966,7 @@ JDK 1.8 去掉了 Segment,和 HashMap 一样使用 Node 数组加链表(或
1. 用 spread() 计算哈希值,把 hashCode 的高 16 位异或到低 16 位,让高位也参与决定桶下标;
2. 如果桶是空的,用 CAS 把新节点放进去,不加锁。CAS 失败说明有其它线程刚刚放入了节点,重新循环;
3. 如果桶的头节点是 ForwardingNode(哈希值为 MOVED),说明正在扩容,当前线程先帮忙迁移数据;
-4. 否则用 synchronized 锁住桶的头节点,在链表或红黑树中插入或更新,链表长度达到 8 时转为红黑树;
+4. 否则用 synchronized 锁住桶的头节点,在链表或红黑树中插入或更新,链表长度达到 8 时转为红黑树,数组长度小于 64 时则只扩容;
5. 最后更新元素个数。
get 不加锁:数组元素通过 Unsafe 的 volatile 方式读取,节点的 val 和 next 都是 volatile,能读到其它线程写入的最新值。
@@ -1045,7 +1074,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) {
以下是使用 LinkedHashMap 实现的一个 LRU 缓存:
-- 设定最大缓存空间 MAX_ENTRIES 为 3;
+- 设定最大缓存空间 MAX_ENTRIES 为 3;
- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
- 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
@@ -1075,7 +1104,7 @@ public static void main(String[] args) {
}
```
-```html
+```text
[3, 1, 4]
```
@@ -1083,7 +1112,7 @@ public static void main(String[] args) {
#### 存储结构
-WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。
+WeakHashMap 的 Entry 继承自 WeakReference,键被 WeakReference 关联,没有其它强引用时,键在下一次垃圾收集时会被回收,对应的 Entry 随后从 Map 中清除。
WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收。
@@ -1098,8 +1127,8 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
ConcurrentCache 采取的是分代缓存:
- 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);
-- 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。
-- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收。
+- 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收;
+- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收;
- 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。
```java
@@ -1138,6 +1167,82 @@ public final class ConcurrentCache {
```
+## 附录:实测
+
+以下实验的环境为 Apple M1 Pro(10 核)、macOS,JDK 为 Adoptium OpenJDK 21.0.13。
+
+### A1 ArrayList 的扩容与常见陷阱
+
+用反射读取 elementData 的长度,运行时需要加上 --add-opens java.base/java.util=ALL-UNNAMED:
+
+```java
+Field f = ArrayList.class.getDeclaredField("elementData");
+f.setAccessible(true);
+ArrayList list = new ArrayList<>();
+for (int i = 1; i <= 23; i++) {
+ list.add("e" + i);
+ int cap = ((Object[]) f.get(list)).length; // 容量变化时打印
+}
+```
+
+```text
+new ArrayList<>():容量 0
+添加第 1 个元素后:容量 10,size 1
+添加第 11 个元素后:容量 15,size 11
+添加第 16 个元素后:容量 22,size 16
+添加第 23 个元素后:容量 33,size 23
+```
+
+正文中 foreach 删除元素和 Arrays.asList() 的几种用法:
+
+```text
+foreach 中 remove:ConcurrentModificationException
+Iterator.remove:[b, c]
+removeIf:[b, c]
+asList set:[10, 2, 3]
+asList add:UnsupportedOperationException
+修改数组后 list:[100, 2, 3]
+Arrays.asList(int[]).size():1
+```
+
+ArrayList 与 LinkedList 的插入:先放入 10 万个元素,再执行 list.add(list.size() / 2, i) 1 万次;另外从空列表开始执行 list.add(0, i) 10 万次:
+
+```text
+round 0: 10 万个元素中间插入 1 万次 ArrayList 51 ms,LinkedList 844 ms;头部插入 10 万次 ArrayList 445 ms,LinkedList 2 ms
+round 1: 10 万个元素中间插入 1 万次 ArrayList 47 ms,LinkedList 1029 ms;头部插入 10 万次 ArrayList 441 ms,LinkedList 3 ms
+round 2: 10 万个元素中间插入 1 万次 ArrayList 47 ms,LinkedList 831 ms;头部插入 10 万次 ArrayList 441 ms,LinkedList 0 ms
+```
+
+### A2 HashMap 的并发写入
+
+8 个线程各插入 10 万个不同的键(线程 t 插入 t × 100000 到 t × 100000 + 99999),结束后查看 size:
+
+```java
+for (int t = 0; t < 8; t++) {
+ int base = t * 100_000;
+ pool.submit(() -> { for (int i = 0; i < 100_000; i++) map.put(base + i, i); });
+}
+```
+
+```text
+HashMap size=111068 ConcurrentHashMap size=800000
+HashMap size=323870 ConcurrentHashMap size=800000
+HashMap size=732440 ConcurrentHashMap size=800000
+```
+
+改用守护线程,每轮最多等待 10 秒,记录没有结束的线程停在哪里。两次运行的输出(节选):
+
+```text
+Exception in thread "putter-6" java.lang.ClassCastException: class java.util.HashMap$Node cannot be cast to class java.util.HashMap$TreeNode
+ at java.base/java.util.HashMap$TreeNode.moveRootToFront(HashMap.java:1994)
+ at java.base/java.util.HashMap$TreeNode.treeify(HashMap.java:2110)
+ at java.base/java.util.HashMap.treeifyBin(HashMap.java:778)
+run 0: size=278394 stuckThreads=0
+
+run 0: size=263620 stuckThreads=4
+ putter-4 at java.base/java.util.HashMap$TreeNode.treeify(HashMap.java:2107) / java.base/java.util.HashMap$TreeNode.split(HashMap.java:2339)
+```
+
## 参考资料
- Eckel B. Java 编程思想 [M]. 机械工业出版社, 2002.
@@ -1149,5 +1254,7 @@ public final class ConcurrentCache {
- [探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/)
- [HashMap 相关面试题及其解答](https://www.jianshu.com/p/75adf47958a7)
- [Java 集合细节(二):asList 的缺陷](http://wiki.jikexueyuan.com/project/java-enhancement/java-thirtysix.html)
-- [Java Collection Framework – The LinkedList Class](http://javaconceptoftheday.com/java-collection-framework-linkedlist-class/)
-
+- [Java Collection Framework – The LinkedList Class](https://javaconceptoftheday.com/java-collection-framework-linkedlist-class/)
+- [JDK 8 源码 java.util](https://github.com/openjdk/jdk8u/tree/master/jdk/src/share/classes/java/util)
+- [Java SE 21 API: Arrays.asList](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Arrays.html#asList(T...))
+- [Java SE 21 API: ArrayDeque](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/ArrayDeque.html)
diff --git a/notes/pics/jcol-arraylist.png b/notes/pics/jcol-arraylist.png
new file mode 100644
index 00000000..c21d3fb9
Binary files /dev/null and b/notes/pics/jcol-arraylist.png differ
diff --git a/notes/pics/jcol-chaining.png b/notes/pics/jcol-chaining.png
new file mode 100644
index 00000000..b39ea540
Binary files /dev/null and b/notes/pics/jcol-chaining.png differ
diff --git a/notes/pics/jcol-chm7.png b/notes/pics/jcol-chm7.png
new file mode 100644
index 00000000..3891b31b
Binary files /dev/null and b/notes/pics/jcol-chm7.png differ
diff --git a/notes/pics/jcol-collection.png b/notes/pics/jcol-collection.png
new file mode 100644
index 00000000..79cde018
Binary files /dev/null and b/notes/pics/jcol-collection.png differ
diff --git a/notes/pics/jcol-hashmap.png b/notes/pics/jcol-hashmap.png
new file mode 100644
index 00000000..0d74f409
Binary files /dev/null and b/notes/pics/jcol-hashmap.png differ
diff --git a/notes/pics/jcol-iterator.png b/notes/pics/jcol-iterator.png
new file mode 100644
index 00000000..7494024e
Binary files /dev/null and b/notes/pics/jcol-iterator.png differ
diff --git a/notes/pics/jcol-linkedlist.png b/notes/pics/jcol-linkedlist.png
new file mode 100644
index 00000000..1cd937c8
Binary files /dev/null and b/notes/pics/jcol-linkedlist.png differ
diff --git a/notes/pics/jcol-map.png b/notes/pics/jcol-map.png
new file mode 100644
index 00000000..c2ac01f1
Binary files /dev/null and b/notes/pics/jcol-map.png differ