Java源码分析-List集合

170 阅读7分钟

前言

在JCF(Java Collection Frame)中List集合是最常用的集合之一,List集合是一个元素有序,可重复的集合,可以通过集合中顺序索引操作集合中的元素。List集合主要包括4个接口,2个抽象类以及4个具体实现类,类关系如下图所示。

image-20230312170136952

接口简介

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)

image-20230312181521843

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。

image-20230312193135627

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

数组和栈结构转换示意图如下所示

image-20230313220006717

注意:

Stack集合在JDK1.6之后就不再推荐使用了,在实际工作中,如果需要再无需保证线程安全的场景中使用栈结构,推荐使用java.util.ArrayDeque集合;如果需要在保证线程安全性的场景中使用栈结构,则推荐使用java.util.concurrent.LinkedBlockingQueue集合。

如果是在JDK1.6之前,LinkedList也提供了操作栈语意的方法,在无需保证线程安全的场景中也可以替代Stack。