JAVASE-集合-List

2 阅读9分钟

有序、重复、索引 可以配合小弟数据结构文章结合理解 juejin.cn/post/759271…

特性ArrayListLinkedListVector
物理结构连续数组 (Object[])双向链表 (Node)连续数组 (Object[])
扩容倍数1.5 倍 (old + old >> 1) (按需分配节点)2 倍 (或指定增量)
初始化延迟加载 (首次 add 扩容 10)立即创建对象 (空链表)默认初始化容量为 10
同步性线程不安全 (异步)线程不安全 (异步)线程安全 (synchronized)
性能优势随机访问 O(1)O(1)首尾增删 O(1)O(1)兼容老版本代码
内存占用连续大内存,有预留浪费离散内存,指针占用大连续大内存,有预留浪费

1、定义和特点

特点

List 接口是 Collection 的子接口,它在单列集合的基础上增加了“位置”的概念。

(1) 核心特征归纳

  • 有序性 (Order):元素存储顺序与插入顺序一致。
  • 索引访问 (Index):每个元素都有对应的整数索引(0 到 size-1),支持精确位置操作。
  • 可重复性 (Repetition):允许存储多个 equals 返回 true 的元素,也允许存储多个 null。 (2) 物理结构决定选型 (重点对比)

在 Java 中,选型本质上是在连续内存数组离散内存链表之间做权衡。

维度ArrayList (动态数组)LinkedList (双向链表)
底层数据结构连续的 Object[] 数组离散的 Node 双向节点
随机访问 (get)极快 (O(1)O(1)):直接通过地址偏移计算。慢 (O(n)O(n)):需从头或尾逐个指针跳转。
首尾操作尾部快 (O(1)O(1));头部慢 (O(n)O(n),需移动全表)。极快 (O(1)O(1)):直接修改首尾指针。
中间插入/删除慢 (O(n)O(n)):涉及 System.arraycopy 大规模移动。快 (O(1)O(1)):仅需修改前后节点指针。
内存开销较低:仅存储元素本身(但有预留空间浪费)。较高:每个数据需额外存储两个指针(prev/next)。
CPU 缓存友好度:数组连续性利于预取。:节点散落在堆内存各处。

选型结论

  • 若应用场景以 “读多写少”“末尾追加” 为主,首选 ArrayList
  • 若涉及频繁的 “头尾增删”“队列操作”,考虑 LinkedList

List的api

除了继承自 Collection 的 API 外,List 额外提供了基于索引的操作:

操作分类方法签名重点备注
add(int index, E element)在指定位置插入,后续元素全部后移。
remove(int index)返回被删除的元素,后续元素前移。
set(int index, E element)替换指定位置元素,返回旧元素。
get(int index)核心方法。List 随机访问的基石。
搜索indexOf(Object o) / lastIndexOf查找元素首次/最后一次出现的位置,找不到返回 -1。
截取subList(int from, int to)避坑:返回的是原 List 的视图,对 subList 的修改会直接反映在原 List 上。

2、ArrayList

ArrayList 是基于动态数组实现的 List 接口实现类。它的核心在于如何通过“预留空间”和“自动扩容”来管理这个非固定长度的数组。

(1) 存储结构与 transient 优化

核心成员变量


   /** 
   * 存储 ArrayList 元素的数组缓冲区。 
   * ArrayList 的容量就是该数组缓冲区的长度。 
   * 任何 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList, 
   * 在添加第一个元素时都将被扩容至 DEFAULT_CAPACITY(默认容量 10)。 
   */
   transient Object[] elementData; // 非私有以简化嵌套类访问
   
   /**  
   * ArrayList 的大小(包含的元素数量) 	 
   */  	
   private int size;

transient 的意义: 由于 elementData 的容量(capacity)通常大于 size,为了避免序列化那些无效的 null 元素造成浪费,ArrayList 标记其为 transient。它手动重写了序列化方法,仅持久化有效数据,从而实现空间利用率的最大化。

(2) 初始化:

