常见集合源码分析

ArrayList

底层结构

1
transient Object[] elementData; // non-private to simplify nested class access

底层是一个数组,默认大小为10,当数组容量不够时,会进行扩容,扩容大小为原来的1.5倍。

添加元素

1
2
3
4
5
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容时,会先计算新的容量大小,然后进行扩容,扩容后,会将原来的数组复制到新的数组中。

删除元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

删除元素时,会将后面的元素向前移动,然后将最后一个元素置为null,方便垃圾回收。

优点

  1. 底层是数组,查询速度快
  2. 插入和删除元素时,需要移动元素,效率较低

缺点

  1. 底层是数组,当数组容量不够时,需要扩容,扩容效率较低
  2. 删除元素时,需要移动元素,效率较低

LinkedList

底层结构

1
2
3
4
5
6
7
8
9
10
11
12
13
transient int size = 0;

/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;

/**
* Pointer to last node.
* Invariant: (first == null && last == null) || (last.next == null && last.item != null) */
transient Node<E> last;

底层是一个双向链表,每个节点都包含一个元素和两个指针,分别指向前一个节点和后一个节点。

添加元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean add(E e) {
linkLast(e);
return true;
}

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++;
}

添加元素时,会创建一个新节点,然后将最后一个节点指向新节点,如果链表为空,将第一个节点指向新节点。

删除元素

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
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}

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;
if (next == null) // 如果下一个节点为空,将最后一个节点指向前一个节点
last = prev;
else
next.prev = prev;

x.item = null; // 释放节点
x.next = null; // 释放节点
x.prev = null; // 释放节点
size--;
modCount++;
return element;
}

删除元素时,会找到要删除的节点,然后将前一个节点指向下一个节点,将下一个节点指向前一个节点,最后释放节点。

优点

  1. 底层是双向链表,插入和删除元素时,不需要移动元素,效率较高
  2. 可以在任意位置插入和删除元素

缺点

  1. 底层是双向链表,查询速度较慢
  2. 占用内存较多

HashMap

底层结构

1
transient Node<K,V>[] table;

底层是一个数组,每个数组元素是一个链表,当链表长度超过8时,会转换为红黑树。

添加元素

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
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

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 {
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;
}

添加元素时,会先计算key的hash值,然后根据hash值找到对应的数组位置,如果数组位置为空,则直接添加元素,如果数组位置不为空,则判断是否为红黑树,如果是红黑树,则调用红黑树的添加方法,如果不是红黑树,则遍历链表,找到对应的元素,如果找到了,则更新元素的值,如果没有找到,则添加元素到链表的末尾,如果链表长度超过8,则将链表转换为红黑树。

删除元素

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
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;

}

删除元素时,会先计算key的hash值,然后根据hash值找到对应的数组位置,如果数组位置为空,则直接返回null,如果数组位置不为空,则判断是否为红黑树,如果是红黑树,则调用红黑树的删除方法,如果不是红黑树,则遍历链表,找到对应的元素,如果找到了,则删除元素,如果没有找到,则直接返回null。

优点

  1. 底层是数组+链表+红黑树,查询速度较快
  2. 可以快速定位元素

缺点

  1. 底层是数组+链表+红黑树,占用内存较多
  2. 在高并发情况下,容易出现死循环问题

HashMap jdk1.7和jdk1.8的区别

  1. jdk1.7中,HashMap的底层是数组+链表,当链表长度超过8时,会转换为红黑树,而jdk1.8中,HashMap的底层是数组+链表+红黑树,当链表长度超过8时,会转换为红黑树。
  2. jdk1.7中,HashMap在扩容时,会重新计算每个元素的hash值,然后重新分配到新的数组位置,而jdk1.8中,HashMap在扩容时,会直接将原数组中的元素移动到新的数组位置,不需要重新计算hash值。
  3. jdk1.7中,HashMap在扩容时,可能会出现死循环问题,而jdk1.8中,HashMap在扩容时,不会出现死循环问题。
  4. jdk1.7中,HashMap的初始容量为16,而jdk1.8中,HashMap的初始容量为16。
  5. jdk1.7中,HashMap的负载因子为0.75,而jdk1.8中,HashMap的负载因子为0.75。
  6. jdk1.7中,HashMap的扩容方式是2的幂次方,而jdk1.8中,HashMap的扩容方式是2的幂次方。
  7. jdk1.7中,HashMap的迭代器是fail-fast的,而jdk1.8中,HashMap的迭代器不是fail-fast的。