集合类关系图
简图:
详图:
1. List
1.1 ArrayList
简介:
ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。
底层实现:
顾名思义,ArrayList就是用数组实现的列表。
默认大小:
10。private static final int DEFAULT_CAPACITY=10;
自动扩容: (通过add方法理解)
- 初始容量:创建ArrayList对象时,会分配一个初始容量,默认为10。
- 扩容触发条件:当ArrayList的size超过当前容量时,就会触发扩容操作。
- 扩容策略:ArrayList在扩容时,会创建一个新的更大容量的数组,并将原有元素复制到新数组中。
- 扩容大小:根据添加的元素个数计算出最小容量minCapacity(原数组元素个数+添加元素个数),计算出最小增长minGrowth(最小容量-原数组长度)和首选增长prefGrowth(原数组长度 >> 1,即1.5倍扩容),再计算出首选长度prefLength(原数组长度 + Math.max(minGrowth,prefGrowth))。对首选长度校验(0< prefLength && prefLength <=SOFT_MAX_ARRAY_LENGTH),是则返回首选长度作为扩容后的大小,否则进一步计算。
// 扩容核心代码
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
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 {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
Fail-Fast机制:
ArrayList也采用了快速失败的机制,通过记录修改计数器modCount来实现。首先在构造迭代器的时候,将当前的修改计数器的值保存,之后进行遍历的时候,每访问一个数据,都要检查当前集合的修改次数是否合法,如果有其他线程修改了集合,那么modCount就会被修改,当前修改计数器的值与之前保存的值(即期望值)不同,那么将抛出ConcurrentModificationException。
使用注意事项:
- 尽量预估初始容量:如果能够预先知道大致需要存储多少个元素,可以通过指定初始容量来减少扩容次数,提高性能。
- 避免频繁插入和删除操作:由于插入和删除操作的性能较低,如果需要频繁进行这些操作,建议考虑其他数据结构,如LinkedList。
- 不支持并发,如需要并发操作,需要使用Collections.synchronizedList或者CopyOnWriteArrayList来替代ArrayList。
1.2 LinkedList
简介:
LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。这样看来,LinkedList简直就是个全能冠军。当你需要使用栈或者队列时,可以考虑使用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue的类(它是个接口名字)。关于栈或队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能。
底层实现:
顾名思义,LinkedList底层通过双向链表实现。双向链表的每个节点用内部类Node表示。LinkedList通过first和last引用分别指向链表的第一个和最后一个元素。
核心代码:增删的核心方法unlink()、linkLast()
//unlink是remove内部实现方法
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
//linkLast是add内部实现方法
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
方法区分:
peek(): 查看第一个元素
poll(): 移除第一个元素并返回,链表为空返回null
pop(): 弹出第一个元素,链表为空抛出异常
push( E e): 从头部插入元素
offer(E e): 从尾部插入元素
Queue接口继承自Collection接口,除了最基本的Collection的方法之外,它还支持额外的insertion, extraction和inspection操作。这里有两组格式,共6个方法,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。
| Throws exception | Returns special value | |
|---|---|---|
| Insert | add(e) | offer(e) |
| Remove | remove() | poll() |
| Examine | element() | peek() |
Deque是"double ended queue", 表示双向的队列,英文读作"deck". Deque 继承自 Queue接口,除了支持Queue的方法之外,还支持insert, remove和examine操作,由于Deque是双向的,所以可以对队列的头和尾都进行操作,它同时也支持两组格式,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。共12个方法如下:
| First Element - Head | Last Element - Tail | |||
|---|---|---|---|---|
| Throws exception | Special value | Throws exception | Special value | |
| Insert | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
| Remove | removeFirst() | pollFirst() | removeLast() | pollLast() |
| Examine | getFirst() | peekFirst() | getLast() | peekLast() |
1.3 Vector
简介:
Vector实现了List接口,与ArrayList类似,底层通过数组实现。与ArrayList不同之处是Vector的方法都是线程安全的,这是通过在每个方法上使用同步锁synchronized来实现的,确保在同一时刻只有一个线程能够修改Vector。
底层实现:
底层通过数组实现。
默认大小:
10。
public Vector() { this(10); }
自动扩容: (通过add方法理解)
- 初始容量:创建Vector对象时,会分配一个初始容量,默认为10。
- 扩容触发条件:当Vector的size超过当前容量时,就会触发扩容操作。
- 扩容策略:Vector在扩容时,会创建一个新的更大容量的数组,并将原有元素复制到新数组中。
- 扩容大小:扩容逻辑与ArrayList类似,不同之处是首选增长prefGrowth(默认2倍扩容)。
// 扩容核心代码
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
capacityIncrement > 0 ? capacityIncrement : oldCapacity
/* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
}
2. Queue
2.1 PriorityQueue
简介:
PriorityQueue,即优先队列。优先队列的作用是能保证每次取出的元素都是队列中权值最小的(Java的优先队列每次取最小元素,C++的优先队列每次取最大元素)。这里牵涉到了大小关系,元素大小的评判可以通过元素本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator,类似于C++的仿函数)。
Java中PriorityQueue实现了Queue接口,不允许放入null元素;其通过堆实现,具体说是通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。
底层实现:
通过数组表示的小顶堆实现。小顶堆父子节点下标关系:
leftNo = parentNo*2+1
rightNo = parentNo*2+2
parentNo = (nodeNo-1)/2
核心代码:
添加元素:(add(E)和offer(E)方法,add内部调用的offer)
offer(E):
先判断是否需要扩容,如果需要先扩容。先将插入的元素放置在数组尾部,然后向上调整顶堆位置。从数组尾部元素开始进行循环,找到其父节点并将插入元素与其元素比较,如果小于将父节点的元素与插入元素调换位置,继续循环上述动作。如果大于(调整完毕)退出循环,直接插入到此位置。
// 添加元素
public boolean offer(E e) {
if (e == null)//不允许放入null元素
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);//自动扩容
siftUp(i, e);//把e放到数组i位置,然后向上调整
size = i + 1;
return true;
}
// 自动扩容:如果原容量小于64就2倍扩容,否则1.5倍扩容
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity < 64 ? oldCapacity + 2 : oldCapacity >> 1
/* preferred growth */);
queue = Arrays.copyOf(queue, newCapacity);
}
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x, queue, comparator);
else
siftUpComparable(k, x, queue);
}
// 向上调整顶堆
private static <T> void siftUpComparable(int k, T x, Object[] es) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = es[parent];
if (key.compareTo((T) e) >= 0)
break;
es[k] = e;
k = parent;
}
es[k] = key;
}
删除元素:(poll()、remove(Object)、removeEq(Object)、removeAt(int)等)
poll():
把数组尾部元素放在头部,将尾部元素置空并将元素个数减一,然后向下调整顶堆位置。从当前位置(顶堆顶部)开始进行循环,找出左右子节点中较小的一个,与它调换位置,直到当前节点是叶子结点或者当前节点的值小于或等于插入的元素 x 的值(调整完毕)退出循环。
// 删除元素
public E poll() {
final Object[] es;
final E result;
if ((result = (E) ((es = queue)[0])) != null) {
modCount++;
final int n;
final E x = (E) es[(n = --size)];
es[n] = null;
if (n > 0) {
final Comparator<? super E> cmp;
if ((cmp = comparator) == null)
siftDownComparable(0, x, es, n);
else
siftDownUsingComparator(0, x, es, n, cmp);
}
}
return result;
}
// 向下调整顶堆的代码
private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
// assert n > 0;
Comparable<? super T> key = (Comparable<? super T>)x;//将传入的元素 x 强制转换为 Comparable 接口,以便后续可以使用 compareTo 方法来比较元素
int half = n >>> 1; //将堆的大小 n 除以 2,得到非叶子节点的数量
while (k < half) {//直到当前位置 k 不再是非叶子节点
int child = (k << 1) + 1; //计算当前节点的左孩子的索引
Object c = es[child];//获取左孩子节点的值
int right = child + 1;//计算当前节点的右孩子的索引
if (right < n &&
((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
c = es[child = right];//检查右孩子是否存在且比左孩子小,如果右孩子存在且比左孩子小,则将 c 更新为右孩子的值,并将 child 更新为右孩子的索引
if (key.compareTo((T) c) <= 0)
break;//如果当前节点的值小于或等于插入的元素 x 的值,则退出循环
es[k] = c;//将当前节点的值更新为孩子节点的值
k = child;//将当前位置 k 更新为孩子节点的位置,以便继续向下迭代
}
es[k] = key;//将插入的元素 x 放置到最终位置 k 上,以保持堆的有序性
}
removeAt(int):
remove(Object)、removeEq(Object)内部都是调用removeAt
先将数据元素个数减一,然后将数组尾部的元素放到要删除的下标位置,将数组尾部置空;从删除下标位置向下调整顶堆位置;
// 删除指定下标元素
E removeAt(int i) {
// assert i >= 0 && i < size;
final Object[] es = queue;
modCount++;
int s = --size;
if (s == i) // removed last element
es[i] = null;//检查是否删除的是最后一个元素。如果是最后一个元素,直接将该位置的元素设为 null。
else {
E moved = (E) es[s];
es[s] = null;
siftDown(i, moved);//将moved元素放在i下标位置,然后向下调整顶堆
if (es[i] == moved) {//如果向下调整完,moved元素还在i下标位置,就向上调整顶堆
siftUp(i, moved);
if (es[i] != moved)
return moved;
}
}
return null;
}
2.2 ArrayDque
简介:
ArrayDeque是Deque的实现,官方推荐使用AarryDeque用作栈和队列。
底层实现:
ArrayDeque底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即循环数组(circular array),也就是说数组的任何一点都可能被看作起点或者终点。ArrayDeque是非线程安全的(not thread-safe),当多个线程同时使用的时候,需要程序员手动同步;另外,该容器不允许放入null元素。
默认大小:
16。
public ArrayDeque() { elements = new Object[16 + 1]; }
元素定位:
使用了位操作,非常巧妙的省去了边界处理等问题,简化了操作。
add相关方法,通过位运算定位添加元素,之后检查是否需要扩容;
poll相关方法,置空对应位置,位运算定位,返回元素;
自动扩容: (通过add方法理解)
- 扩容触发条件:当添加元素后发现head == tail,就会触发扩容操作。
- 扩容策略:ArrayDeque在扩容时,会创建一个新的更大容量的数组,并将原有元素复制到新数组中。
- 扩容大小:
// 扩容核心代码
private void grow(int needed) {
// overflow-conscious code
final int oldCapacity = elements.length;
int newCapacity;
// Double capacity if small; else grow by 50%
int jump = (oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1);//计算扩容的步长。如果当前容量小于64,扩容步长为当前容量加2,否则为当前容量的一半。
if (jump < needed
|| (newCapacity = (oldCapacity + jump)) - MAX_ARRAY_SIZE > 0)
newCapacity = newCapacity(needed, jump);//检查是否需要进行扩容。如果步长小于所需的额外容量(needed),或者新容量减去超过了最大数组容量限制,则调用 newCapacity(needed, jump) 方法计算新容量。
final Object[] es = elements = Arrays.copyOf(elements, newCapacity);
// Exceptionally, here tail == head needs to be disambiguated
if (tail < head || (tail == head && es[head] != null)) {//判断队列是否发生了循环,即队列的头部在数组的末尾,如果发生了循环,就需要进行一些额外的处理
// wrap around; slide first leg forward to end of array
int newSpace = newCapacity - oldCapacity;
System.arraycopy(es, head,
es, head + newSpace,
oldCapacity - head);//将原数组中头指针 head 到末尾的元素复制到新数组的末尾。这是为了处理队列循环的情况,将头部的元素“移动”到数组的末尾
for (int i = head, to = (head += newSpace); i < to; i++)
es[i] = null;//循环遍历数组中原头指针 head 到新头指针位置 to 之间的元素,并将它们在新数组中的位置设为 null,以释放对这些对象的引用,帮助垃圾回收系统回收不再需要的内存。
}
}
3. Map
3.1 HashMap
简介:
HashMap 是 Map 的一个实现类,它代表的是一种键值对的数据存储形式。Key 不允许重复出现,Value 随意。
底层实现:
jdk 8 之前,其内部是由数组+链表来实现的,而 jdk 8 对于链表长度超过 8 的链表将转储为红黑树(时间复杂度由O(N)降低为 O(logN))。大致的数据存储形式如下:
默认大小:
16。static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
自动扩容: (通过put方法理解)
核心方法resize(),初始化表或将表扩容两倍。代码分为两块,前半段确定新表的大小和阈值,后半段创建新表并迁移数据。
后半段迁移数据,原数据中的链表根据算法((e.hash & oldCap) == 0) 被一分为二迁到新数组中。
//put方法中插入后,判断是否需要扩容
if (++size > threshold)//threshold为扩容阈值,threshold = capacity * load factor
resize();
//扩容函数:初始化表大小或将表大小加倍
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) { // 对应数组扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 将数组大小扩大一倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 将阈值扩大一倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候
newCap = oldThr;
else {// 对应使用 new HashMap() 初始化后,第一次 put 的时候
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 用新的数组大小初始化新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 如果是初始化数组,到这里就结束了,返回 newTab 即可
if (oldTab != null) {
// 开始遍历原数组,进行数据迁移。
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 如果该数组位置上只有单个元素,那就简单了,简单迁移这个元素就可以了
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 {
// 这块是处理链表的情况,
// 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序
// loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表,代码还是比较简单的
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) {//lo链表:低位链表
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {//hi链表:高位链表
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
// 第一条链表
newTab[j] = loHead;//(e.hash & oldCap) == 0
}
if (hiTail != null) {
hiTail.next = null;
// 第二条链表的新的位置是 j + oldCap,这个很好理解
newTab[j + oldCap] = hiHead;//(e.hash & oldCap) == 1
}
}
}
}
}
return newTab;
}
核心方法:put(K key, V value)、get(K key)、remove(Object key)
put方法:
定位table位置,然后判断所在bucket的数据结构类型。链表就遍历链表,如果已存在key,则更新值,未存在插入到链表尾部;红黑树就遍历红黑树,如果已存在key,则更新值,未存在插入到树中。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 第四个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
// 第五个参数 evict 我们这里不关心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度
// 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {// 数组该位置有数据
Node<K,V> e; K k;
// 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
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) {
// 插入到链表的最后面(Java7 是插入到链表的最前面)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 8 个
// 会触发下面的 treeifyBin,也就是将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在该链表中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
break;
p = e;
}
}
// e!=null 说明存在旧值的key与要插入的key"相等"
// 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
get方法:
定位table位置,然后判断所在bucket的数据结构类型。链表就遍历链表,如果已存在key,则返回对应的值,未存在返回null;红黑树就遍历红黑树,如果已存在key,则返回对应的值,未存在返回null。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != 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;
}
remove方法:
定位table位置,然后判断所在bucket的数据结构类型。无论是链表还是红黑树,都分为两步,第一步定位找到对应的key,第二步删除对应key的节点。如果找不到,直接返回null。
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
3.2 LinkedHashMap
简介:
LinkedHashMap是HashMap的直接子类,二者唯一的区别是LinkedHashMap在HashMap的基础上,采用双向链表(doubly-linked list)的形式将所有entry连接起来,这样是为保证元素的迭代顺序跟插入顺序相同。
与HashMap的不同之处:
- 结构不同:
HashMap和双向链表的密切配合和分工合作造就了LinkedHashMap。特别需要注意的是,next用于维护HashMap各个桶中的Entry链,before、after用于维护LinkedHashMap的双向链表,虽然它们的作用对象都是Entry,但是各自分离,是两码事儿。
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
- 代码不同:
LinkedHashMap相比HashMap,新增了双向链表的头节点、尾节点、访问顺序成员变量。
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* The iteration ordering method for this linked hash map: { @code true}
* for access-order, { @code false} for insertion-order.
*
* @serial
*/
final boolean accessOrder;
如果accessOrder为true,put时将新插入的元素放入到双向链表的尾部,get时将当前访问的Entry移到双向链表的尾部。当标志位accessOrder的值为false时,put时将新插入的元素放入到双向链表的尾部,get时对当前访问的Entry不做处理。
当标志位accessOrder的值为false时,表示双向链表中的元素按照Entry插入LinkedHashMap中的先后顺序排序,即每次put到LinkedHashMap中的Entry都放在双向链表的尾部,这样遍历双向链表时,Entry的输出顺序便和插入的顺序一致,这也是默认的双向链表的存储顺序。
在HashMap中最常用的两个操作就是:put(Key,Value) 和 get(Key)。同样地,在 LinkedHashMap 中最常用的也是这两个操作。对于put(Key,Value)方法而言,LinkedHashMap完全继承了HashMap的 put(Key,Value) 方法,只是对putVal(hash,key, value, onlyIfAbsent,evict)方法所调用的afterNodeAccess方法和afterNodeInsertion方法进行了重写;对于get(Key)方法,LinkedHashMap则直接对它进行了重写。
//重写HashMap的get方法
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
//可见仅有accessOrder为true时,且访问节点不等于尾节点时,该方法才有意义。通过before/after重定向,将新访问节点链接为链表尾节点。
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
//putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
// afterNodeInsertion方法由于removeEldestEntry方法默认一直返回的false而无执行意义。也就意味着如果想要让它有意义必须重写removeEldestEntry方法。
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
* @param eldest 最老的节点(即头节点)
* @return 如果map应该删除头节点就返回true,否则返回false
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
3.3 TreeMap
底层实现为红黑树。比较复杂,参考: juejin.cn/post/684490…
4. Set
4.1 HashSet
简介:
HashSet是对HashMap的简单包装,对HashSet的函数调用都会转换成合适的HashMap方法。
4.2 LinkedHashSet
简介:
LinkedHashSet是HashSet的直接子类,二者唯一的区别是LinkedHashSet在HashSet的基础上,采用双向链表(doubly-linked list)的形式将所有entry连接起来,这样是为保证元素的迭代顺序跟插入顺序相同。
构造函数调用父类HashSet的构造方法,new了一个LinkedHashMap。
4.3 TreeSet
TreeSet底层使用NavigableMap和TreeMap来实现的。参考: juejin.cn/post/684490…
补充知识:
hash方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
满二叉树:
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
完全二叉树:
对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。