常用集合
先上一张xmind覆盖常用集合的知识点:
源码分析
1. ArrayList
- 初始化:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//无参数直接初始化,数组大小为空
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//指定初始数据初始化
public ArrayList(Collection<? extends E> c) {
//elementData 是保存数组的容器,默认为 null
elementData = c.toArray();
//如果给定的集合(c)数据有值
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//如果集合元素类型不是 Object 类型,我们会转成 Object
if (elementData.getClass() != Object[].class) {
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
} else {
// 给定集合(c)无值,则默认空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
- 新增元素:
public boolean add(E e) {
//确保数组大小是否足够,不够执行扩容
ensureCapacityInternal(size + 1);
//直接赋值,故线程不安全
elementData[size++] = e;
return true;
}
- 扩容:
private void ensureCapacityInternal(int minCapacity) {
//有给定初始值以给定的大小为准
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//确保容积足够
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//如果我们期望的最小容量大于目前数组的长度,那么就扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//扩容,并做现有数据拷贝
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容后的值 < 我们的期望值,则提到期望值
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果扩容后的值 > jvm所能分配空间的最大值,则卡上限
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
- 删除:
public boolean remove(Object o) {
//如果要删除的值是null,找到第一个值是null的删除
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//如果要删除的值不为null,找到第一个和要删除的值相等的删除
for (int index = 0; index < size; index++)
//这里是根据equals来判断值相等的,相等后再根据索引位置进行删除
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
// umMoved表示删除index元素后,需要从index后移动多少个元素到前面去
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
//数组最后一个位置赋值 null,帮助GC
elementData[--size] = null;
}
- 迭代器:
public boolean hasNext() {
//cursor表示下一个元素的位置,size 表示实际大小
return cursor != size;
}
public E next() {
//迭代过程中判断版本号有无被修改,有被修改则抛出ConcurrentModificationException异常
checkForComodification();
//本次迭代过程中,元素的索引位置
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 下一次迭代时,元素的位置,为下一次迭代做准备
cursor = i + 1;
// 返回元素值
return (E) elementData[lastRet = i];
}
//版本号比较
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
public void remove() {
//如果上一次操作时,数组的位置已经小于0则说明数组已经被删除完了
if (lastRet < 0)
throw new IllegalStateException();
//迭代过程中,判断版本号有无被修改
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
//-1表示元素已经被删除,这里也防止重复删除
lastRet = -1;
//删除元素时modCount的值已经改变,在此赋值给expectedModCoun,保证两者数据一直
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
2. LinkedList
- 新增:
// 从尾部开始追加节点
void linkLast(E e) {
final Node<E> l = last;
//l是新节点的前一个节点,当前值是尾节点值
// e 表示当前新增节点,当前新增节点后一个节点是 null
final Node<E> newNode = new Node<>(l, e, null);
// 新建节点追加到尾部
last = newNode;
//如果链表为空则新建的节点
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
- 删除:
private E unlinkFirst(Node<E> f) {
//拿出头节点返回
final E element = f.item;
final Node<E> next = f.next;
//帮助GC回收头节点
f.item = null;
f.next = null;
//头节点的下一个节点成为头节点
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
- 查询
//根据链表索引位置查询节点
Node<E> node(int index) {
//我们发现这里根据索引位置用了一次二分
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
3. HashMap
- 新增:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
//n表示数组的长度,i 为数组索引下标
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
//如果数组为空,使用 resize 方法初始化
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 {
//hash冲突解决
Node<K, V> e;
K k;
//如果 key的hash和值都相等,直接把当前下标位置的 Node 值赋值给临时变量
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) {
//p.next == null时p到了链表尾节点
if ((e = p.next) == null) {
//把新节点放到链表的尾部
p.next = newNode(hash, key, value, null);
// 当链表的长度>=8时,链表转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) { treeifyBin(tab, hash);
}
break;
}
//现有元素和新增的元素相等,结束循环
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
break;
}
p = e;
}
}
//说明新节点的新增位置已经找到了
if (e != null) {
V oldValue = e.value;
//当onlyIfAbsent 为 false 时,才会覆盖值
if (!onlyIfAbsent || oldValue == null) { e.value = value; }
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果HashMap的大小超过了扩容阈值,开始扩容
if (++size > threshold) {
resize();
}
afterNodeInsertion(evict);
return null;
}
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
TreeNode<K,V> root = (parent != null) ? root() : this;
//自旋
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
// p hash 值大于 h,说明 p 在 h 的右边
if ((ph = p.hash) > h)
dir = -1;
// p hash 值小于 h,说明 p 在 h 的左边
else if (ph < h)
dir = 1;
//要放进去key在当前树中已经存在了(equals来判断)
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//自己实现的Comparable的话,则需要用compareTo
else if ((kc == null &&
//得到key的Class类型,如果key没有实现Comparable就是null
(kc = comparableClassFor(k)) == null) ||
//当前节点pk和入参k不等
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
//找到和当前hashcode值相近的节点
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//把新节点放在当前子节点为空的位置上
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//当前节点和新节点建立父子关系
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
//moveRootToFront方法把算出来的root放到根节点上
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
4. LinkedHashMap
- 新增:
//新增节点追加到链表的尾部
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
// 追加到链表的尾部
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
//last为空说明链表为空,首尾节点相同
if (last == null)
head = p;
else {
//链表不空建立节点前后关系
p.before = last;
last.after = p;
}
}
- LRU策略:
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
// 如果设置了LRU策略,则把当前 key 移动到队尾
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
//删除很少被访问的元素,被 HashMap 的 put 方法所调用
void afterNodeInsertion(boolean evict) {
//得到元素头节点
LinkedHashMap.Entry<K,V> first;
//removeEldestEntry来控制删除策略,如果队列不为空且允许删除时删除头节点
if (evict && (first = head) != null
&& removeEldestEntry(first)) {
K key = first.key;
//removeNode删除头节点
removeNode(hash(key), key, null, false, true);
}
}
总结
- 基础集合均是线程不安全的,故多线程时需要切换到线程安全的数据结构
- HashMap在JDK 8以后底层相当于重写了,能够看出设计者希望在性能和易用这两点上寻求balance
- 红黑树5个特性:根黑、叶子黑、节点非黑即红、红父子黑、节点子树路径黑节点数量相等,在update节点后会通过自旋来维护这5个特性,本质是为了保证树的平衡性进而保证查询所需次数和树深度基本一致让均摊时间复杂度保持在log(N)
- 为什么我们需要红黑树:因为二叉查找树可能会退化成链表,平衡二叉树(AVL,每个节点的左子树和右子树的高度差<=1)可以解决二叉查找树问题但update时rebalance过于频繁,故折衷下设计了红黑树折衷不严格的平衡树
- Map的hash算法设计尽可能的在减少碰撞,但终归只是个trick,必须要设计一套严格的冲突解决方案如链地址法,日常设计技术方案也需如此