前言
在JCF(Java Collection Frame)中List集合是最常用的集合之一,List集合是一个元素有序,可重复的集合,可以通过集合中顺序索引操作集合中的元素。List集合主要包括4个接口,2个抽象类以及4个具体实现类,类关系如下图所示。
接口简介
Iterable接口
Iterable是集合框架的顶层接口,在List、Set、Queue集合中都有被用到。实现了Iterable接口的集合框架支持for-each循环语句。
public interface Iterable<T> {
// 获取集合迭代器类
Iterator<T> iterator();
// 1.8开始支持for-each语法
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
Collection接口
Collection接口是JCF中一个很重要的接口,这个接口没有直接实现类。集合是按照某种逻辑结构存储数据,它的实现类中的数据结构可以是链表,数组,哈希表,树等。
public interface Collection<E> extends Iterable<E> {
// 返回集合中元素的个数
int size();
// 判断集合中是否包含某个元素
boolean contains(Object o);
// 向集合中添加元素
boolean add(E e);
// 删除集合中的元素
boolean remove(Object o);
// 判断集合中是否包括目标集合中的所有元素
boolean containsAll(Collection<?> c);
// 将指定集合中的对象添加到当前集合
boolean addAll(Collection<? extends E> c);
// 删除指定集合中不存在的对象
boolean removeAll(Collection<?> c);
// 删除满足某个条件的对象
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
// 仅保留目标集合中存在的对象,删除目标集合中不存在的对象
boolean retainAll(Collection<?> c);
// 删除集合中的所有元素
void clear();
// 逐个比较集合中对象,如果两个集合中的对象是否equals,并返回结果
boolean equals(Object o);
// 生成
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
AbstractList抽象类
AbstractList抽象类时List接口的实现,它提供了随机访问数据结构(如数组)的一个实现的模板,对于顺序访问数据(如链表),优先使用AbstractSequentialList,AbstractSequentialList是AbstractList的子类。
LinkedList介绍
LinkedList集合底层数据结构是一个双向链表,它不支持随机访问,如果要查找链表中某个索引的数据对象,只能从头部或者尾部依次查询。如果是添加、查看链表头/尾节点,时间复杂度为O(1),查看或者修改链表中间节点,则最差情况时间复杂度为O(n)
LinkedList只有三个属性,包括链表中元素数量,first节点和last节点,鉴于链表的特性,LinkedList在创建时不需要设置默认容量。
// LinkedList成员变量
// 链表中元素数量
transient int size = 0;
// first节点
transient Node<E> first;
// last节点
transient Node<E> last;
下面我们主要分析下LinkedList中add节点和remove节点的过程,LinkedList#add源码如下
public void add(int index, E element) {
// 校验index范围,0≤index≤size
checkPositionIndex(index);
// 如果index==size,则是在链表尾部添加元素
if (index == size)
linkLast(element);
else
// 根据index查找到某个节点,并调用linkBefore方法添加节点
linkBefore(element, node(index));
}
// 查找链表第index个node
Node<E> node(int index) {
// 如果查找的index小于size的1/2,则从first节点往后找
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;
}
}
在某个节点之前添加节点代码如下
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev;
// 创建node节点,设置当前节点的pred节点为原succ节点的pred节点,next节点为succ节点
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
// 如果pred节点==null,说明succ节点是first节点,则把新节点设置为first节点即可
if (pred == null)
first = newNode;
else
// succ节点不是first节点,则修改prev节点的next
pred.next = newNode;
size++; // 修改链表size
modCount++;
}
创建节点过程如下,主要分为两步,第一步先创建节点,同时设置新节点的prev节点和next节点,第二步再修改原prev节点的next和succ节点的prev。
LinkedList#remove过程如下,首先先校验remove的index是否0≤index≤size,然后将当前节点unlink
public E remove(int index) {
checkElementIndex(index);
// node方法参考add里的分析,查找链表中第index个节点
return unlink(node(index));
}
E unlink(Node<E> x) {
// 当前节点
final E element = x.item;
// 当前节点的后继节点
final Node<E> next = x.next;
// 当前节点的前置节点
final Node<E> prev = x.prev;
if (prev == null) {
// 如果prev节点为空,说明当前节点是first节点,则将first节点置为next节点
first = next;
} else {
// 将prev节点的next节点置为next节点
prev.next = next;
x.prev = null;
}
// 如果next节点为空,说明当前节点是last节点,则将last节点置为next节点
if (next == null) {
last = prev;
} else {
// 将next节点的prev节点置为prev节点
next.prev = prev;
x.next = null;
}
x.item = null;
size--; // 修改链表size
modCount++;
return element;
}
ArrayList介绍
ArrayList集合本质上是一个动态数组,数组的长度可以根据数组内元素的个数动态变化,它和LinkedList相比,随机访问节点的时间复杂度是O(1),而LinkedList随机访问节点的时间复杂度为O(n)。LinkedList对单个元素的操作时间复杂度是O(1),由于ArrayList操作单个元素可能会导致数组元素的移动,最差的时间复杂度为O(n)。
构建ArrayList时,默认是空数组,只有添加元素时才会初始化一个默认长度的数组
public ArrayList() {
// DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值为{}
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
ArrayList的属性只有两个,一个是存数据的数组,另一个是当前数组中存放元素的size
// 数组
transient Object[] elementData;
// 数组中存放元素的个数
private int size;
注意: 这里的size不是elementData的length,size是elementData存放元素的数量
ArrayList在指定位置添加元素方法如下
public void add(int index, E element) {
// 校验0≤index≤size,如果超出范围则抛出IndexOutOfBoundsException
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
// 如果当前存放元素的个数已经为elementData的length,需要扩容后才能添加元素
if ((s = size) == (elementData = this.elementData).length)
// elementData扩容
elementData = grow();
// index之后的元素拷贝到索引位置+1的位置,也就是向后移动一位
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
// 将当前元素设置到索引为index的位置上
elementData[index] = element;
size = s + 1;
}
数组扩容默认扩容1.5倍,元素扩容大小会取minCapacity和1.5倍旧容量的更大值
private int newCapacity(int minCapacity) {
int oldCapacity = elementData.length;
// 新容量为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 新容量-最小容量为负数,有两种情况
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// 说明是初始化数组,数组容量为minCapacity和默认容量(10)的更大的那个值
return Math.max(DEFAULT_CAPACITY, minCapacity);
// minCapacity<0,说明扩容后1.5倍已经超过了Integer.MAX_VALUE,数据溢出
if (minCapacity < 0)
throw new OutOfMemoryError();
// 1.5倍新容量小于minCapacity,则取minCapacity
return minCapacity;
}
// 如果1.5倍新容量小于Integer.MAX_VALUE-8,则数组长度为newCapacity
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
数组最大长度MAX_ARRAY_SIZE为Integer.MAX_VALUE-8。它是要分配的数组的最大大小(除非必要)。某些 VM 会在数组中保留一些标头字。尝试分配更大的阵列可能会导致内存不足错误:请求的阵列大小超过 VM 限制
ArrayList删除元素的方法,ArrayList#remove底层调用了fastRemove方法,fastRemove方法中的代码相对来说比较简单,将被删除元素后面的元素往前移动一位。
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
// 将i之后的元素往前移动一个位置
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
ArrayList的扩缩容
当ArrayList添加元素时,如果ArrayList中数组容量不足,ArrayList会默认扩容1.5倍。
当ArrayList删除元素时,ArrayList没有缩容机制,如果需要缩小数组容量,只能手动调用trimToSize()
Vector介绍
Vector是Java早期版本提供的List集合,它的底层也是数组,因此Vector也是支持随机访问的。虽然它底层的数据结构与ArrayList相同,Vector与ArrayList相比,最大的差异是Vector是线程安全的,ArrayList不是线程安全的。
我们来看一下Vector中的add方法,add方法调用了insertElementAt方法,insertElementAt方法是一个同步方法,Vector中的其他方法也都被synchronized修饰,Vector是一个线程安全的集合。
public void add(int index, E element) {
insertElementAt(element, index);
}
public synchronized void insertElementAt(E obj, int index) {
// 省略部分代码,与ArrayList中的源码类似
}
Vector的默认扩容容量与ArrayList略有不同,默认的扩容数组容量是原数组容量的2倍,而ArrayList的默认扩容容量为1.5倍。Vector也没有默认的缩容机制,如果需要缩小数组容量,Vector也提供了trimToSize()方法。
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 默认capacityIncrement=0,新的容量是旧的2倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity <= 0) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
Stack介绍
Stack顾名思义是一个栈,栈结构是集合中具有先进后出(LIFO)的特性。Stack继承了Vector,因此底层数据结构依然是数组,数组的最后一个元素模拟栈的栈顶,数组的第一个元素作为栈底。
从下面源码可以看出,Stack具有Vector的所有功能,并且提供了操作栈特有的pop(),peak(),push()等方法,由于这些方法都是操作数组中的最后一个元素,因此它的时间复杂度为O(1)
public
class Stack<E> extends Vector<E> {
// 将元素压入栈顶
public E push(E item) {
addElement(item);
return item;
}
// 弹出栈顶元素
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
// 查看栈顶元素
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
// 栈是否为空
public boolean empty() {
return size() == 0;
}
}
数组和栈结构转换示意图如下所示
注意:
Stack集合在JDK1.6之后就不再推荐使用了,在实际工作中,如果需要再无需保证线程安全的场景中使用栈结构,推荐使用
java.util.ArrayDeque集合;如果需要在保证线程安全性的场景中使用栈结构,则推荐使用java.util.concurrent.LinkedBlockingQueue集合。如果是在JDK1.6之前,LinkedList也提供了操作栈语意的方法,在无需保证线程安全的场景中也可以替代Stack。