延迟分配(Lazy Initialization)策略 在 JDK 1.7 之后,ArrayList 为了优化内存,采用了延迟初始化的方式:

  1. 空参构造List list = new ArrayList(); 时,底层并不会立即创建长度为 10 的数组。
  2. 默认指向:它会将 elementData 指向一个共享的空数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  3. 首次触发:只有在真正执行第一次 add() 操作时,才会将数组扩容至默认容量 10
public ArrayList() {  
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;  
}

(3) 扩容算法:1.5 倍

当数组空间不足时,会触发 grow(int minCapacity) 方法:

  • 计算规则int newCapacity = oldCapacity + (oldCapacity >> 1);
    • 逻辑:旧容量加上旧容量的一半,即 1.5 倍扩容
    • 优化:使用位运算符 >>(右移一位),计算速度极快,且在容量增长与内存开销之间取得了动态平衡。
  • 数据迁移:调用 Arrays.copyOf(),其底层最终执行 System.arraycopy()(Native 方法),通过 C++ 实现内存块的整体搬迁。
/** * 增加容量以确保它至少能容纳由最小容量参数指定的元素数量。
 * @param minCapacity 所需的最小容量
 * @throws OutOfMemoryError 如果 minCapacity 小于零
 */
private Object[] grow(int minCapacity) {  
    int oldCapacity = elementData.length;  
    // 如果当前容量 > 0,或者不是默认的空数组,则执行 1.5 倍扩容逻辑
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {  
        int newCapacity = ArraysSupport.newLength(oldCapacity,  
                minCapacity - oldCapacity, /* 最小增长步长 */  
                oldCapacity >> 1           /* 首选增长步长 (0.5倍旧容量) */);  
        return elementData = Arrays.copyOf(elementData, newCapacity);  
    } else {  
        // 第一次添加元素,直接初始化为 10(或 minCapacity 中的较大者)
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];  
    }  
}

(4) 核心操作的代码分析

  • 查询 (get)O(1)O(1)。基于数组下标的寻址公式:BaseAddress+index×ElementSizeBaseAddress + index \times ElementSize
  /**
 * 返回此列表中指定位置的元素。
 *
 * @param index 要返回的元素的索引
 * @return 列表中指定位置的元素
 * @throws IndexOutOfBoundsException 如果索引越界
 */
public E get(int index) {
    // 检查索引是否在有效范围内 (0 <= index < size)
    Objects.checkIndex(index, size);
    return elementData(index);
}

// 实际访问数组并强转类型
@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}
  • 插入 (add)
    • 尾部插入:通常为 O(1)O(1),若触发扩容则为 O(n)O(n)
    • 中间插入:O(n)O(n)。需要将插入点后的所有元素整体后移一位。
/**
 * 将指定的元素追加到此列表的末尾。
 */
public boolean add(E e) {
    modCount++; // 结构性修改计数自增
    add(e, elementData, size);
    return true;
}

/**
 * 此辅助方法从 add(E) 中分离出来,以保持方法字节码大小小于 35,
 * 这有助于在 C1 编译循环中调用 add(E) 时进行内联优化。
 */
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow(); // 空间已满,触发扩容
    elementData[s] = e;       // 在末尾赋值
    size = s + 1;             // 逻辑长度加 1
}

/**
 * 在此列表的指定位置插入指定元素。
 * 将当前在该位置的元素(如果有)以及所有后续元素向右移动(索引加 1)。
 */
public void add(int index, E element) {
    rangeCheckForAdd(index); // 专门为 add 操作设计的边界检查
    modCount++;
    final int s;
    Object[] elementData;
    // 1. 检查是否需要扩容
    if ((s = size) == (elementData = this.elementData).length)
        elementData = grow();
    // 2. 核心:调用 Native 方法进行内存拷贝,将 index 后的元素后移一位
    System.arraycopy(elementData, index,
                     elementData, index + 1,
                     s - index);
    // 3. 覆盖目标位置
    elementData[index] = element;
    size = s + 1;
}    
  • 删除 (remove)O(n)O(n)。需要将被删元素后的所有元素整体前移,并手动将最后一个索引位设为 null 以便 GC 回收。
