本文介绍了作为一个androider应该掌握的集合知识, 包括集合源码分析,重点方法解读以及集合的特征.
相关集合对比
ArrayList和Vector
- Vector是线程安全的,而ArrayList线程不安全
- Vector实现了同步,所以开销要比ArrayList大,访问速度慢
- Vector扩容默认为2倍,ArrayList为1.5倍
- 实现ArrayList同步可以通过Collections.sychronizedList(arrayList)得到一个线程安全得ArrayList;或者使用CopyOnWriteArrayList
ArrayList和LinkedList
- ArrayList基于动态数组实现,LinkedList基于双向链表实现
集合解析
ArrayList
- 底层数组实现
- 默认长度为10
- 只会序列化当前有元素的数组位置,并不会将数组整体序列化
1.7与1.8中的区别
- 调用无参构造器时,1.7会创建长度为10的数组,而1.8会创建一个空数组
扩容
按照1.5倍扩容, 如当前是10, 扩容之后size变为15
如果调用addAll, 添加元素后,直接超过了1.5倍的size,则直接将size设置为添加元素后的size 如:初始size为0, 直接addAll了size为11的数组, 则ArrayList中的数组直接长度为11, 再add元素时按照1.5倍规则进行扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 重点:扩容为原来的1.5倍
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);
}
添加元素
先检测是否当前数组已经满了, 如果满了先进行扩容
- 在数组最后添加元素
public boolean add(E e) {
// 先去检测是否需要扩容,如果需要调用grow()方法进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
- 在数组指定index处添加元素
public void add(int index, E element) {
// 判断index合法性
rangeCheckForAdd(index);
// 检测是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将数组中的index以后的元素全部向后移动一位
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 将要添加的元素添加到index处
elementData[index] = element;
size++;
}
删除元素
- 按照index删除元素
public E remove(int index) {
// 判断index合法性
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
// 将index+1以后的数据向前移动一位到index
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 将最后一位元素赋空
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
- 按照存储元素进行删除
public boolean remove(Object o) {
// 检测是否当前要删除的元素是空的
// 如果元素为null,遍历数组将第一个null元素删除
// 如果元素不为null,遍历数组将对应元素删除
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
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
}
Fail-fast机制
modCount用来记录ArrayList结构变化次数,添加或者删除元素,或者调整内部数组的大小都会导致结构变化,在进行序列化或者迭代过程需要比较操作前后modCount是否变化,如果变化会抛出ConcurrentModificationException
Vector
与ArrayList类似,区别在于使用synchronized实现同步, 在remove,add,get的时候都会通过synchronized同步
扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// capacityIncrement 是构造函数中传入的扩容值,如果不传入默认为0,则将之前容量扩容2倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
CopyOnWriteArrayList
- 写操作是通过在一个复制的数组上进行,读操作在原数组中进行
- 写操作需要加锁,防止并发写入出现数据丢失情况
- 写操作结束之后将原始数据指向新的复制数组
- remove和add场景都通过对象锁进行保证同步, get时直接获取,未实现同步
适用场景
- 写的同时可以进行读取操作,大大提高了读取的性能,使用读多写少场景
缺陷
- 内存占用过高 写操作需要复制新数组,是内存占用为原来的两倍
- 数据不一致 读操作不能实时读取数据,存在写入操作未同步到数组中的情况
get
private E get(Object[] a, int index) {
return (E) a[index];
}
remove
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
add
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
LinkedList
LinkedList实际上就是一个双向链表,可以实现队列,栈等功能,具体操作都是基于双向链表节点操作,相对简单
会记录首尾节点
add
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
// 直接添加到链表末尾
linkLast(element);
else
// 获取到当前index的node节点,用于插入元素
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
// 添加新节点
final Node<E> newNode = new Node<>(pred, e, succ);
// 更新后节点
succ.prev = newNode;
// 更新前节点
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
remove
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;
x.prev = null;
}
// 更新后节点
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
// 将当前节点置空
x.item = null;
size--;
modCount++;
return element;
}
get
获取的时候会进行一个简单的优化, 根据index和中心点进行比较, 从头或尾开始遍历查找
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
HashMap
详细解读,请参照 烟雨星空大神的HashMap底层原理
HashMap 数据结构
- 1.7中是以数组 + 链表形式存储
- 1.8中是以数组 + 链表(或者红黑树)存储
重要参数
- 容量 capacity
- 必须是2的n次幂,如果传入了不是2的n次幂的容量,则会通过tableSizeFor()方法转换成大于容量的最小2的n次幂,如传入14,容量会被设置成16
- 最大容量: 1<<30,即2^32
- 实际存放元素个数 size
- 数组中元素个数
- 数组扩容阈值 threshold
- 数组元素size达到阈值,进行resize(),将容量扩大
- 默认负载因子 loadFactor
- 0.75
负载因子选取0.75,是对空间和时间的权衡,如果太小,空间利用率低,导致频繁扩容;如果太大,hash冲突概率增高,影响效率,增加操作元素时间
- 链表与红黑树转换阈值
- 只有当数组容量达到64时,才会出现红黑树结构
- 当链表长度达到8,链表会转换成红黑树
- 当红黑树元素减少到6个时,会退化成链表
- fail-fast机制保障 modCount
hash()
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
- 通过key的hash值与hash值右移16位异或,保证了高16位的特性,同时将高16位也添加到计算当中,降低hash冲突概率
- 异或运算预期值(0或1)的概率比为1:1
tableSizeFor()
该方法确保了map的容量必须是2的n次幂,实现算法是将n-1,然后将n-1 分别无符号右移1,2,4,8,16 位并与n的每一位相或确保,所有位数都能变成1,然后再 + 1就能得到大于当前值的最小2的n次幂
该保证了保证了当前容量首位后的值均为1,再加一,从而得到了翻倍的效果
put() 添加元素
- 判断数组是否为空,如果是空则进行resize()扩容
- 通过 (n-1)&hash确认元素在数组中bucket的位置
- 如果当前数组中为null,表示没有元素,则直接将值添加到该位置
- 如果当前数组中元素不是null
- 如果当前hash值与当前元素相同,则将当前元素覆盖
- 如果当前hash值不同,并且是红黑树结构,则将值添加到红黑树中
- 如果当前hash值不同,并且非红黑树结构,即是链表,则便利链表,采用尾插法(JDK 1.8)将值添加到链表尾部
- 在插入链表过程中,如果链表长度超过8,则将当前链表转换成红黑树
- 如果发生了碰撞,需要替换新值,则将旧值返回。方法结束
- fail-fast机制,++modCount
- 检测当前元素个数是否超过阈值,需要扩容
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;
// 如果map为null, 则直接扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 当前数组的位置为null, 则直接将元素添加到数组上
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 当前数组中的元素的元素与当前元素key相同, 进行替换value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果是树节点, 则将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);
// 如果当前元素大于64个, 并且当前链表超过了7个,进行树化
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;
}
resize() 扩容
- 扩容将之前的容量左移一位,达到2倍之前容量的数组,
- 判断之前的数组是否为null,如果非null,说明之前存在元素,需要进行重新分配元素
- 遍历旧数组
- 如果当前数组元素非null,且元素的next节点为null,说明只有一个元素,则重新hash()和新数组容量取模,得到新下标位置即可
- 否则如果当前是红黑树结构,则拆分红黑树,必要时可能会退化成链表
- 否则说明当前元素是链表,将链表拆分,通过 (e.hash & oldCap) == 0 算法将链表元素分配到当前下标或者(当前下标+老数组容量)的位置
(e.hash & oldCap) == 0 算法的核心就是,扩容之后,确定下标的hash值不变,但是由于扩容2倍原因,导致,参与计算的位数增加一位,为老数组容量的位置,所以,只需要判断老数组容量的位置如果是0,保持原位,为1将元素移动到(当前下标+老数组容量)位置
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;
}
// 正常非空场景, 左移一位进行容量 * 2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 初始情况, 并且设置了容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 什么也没有设置
else { // zero initial threshold signifies using defaults
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;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// map不为空
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)
// 根据当前hash直接将元素设置为当前位置或者当前位置+ oldCap
newTab[e.hash & (newCap - 1)] = e;
// 树节点处理
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 链表一分为二
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) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 放在原位值 + oldCap数组节点
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
get() 获取元素
- 检测数组不为null,并且当前下标元素不为null
- 检测hash值是否与第一个元素相同
- 如果不同,检测是否是红黑树,遍历红黑树
- 如果是链表,遍历找到链表结尾
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;
}
LinkedHashMap
- 实质上与HashMap原理相同,额外的在HashMap的基础上添加了一个双链表来维护有序性
- 节点除了next指向链表中的下一个节点外,额外添加了before,after节点,用于维护双链表中顺序
- 有两种排序方法:HashMap中有accessOrder来标记排序顺序
- 默认为false,表示维护插入顺序
- true表示按照最近使用情况排序,即LRU算法,最近使用的放在链表尾部,最久未使用在链表头部
重点方法
实现思路与HashMap基本相同,我们重点看维护双向链表的方法,无外乎就是如何添加,删除,更新节点在数组中的位置
put,添加方法
LinkedHashMap中并未实现put方法,也就是说他使用的是他父类HashMap中的put(), 但是此时又分成两种情况,map中是否已经存在该节点,即要添加的节点,是需要新增还是更新即可,新增指的是bucket桶中并无元素或者bucket桶中有元素,但是当前桶中链表不存在该元素。
- 新增节点情况
// HashMap#put()
if ((p = tab[i = (n - 1) & hash]) == null)
// 我们来看一下在添加一个map中没有的数据时会创建新节点,LinkedHashMap重写了newNode()方法
tab[i] = newNode(hash, key, value, null);
// LinkedHashMap#newNode(yuansu)
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;
}
// LinkedHashMap#linkNodeLast()
// 我们可以很清晰的看出来他就是在操作双向链表,将添加的元素放在尾部
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
- 更新节点情况 我们依旧从put方法下手,
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
...
// e正常来讲已经添加到map中, 所以一定会不为null
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;
}
这里面我们看到有两个方法,第一个是 afterNodeAccess() 一个是afterNodeInsertion(), 我们先一个一个看
- afterNodeAccess HashMap中默认为空方法,LinkedHashMap中实现了该方法
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;
}
}
- 简单总结来说就是
- 如果使用的是LRU算法,则需要更新双向链表中的节点位置,即将当前节点的前后节点相连(相当于删除当前节点),并将当前节点添加到尾部;
- 如果使用的是默认插入算法,则什么也不需要操作
- afterNodeInsertion()
// 同样, 该方法也是LinkedHashMap中实现的HashMap的空方法, 但是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);
}
}
get()
public V get(Object key) {
Node<K,V> e;
// getNode方法就是HashMap中获取节点方法,不赘述
if ((e = getNode(hash(key), key)) == null)
return null;
// 如果是LRU排序,则调用上述说的afterNodeAccess方法, 进行更新双向链表;如果是默认插入顺序,则什么也不做
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
remove()
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 (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;
}
// afterNodeRemoval() 就是将当前节点在双向链表中的前后节点相连,即删除当前节点。
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
Hashtable
操作时,会通过synchronized 保证同步
hash
hash计算的方法通过hash后和当前数组取余
(hash & 0x7FFFFFFF) % tab.length
扩容
- 容量设置为之前的2n + 1
- 调整容量之后, 重置原数组位置时, 每次添加元素是添加到链表的头部