auto commit
This commit is contained in:
141
notes/Java 容器.md
141
notes/Java 容器.md
@ -1,32 +1,22 @@
|
||||
# Java 容器
|
||||
<!-- GFM-TOC -->
|
||||
* [一、概览](#一概览)
|
||||
* [Collection](#collection)
|
||||
* [Map](#map)
|
||||
* [二、容器中的设计模式](#二容器中的设计模式)
|
||||
* [迭代器模式](#迭代器模式)
|
||||
* [适配器模式](#适配器模式)
|
||||
* [三、源码分析](#三源码分析)
|
||||
* [ArrayList](#arraylist)
|
||||
* [Vector](#vector)
|
||||
* [CopyOnWriteArrayList](#copyonwritearraylist)
|
||||
* [LinkedList](#linkedlist)
|
||||
* [HashMap](#hashmap)
|
||||
* [ConcurrentHashMap](#concurrenthashmap)
|
||||
* [LinkedHashMap](#linkedhashmap)
|
||||
* [WeakHashMap](#weakhashmap)
|
||||
* [参考资料](#参考资料)
|
||||
* [Java 容器](#java-容器)
|
||||
* [一、概览](#一概览)
|
||||
* [二、容器中的设计模式](#二容器中的设计模式)
|
||||
* [三、源码分析](#三源码分析)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、概览
|
||||
## 一、概览
|
||||
|
||||
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
|
||||
|
||||
## Collection
|
||||
### Collection
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208220948084.png"/> </div><br>
|
||||
|
||||
### 1. Set
|
||||
#### 1. Set
|
||||
|
||||
- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
|
||||
|
||||
@ -34,7 +24,7 @@
|
||||
|
||||
- LinkedHashSet:具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。
|
||||
|
||||
### 2. List
|
||||
#### 2. List
|
||||
|
||||
- ArrayList:基于动态数组实现,支持随机访问。
|
||||
|
||||
@ -42,13 +32,13 @@
|
||||
|
||||
- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
|
||||
|
||||
### 3. Queue
|
||||
#### 3. Queue
|
||||
|
||||
- LinkedList:可以用它来实现双向队列。
|
||||
|
||||
- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
|
||||
|
||||
## Map
|
||||
### Map
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20201101234335837.png"/> </div><br>
|
||||
|
||||
@ -61,9 +51,9 @@
|
||||
- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
|
||||
|
||||
|
||||
# 二、容器中的设计模式
|
||||
## 二、容器中的设计模式
|
||||
|
||||
## 迭代器模式
|
||||
### 迭代器模式
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208225301973.png"/> </div><br>
|
||||
|
||||
@ -80,7 +70,7 @@ for (String item : list) {
|
||||
}
|
||||
```
|
||||
|
||||
## 适配器模式
|
||||
### 适配器模式
|
||||
|
||||
java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
|
||||
|
||||
@ -102,16 +92,16 @@ List list = Arrays.asList(arr);
|
||||
List list = Arrays.asList(1, 2, 3);
|
||||
```
|
||||
|
||||
# 三、源码分析
|
||||
## 三、源码分析
|
||||
|
||||
如果没有特别说明,以下源码分析基于 JDK 1.8。
|
||||
|
||||
在 IDEA 中 double shift 调出 Search EveryWhere,查找源码文件,找到之后就可以阅读源码。
|
||||
|
||||
## ArrayList
|
||||
### ArrayList
|
||||
|
||||
|
||||
### 1. 概览
|
||||
#### 1. 概览
|
||||
|
||||
因为 ArrayList 是基于数组实现的,所以支持快速随机访问。RandomAccess 接口标识着该类支持快速随机访问。
|
||||
|
||||
@ -128,9 +118,9 @@ private static final int DEFAULT_CAPACITY = 10;
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208232221265.png"/> </div><br>
|
||||
|
||||
### 2. 扩容
|
||||
#### 2. 扩容
|
||||
|
||||
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。
|
||||
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity \>\> 1)`,也就是旧容量的 1.5 倍。
|
||||
|
||||
扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
|
||||
|
||||
@ -168,7 +158,7 @@ private void grow(int minCapacity) {
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 删除元素
|
||||
#### 3. 删除元素
|
||||
|
||||
需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的。
|
||||
|
||||
@ -185,7 +175,7 @@ public E remove(int index) {
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 序列化
|
||||
#### 4. 序列化
|
||||
|
||||
ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。
|
||||
|
||||
@ -250,16 +240,16 @@ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
|
||||
oos.writeObject(list);
|
||||
```
|
||||
|
||||
### 5. Fail-Fast
|
||||
#### 5. Fail-Fast
|
||||
|
||||
modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
|
||||
|
||||
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。代码参考上节序列化中的 writeObject() 方法。
|
||||
|
||||
|
||||
## Vector
|
||||
### Vector
|
||||
|
||||
### 1. 同步
|
||||
#### 1. 同步
|
||||
|
||||
它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。
|
||||
|
||||
@ -279,7 +269,7 @@ public synchronized E get(int index) {
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 扩容
|
||||
#### 2. 扩容
|
||||
|
||||
Vector 的构造函数可以传入 capacityIncrement 参数,它的作用是在扩容时使容量 capacity 增长 capacityIncrement。如果这个参数的值小于等于 0,扩容时每次都令 capacity 为原来的两倍。
|
||||
|
||||
@ -320,12 +310,12 @@ public Vector() {
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 与 ArrayList 的比较
|
||||
#### 3. 与 ArrayList 的比较
|
||||
|
||||
- Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
|
||||
- Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍。
|
||||
|
||||
### 4. 替代方案
|
||||
#### 4. 替代方案
|
||||
|
||||
可以使用 `Collections.synchronizedList();` 得到一个线程安全的 ArrayList。
|
||||
|
||||
@ -340,9 +330,9 @@ List<String> synList = Collections.synchronizedList(list);
|
||||
List<String> list = new CopyOnWriteArrayList<>();
|
||||
```
|
||||
|
||||
## CopyOnWriteArrayList
|
||||
### CopyOnWriteArrayList
|
||||
|
||||
### 1. 读写分离
|
||||
#### 1. 读写分离
|
||||
|
||||
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
|
||||
|
||||
@ -378,7 +368,7 @@ private E get(Object[] a, int index) {
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 适用场景
|
||||
#### 2. 适用场景
|
||||
|
||||
CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
|
||||
|
||||
@ -389,9 +379,9 @@ CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读
|
||||
|
||||
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
|
||||
|
||||
## LinkedList
|
||||
### LinkedList
|
||||
|
||||
### 1. 概览
|
||||
#### 1. 概览
|
||||
|
||||
基于双向链表实现,使用 Node 存储链表节点信息。
|
||||
|
||||
@ -412,18 +402,18 @@ transient Node<E> last;
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208233940066.png"/> </div><br>
|
||||
|
||||
### 2. 与 ArrayList 的比较
|
||||
#### 2. 与 ArrayList 的比较
|
||||
|
||||
ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。ArrayList 和 LinkedList 的区别可以归结为数组和链表的区别:
|
||||
|
||||
- 数组支持随机访问,但插入删除的代价很高,需要移动大量元素;
|
||||
- 链表不支持随机访问,但插入删除只需要改变指针。
|
||||
|
||||
## HashMap
|
||||
### HashMap
|
||||
|
||||
为了便于理解,以下源码分析以 JDK 1.7 为主。
|
||||
|
||||
### 1. 存储结构
|
||||
#### 1. 存储结构
|
||||
|
||||
内部包含了一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry。
|
||||
|
||||
@ -486,7 +476,7 @@ static class Entry<K,V> implements Map.Entry<K,V> {
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 拉链法的工作原理
|
||||
#### 2. 拉链法的工作原理
|
||||
|
||||
```java
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
@ -496,11 +486,11 @@ map.put("K3", "V3");
|
||||
```
|
||||
|
||||
- 新建一个 HashMap,默认大小为 16;
|
||||
- 插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。
|
||||
- 插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
|
||||
- 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 前面。
|
||||
- 插入 <K1,V1\> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 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> 后面,而是插入在链表头部。
|
||||
应该注意到链表的插入是以头插法方式进行的,例如上面的 <K3,V3\> 不是插在 <K2,V2\> 后面,而是插入在链表头部。
|
||||
|
||||
查找需要分成两步进行:
|
||||
|
||||
@ -509,7 +499,7 @@ map.put("K3", "V3");
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208235258643.png"/> </div><br>
|
||||
|
||||
### 3. put 操作
|
||||
#### 3. put 操作
|
||||
|
||||
```java
|
||||
public V put(K key, V value) {
|
||||
@ -588,7 +578,7 @@ Entry(int h, K k, V v, Entry<K,V> n) {
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 确定桶下标
|
||||
#### 4. 确定桶下标
|
||||
|
||||
很多操作都需要先确定一个键值对所在的桶下标。
|
||||
|
||||
@ -624,7 +614,7 @@ public final int hashCode() {
|
||||
|
||||
**4.2 取模**
|
||||
|
||||
令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:
|
||||
令 x = 1\<\<4,即 x 为 2 的 4 次方,它具有以下性质:
|
||||
|
||||
```
|
||||
x : 00010000
|
||||
@ -657,7 +647,7 @@ static int indexFor(int h, int length) {
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 扩容-基本原理
|
||||
#### 5. 扩容-基本原理
|
||||
|
||||
设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此查找的复杂度为 O(N/M)。
|
||||
|
||||
@ -736,7 +726,7 @@ void transfer(Entry[] newTable) {
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 扩容-重新计算桶下标
|
||||
#### 6. 扩容-重新计算桶下标
|
||||
|
||||
在进行扩容时,需要把键值对重新计算桶下标,从而放到对应的桶上。在前面提到,HashMap 使用 hash%capacity 来确定桶下标。HashMap capacity 为 2 的 n 次方这一特点能够极大降低重新计算桶下标操作的复杂度。
|
||||
|
||||
@ -752,7 +742,7 @@ new capacity : 00100000
|
||||
- 为 0,那么 hash%00010000 = hash%00100000,桶位置和原来一致;
|
||||
- 为 1,hash%00010000 = hash%00100000 + 16,桶位置是原位置 + 16。
|
||||
|
||||
### 7. 计算数组容量
|
||||
#### 7. 计算数组容量
|
||||
|
||||
HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。
|
||||
|
||||
@ -785,20 +775,20 @@ static final int tableSizeFor(int cap) {
|
||||
}
|
||||
```
|
||||
|
||||
### 8. 链表转红黑树
|
||||
#### 8. 链表转红黑树
|
||||
|
||||
从 JDK 1.8 开始,一个桶存储的链表长度大于等于 8 时会将链表转换为红黑树。
|
||||
|
||||
### 9. 与 Hashtable 的比较
|
||||
#### 9. 与 Hashtable 的比较
|
||||
|
||||
- Hashtable 使用 synchronized 来进行同步。
|
||||
- HashMap 可以插入键为 null 的 Entry。
|
||||
- HashMap 的迭代器是 fail-fast 迭代器。
|
||||
- HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
|
||||
|
||||
## ConcurrentHashMap
|
||||
### ConcurrentHashMap
|
||||
|
||||
### 1. 存储结构
|
||||
#### 1. 存储结构
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191209001038024.png"/> </div><br>
|
||||
|
||||
@ -845,7 +835,7 @@ final Segment<K,V>[] segments;
|
||||
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
|
||||
```
|
||||
|
||||
### 2. size 操作
|
||||
#### 2. size 操作
|
||||
|
||||
每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
|
||||
|
||||
@ -918,7 +908,7 @@ public int size() {
|
||||
}
|
||||
```
|
||||
|
||||
### 3. JDK 1.8 的改动
|
||||
#### 3. JDK 1.8 的改动
|
||||
|
||||
JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。
|
||||
|
||||
@ -926,9 +916,9 @@ JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败
|
||||
|
||||
并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。
|
||||
|
||||
## LinkedHashMap
|
||||
### LinkedHashMap
|
||||
|
||||
### 存储结构
|
||||
#### 存储结构
|
||||
|
||||
继承自 HashMap,因此具有和 HashMap 一样的快速查找特性。
|
||||
|
||||
@ -963,7 +953,7 @@ void afterNodeAccess(Node<K,V> p) { }
|
||||
void afterNodeInsertion(boolean evict) { }
|
||||
```
|
||||
|
||||
### afterNodeAccess()
|
||||
#### afterNodeAccess()
|
||||
|
||||
当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。
|
||||
|
||||
@ -994,7 +984,7 @@ void afterNodeAccess(Node<K,V> e) { // move node to last
|
||||
}
|
||||
```
|
||||
|
||||
### afterNodeInsertion()
|
||||
#### afterNodeInsertion()
|
||||
|
||||
在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。
|
||||
|
||||
@ -1018,7 +1008,7 @@ protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
|
||||
}
|
||||
```
|
||||
|
||||
### LRU 缓存
|
||||
#### LRU 缓存
|
||||
|
||||
以下是使用 LinkedHashMap 实现的一个 LRU 缓存:
|
||||
|
||||
@ -1056,9 +1046,9 @@ public static void main(String[] args) {
|
||||
[3, 1, 4]
|
||||
```
|
||||
|
||||
## WeakHashMap
|
||||
### WeakHashMap
|
||||
|
||||
### 存储结构
|
||||
#### 存储结构
|
||||
|
||||
WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。
|
||||
|
||||
@ -1068,7 +1058,7 @@ WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存
|
||||
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
|
||||
```
|
||||
|
||||
### ConcurrentCache
|
||||
#### ConcurrentCache
|
||||
|
||||
Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
|
||||
|
||||
@ -1115,7 +1105,7 @@ public final class ConcurrentCache<K, V> {
|
||||
```
|
||||
|
||||
|
||||
# 参考资料
|
||||
## 参考资料
|
||||
|
||||
- Eckel B. Java 编程思想 [M]. 机械工业出版社, 2002.
|
||||
- [Java Collection Framework](https://www.w3resource.com/java-tutorial/java-collections.php)
|
||||
@ -1129,10 +1119,3 @@ public final class ConcurrentCache<K, V> {
|
||||
- [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/)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>
|
||||
|
Reference in New Issue
Block a user