/**
 * 内部删除辅助方法,跳过边界检查且不返回被删除的值。
 * * @param es 存储元素的数组
 * @param i 要删除的索引位
 */
private void fastRemove(Object[] es, int i) {
    modCount++;
    final int newSize;
    // 如果删除的不是最后一个元素,则需要前移覆盖
    if ((newSize = size - 1) > i)
        System.arraycopy(es, i + 1, es, i, newSize - i);
    
    // 关键点:将数组最后一个位置设为 null,让 GC 能够回收不再使用的对象引用
    es[size = newSize] = null;
}

3、LinkedList

LinkedList 底层基于 双向链表 实现。由于其物理存储是离散的,它不具备随机访问的优势,但在频繁增删的场景下表现卓越。

(1) 存储结构

  1. LinkedList 的每个元素都被封装在一个内部类 Node 中。
  2. LinkedList 通过持有头尾两个指针,实现了对整个链表的控制。
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;
    }
}

// 记录当前链表中的节点个数。
transient int size = 0;  
  
// 指向链表的第一个节点。  
transient Node<E> first;  

// 指向链表的最后一个节点。 
transient Node<E> last;

(2) 核心操作的代码分析

1. 查询 (get)

  • 复杂度O(n)O(n)
  • 原理:由于内存不连续,无法通过公式定位,必须顺着指针一个个找。
  • 源码优化:采用了二分折半查找。如果索引在前半段则从 first 向后找,在后半段则从 last 向前找。
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;  
    }  
}

2. 插入 (add)

  • 尾部插入O(1)O(1)。直接修改 last 指针即可。
  • 指定位置插入O(n)O(n)。虽然修改指针是 O(1)O(1),但定位到该位置需要 O(n)O(n)
/**
 * 在指定位置插入元素。
 */
public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element); // 直接追到末尾
    else
        linkBefore(element, node(index)); // 查找到该节点并在其前插入
}

/**
 * 将元素 e 链接到非空节点 succ 之前。
 */
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++;
}

3. 删除 (remove)

  • 头尾删除O(1)O(1)
  • 指定元素删除O(n)O(n)。需要先查找到该元素。
/**
 * 取消链接非空节点 x(内部删除逻辑)。
 */
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) { // 如果是头节点
        first = next;
    } else {
        prev.next = next;
        x.prev = null; // 辅助 GC
    }

    if (next == null) { // 如果是尾节点
        last = prev;
    } else {
        next.prev = prev;
        x.next = null; // 辅助 GC
    }

    x.item = null; // 辅助 GC
    size--;
    modCount++;
    return element;
}

4、Vector (不推荐)

Vector 是 JDK 1.0 就存在的“元老级”集合。它的底层存储逻辑与 ArrayList 几乎一致(都是基于 Object[] 数组),但其核心设计目标是线程安全

(1) 存储与扩容机制

  • 物理结构:同样是 transient Object[] elementData
  • 扩容差异
    • ArrayList 固定扩容为旧容量的 1.5 倍
    • Vector 可以通过构造函数指定 capacityIncrement(增长增量)。如果未指定,默认扩容为旧容量的 2 倍

(2) 核心特性:线程安全

Vector 几乎所有对外暴露的操作方法(add, get, remove, size 等)都加上了 synchronized 关键字。

/**
 * 将指定元素追加到此向量的末尾。
 */
public synchronized boolean add(E e) {
    modCount++;
    add(e, elementData, elementCount);
    return true;
}

/**
 * 返回此向量中指定位置的元素。
 */
public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    return elementData(index);
}
  • 代价:由于采用了全表锁(即任何时候只有一个线程能操作该集合),在高并发场景下性能极差。

(3) 重点总结:Vector vs ArrayList

特性ArrayListVector
诞生版本JDK 1.2 (新)JDK 1.0 (旧)
线程安全线程不安全,性能高线程安全,但性能低
扩容倍数1.5 倍2 倍
推荐程度首选不推荐 (除非有特定陈旧代码维护需求)