Java 集合框架中的 List
List
本文代码基于 openJDK 18
List
接口继承自 Collection
, 除了继承了 Collection
中的能力,自身拓展了几个默认方法:
// 批量修改操作
default void replaceAll(UnaryOperator<E> operator)
// 排序,使用的是 Arrays.sort
default void sort(Comparator<? super E> c)
// 位置访问操作
E get(int index);
E set(int index, E element);
void add(int index, E element); // Collection 有 add ,这里拓展了指定 index 。
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
// 迭代器
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
// 容器
List<E> subList(int fromIndex, int toIndex);
这里简单介绍一下,List
相较于 Collection
拓展的功能:
- 批量操作增加排序,说明
List
是有序的。 - 位置访问操作,说明
List
是可以进行位置索引的。 - 容器支持了子
List
,可以对List
进行截取。
List 接口的直接实现包括:ArrayList、Vector、LinkedList、CopyOnWriteArrayList。
ArrayList
ArrayList 是一个动态数组,支持随机访问。允许存储包括 null
的元素。内部的数据结构是数组。除了实现 List
接口外,还提供了一些方法用来处理数组扩容的方法。
它与 Vector
的区别在于,Vector
是同步的,所有操作方法都加了 synchronized
修饰。而 ArrayList
没有。
继承关系
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
内部数据结构
transient Object[] elementData;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
初始化时,根据 initialCapacity
参数创建大小,默认容量时 0 。
真正保存数据的是 elementData
,这是一个对象数组。
添加数据是顺序的在数组尾部添加新元素,当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。
扩容逻辑
扩容来源于添加操作:
// 插入式的添加
public void add(int index, E element) {
rangeCheckForAdd(index); // 确保 index 在 0 到 size 范围内
modCount++; // 增加操作计数
final int s; // 数组当前容量
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
elementData = grow(); // 扩容
System.arraycopy(elementData, index,
elementData, index + 1,
s - index); // 复制数组
elementData[index] = element;
size = s + 1;
}
// 普通的添加方式,添加到数组尾部
public boolean add(E e) {
modCount++; // 操作次数增加
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length) elementData = grow();
elementData[s] = e;
size = s + 1;
}
扩容逻辑在 grow()
中:
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length; // 原来的容量
// 原来容量大于 0,或数组不是默认数组时,计算新的容量
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* 最小容量 */
oldCapacity >> 1 /* preferred growth */);
// 返回复制好的数组
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
// 默认构造方法的情况,返回一个默认容量为 10 的数组
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
扩容逻辑是,如果是默认构造参数,即 ArrayList()
创建的对象,默认数组容量为 DEFAULT_CAPACITY ,值为 10;
如果原来的数组容量大于 0,通过 ArraysSupport.newLength(oldLength, minGrowth, prefGrowth)
来计算新的容量。
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
int prefLength = oldLength + Math.max(minGrowth, prefGrowth);
return 0 < prefLength && prefLength <= 2147483639 ? prefLength : hugeLength(oldLength, minGrowth);
}
该方法三个参数分别是:
oldLength
:原始数组的长度,即当前数组的长度。minGrowth
:最小增长值,表示数组的最小增长空间。如果数组需要扩容,至少会增长minGrowth
个空间。prefGrowth
:优先增长值,表示数组的优先增长空间。数组需要扩容时,会优先增长prefGrowth
个空间。
首先,通过 prefLength = oldLength + Math.max(minGrowth, prefGrowth);
计算得到一个可能的新长度 prefLength
,其中取 minGrowth
和 prefGrowth
的较大值,以确保新长度至少增长 minGrowth
个空间。
这里的扩容计算为,新容量 = 原始容量 + 最小增长量和原始容量的一半两者的取较大一方的值,一般情况下是 1.5 倍原容量扩容。
接下来,判断 prefLength
是否在一定范围内。检查新长度是否大于 0 且小于等于 Integer.MAX_VALUE
。
如果新长度满足上述条件,则返回 prefLength
作为新的数组长度。否则,调用 hugeLength(oldLength, minGrowth)
方法,该方法将返回一个巨大的容量数(2147483639),如果最小容量仍大于这个数,则返回最大数量,另外就是对 minLength 为负数的情况做了限制。
private static int hugeLength(int oldLength, int minGrowth) {
int minLength = oldLength + minGrowth;
if (minLength < 0) {
throw new OutOfMemoryError("Required array length " + oldLength + " + " + minGrowth + " is too large");
} else {
return minLength <= 2147483639 ? 2147483639 : minLength;
}
}
手动节省容量
ArrayList 还对外提供了一个节省内存空间的自动调整容量方法,但 ArrayLsit 内部并没有自己调用:
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
该方法将此 ArrayLis t实例的容量修剪为列表的当前大小。
Fail-Fast机制
ArrayList
也采用了快速失败的机制,通过记录 modCount
参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。Fast-Fail 机制本身并不能保证线程安全。
Vector
继承关系
与 ArrayList 完全一致:
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
内部数据结构
protected Object[] elementData;
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector() {
this(10);
}
内部数据结构与 ArrayList 相同,默认构造方法设置的容量也是 10。
扩容逻辑
也是从 add 操作入手:
public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
elementCount = s + 1;
}
同样来自于 grow 系列方法:
private Object[] grow() {
return grow(elementCount + 1);
}
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);
}
扩容逻辑与 ArrayList 基本一致,但推荐扩容为 capacityIncrement, capacityIncrement 在构造方法中可以设置:
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
默认是 0,也就是说默认情况下,扩容是原容量 2 倍扩容(ArrayList 是 1.5 倍)。
与 ArrayList 的区别
最大的区别是 Vector 的操作都加上了 synchronized
关键字:
public synchronized boolean add(E e)
public synchronized E get(int index)
public synchronized E set(int index, E element)
所以 Vector 是线程安全的,而 ArrayList 无法保证线程安全。
另一个区别就是默认扩容量不同:Vector 默认是 2 倍,ArrayList 默认是 1.5 倍。
Stack
Vector 有一个子类 Stack,也就是栈结构类型。表示对象的后进先出堆栈。Stack
继承自 Vector
,并拓展了五个允许将容器视为栈结构的操作。 包括常见的 pop
和 push
操作、以及查看栈顶元素的方法、检查栈是否为空的方法以及从栈顶向下进行搜索某个元素,并获取该元素在栈内深度的方法。
但 JDK 更推荐使用 Deque 来实现栈能力。Deque 接口及其实现提供了一组更完整的 LIFO 堆栈操作能力,应该优先考虑使用 Deque 及其实现。例如:
Deque<Integer> stack = new ArrayDeque<Integer>();
LinkedList
基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
继承关系
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
AbstractSequentialList
是List
接口的骨架实现(Skeletal Implementation),它提供了一种最小化实现List
接口所需的努力,可以用作"顺序访问"数据存储(如链表)的后备支持。对于随机访问数据(如数组),应该优先使用AbstractList
而不是这个类。- Deque:双向队列,支持从两端对元素进行插入和移除,后续在 Queue 接口体系详细说明。
- Cloneable:对象复制。
- Serializable:序列化能力。
内部数据结构
transient Node<E> first;
transient Node<E> last;
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
通过 first
和 last
表示双向链表的两个方向的头节点。Node
的数据结构是:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
添加操作
有两种添加方式,一种是添加到链表尾部,另一种是添加到指定位置
public boolean add(E e) {
linkLast(e);
return true;
}
// 将指定的元素插入到列表的指定位置。将当前该位置上的元素(如果有的话)和任何后续的元素都向右移动(索引加一)。
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
linkLast
这里用到了 linkLast 方法将元素添加到链表尾部:
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++;
}
linkBefore
插入到指定位置的添加操作:
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++;
}
这段代码用于在一个链表中的指定节点 succ
之前插入一个新的节点,新节点的元素值为 e
。它会创建一个新的节点 newNode
,然后将 newNode
插入到 pred
和 succ
之间,修改它们之间的链接关系,使得新节点成为 pred
的后继节点,同时也成为 succ
的前驱节点。
在插入新节点后,还会更新列表的大小 size
和修改计数器 modCount
,以确保列表的状态正确并支持快速失败机制。
这里的 succ
参数通过 node(int index)
而来:
// in add()
linkBefore(element, node(index));
查询操作
node 方法中,根据 index 判断是在链表的前半部分还是后半部分,然后在从链表头部或尾部去遍历,提高效率:
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;
}
}
所以链表操作的时间复杂度基本上就是这个方法的时间复杂度 O(size >> 1)
。
删除操作
LinkedList 的 remove 操作有很多,对于链表头部和尾部的删除是 removeFirst 和 removeLast:
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
来自 Deque 的 remove 方法实现:
public E remove() {
return removeFirst();
}
根据 index 删除的方法:
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
根据对象删除的方法:
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
这些删除方法,都有关于链表的操作方法。
删除链表尾部
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
删除链表头部
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
删除链表中的指定元素
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;
}
线程安全
LinkedList 的所有操作都没有线程安全相关的逻辑,所以也是无法保证线程安全的。但 LinkedList 的操作类方法都有 modCount 计数,支持 Fast-Fail ,确保并发操作时避免对不一致的数据进行操作。
Java的快速失败(fast-fail)机制是一种在集合类上用于检测并发修改的机制。
快速失败机制的实现是通过在每次对集合进行迭代或修改操作时,都会检查集合的修改次数(modCount)。集合的 modCount 是一个计数器,用于记录对集合进行修改的次数。当一个迭代器或者一个线程开始遍历集合时,会记录当前集合的 modCount 值。当接下来发生集合的修改(添加、删除等)时,modCount 值会增加。如果在迭代或访问过程中,发现集合的 modCount 值与记录的 modCount 值不一致,就会抛出 ConcurrentModificationException 异常,以提醒程序有并发修改操作发生,进而避免对不一致的数据继续操作。
CopyOnWriteArrayList
CopyOnWriteArrayList 并不是 Java 集合框架中的成员,而是来自于 java.util.concurrent
框架,即 Java 并发框架,所以自然具备保证线程安全的能力。
继承关系
线程安全
/** 保护所有变量的锁 */
final transient Object lock = new Object();
/** 内部的数据结构,只能通过getArray/setArray访问。 */
private transient volatile Object[] array;
通过 lock 对象配合 synchronized
来提供加锁能力;通过 transient 确保数据不会被序列化;通过 volatile 关键字确保写操作会立即同步到主内存,确保多线程的可见性。另外就是禁止指令重排序。
这里使用 lock 对象配合
synchronized
关键字而不是ReentrantLock
,是因为这里保护所有可变操作的锁。(在两者都可以使用时,我们稍微偏向于使用内置监视器而不是ReentrantLock。)早些版本的 JDK 是 ReentrantLock ,openJDK 18 使用 lock 对象配合
synchronized
。
将
array
声明为volatile
可能是因为该变量在多线程环境下会被频繁地读写,并且多个线程之间需要及时地获取到array
的最新值。使用volatile
关键字可以确保线程之间对array
的修改和读取操作是同步的,避免了因为线程缓存导致的数据不一致问题。需要注意的是,
volatile
关键字仅仅保证了可见性和禁止指令重排序,但并不能保证对array
的复合操作(例如读取-修改-写入)是原子性的。如果需要保证复合操作的原子性,需要使用其他同步机制,例如使用锁(synchronized
)或者并发集合类来确保线程安全。
底层数据结构
/** 内部的数据结构,只能通过getArray/setArray访问。 */
private transient volatile Object[] array;
初始化和构造方法
// 创建一个空列表
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
// 创建一个包含集合元素的列表
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] es;
if (c.getClass() == CopyOnWriteArrayList.class)
es = ((CopyOnWriteArrayList<?>)c).getArray();
else {
es = c.toArray();
if (c.getClass() != java.util.ArrayList.class)
es = Arrays.copyOf(es, es.length, Object[].class);
}
setArray(es);
}
// copy 给定的数组创建列表
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
构造方法中最后都调用了 setArray()
方法,将数组保存到了属性 array
中。
添加操作
直接添加元素到数组尾部:
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
synchronized 确保同步,直接通过 Arrays.copyOf(int[], int)
复制一份容量 + 1 的新数组。
添加到指定位置:
public void add(int index, E element) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException(outOfBounds(index, len));
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(es, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
}
}
synchronized 确保同步,本质也是通过 System.arraycopy
系列方法将原来的数组拆成两份然后拼接到 newElements 中,最后通过 setArray 更新底层数据结构。
因为是多线程,所以提供了额外的添加方法,来检查数组中是否已经存在某个元素,如果数组中不存在,则添加;否则,不添加,直接返回,可以保证多线程环境下不会重复添加元素。
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOfRange(e, snapshot, 0, snapshot.length) < 0
&& addIfAbsent(e, snapshot);
}
private boolean addIfAbsent(E e, Object[] snapshot) {
synchronized (lock) {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i]
&& Objects.equals(e, current[i]))
return false;
if (indexOfRange(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
}
}
删除操作
public E remove(int index) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
E oldValue = elementAt(es, index);
int numMoved = len - index - 1;
Object[] newElements;
if (numMoved == 0)
newElements = Arrays.copyOf(es, len - 1);
else {
newElements = new Object[len - 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index + 1, newElements, index,
numMoved);
}
setArray(newElements);
return oldValue;
}
}
也是通过 System.arraycopy
来重新组成数组的。
查询操作
public E get(int index) {
return elementAt(getArray(), index);
}
static <E> E elementAt(Object[] a, int index) {
return (E) a[index];
}
因为底层时数组,所以直接索引。
扩容逻辑
从添加和删除操作可以看出,CopyOnWriteArrayList 都是直接更新容量,然后通过 System.arraycopy
复制,所以扩容每次都是 +1 / -1 。
总结
List 接口下的实现包括:ArrayList、Vector、LinkedList 和 CopyOnWriteArrayList 。它们之间有以下区别:
List 实现 | 底层数据结构 | 扩容机制 | 线程安全 |
---|---|---|---|
ArrayList | 数组 | 每次 1.5 倍扩容 | 无法保证线程安全 |
Vector | 数组 | 每次 2 倍扩容 | sychronized 关键字修饰方法 |
LinkedList | 双向链表 | 链表无需扩容 | 无法保证线程安全 |
CopyOnWriteArrayList | 数组 | 每次根据添加/删除数量扩容 | Object 对象配合 sychronized 代码块 |
转载自:https://juejin.cn/post/7261783831056826423