Java 集合Collection—List

发布于:2025-09-09 ⋅ 阅读:(23) ⋅ 点赞:(0)

ArrayList

ArrayList 就是 Java 集合框架里最常用的 动态数组,查询快、增删慢(中间位置)、自动扩容、非线程安全

创建(3 种)

// 1. 空构造(初始容量 10)
List<String> list = new ArrayList<>();

// 2. 指定容量(避免多次扩容)
List<String> list = new ArrayList<>(100);

// 3. 通过已有集合快速构造
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));

基本 CRUD

list.add("A");                // 尾插
list.add(0, "头");            // 按下标插入
list.get(0);                  // 查
list.set(1, "B");             // 改
list.remove("A");             // 按对象删,返回 boolean
list.remove(0);               // 按下标删,返回被删元素
list.size();                  // 元素个数
list.isEmpty();               // 是否空
list.clear();                 // 清空

遍历(4 样)

// 1. for-each(最简洁)
for (String s : list) System.out.println(s);

// 2. 下标遍历(需要索引时)
for (int i = 0; i < list.size(); i++) System.out.println(list.get(i));

// 3. 迭代器(遍历中删除安全)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("A")) it.remove();
}

// 4. Lambda(Java 8+)
list.forEach(System.out::println);

常见批量/工具操作

// 排序
list.sort(String::compareTo);          // Java 8+
Collections.sort(list);                // 旧写法

// 洗牌
Collections.shuffle(list);

// 反转
Collections.reverse(list);

// 转数组
String[] arr = list.toArray(new String[0]);

// 去重(借助 Set)
List<String> noDup = new ArrayList<>(new LinkedHashSet<>(list));

容量与性能提示

  • 默认初始容量 10,扩容 ×1.5(old + (old >> 1)),频繁扩容影响性能。
    可预估大小时直接 new ArrayList<>(size)

  • 中间插入/删除 = 数组拷贝,复杂度 O(n);只追加或随机读用 ArrayList 最划算。

  • 线程安全替代

List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 或者直接用 CopyOnWriteArrayList(读多写少场景)

一行代码记忆

new ArrayList<>(容量)  // 创
add/get/set/remove     // 增查改删
forEach + iterator       // 遍历安全
sort/shuffle/reverse     // 工具

ArrayList扩容

ArrayList 扩容 = 旧容量 1.5 倍,若仍不够则直接按需分配,最后 Arrays.copyOf 一次性拷贝

// 位于 java.util.ArrayList 中
private void grow(int minCapacity) {   // minCapacity = 当前所需最小容量
    int oldCapacity = elementData.length;          // 旧容量
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 关键1:新容量 = 旧容量 + 旧容量/2  → 1.5 倍
    if (newCapacity - minCapacity < 0)               // 关键2:如果 1.5 倍还不够,就直接用所需容量
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)            // 超过 JVM 允许最大数组长度时做极限处理
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity); // 真正申请新数组并拷贝
}

使用时机

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 每次 add 先保证容量够用
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 空构造第一次 add
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 默认初始容量 10
    }
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)  // 容量不足才触发 grow
        grow(minCapacity);
}

ArrayList源码

ArrayList 就是 “能自动长大的数组”——
底层用 连续 Object[] 存元素,用 size 字段记实际长度,扩容时 1.5 倍拷贝,所有操作围绕 “下标访问 + 数组拷贝” 设计;
查询 O(1)、中间增删 O(n)、内存紧凑、非线程安全。

下面按 字段 → 构造 → 扩容 → 增删查 → 迭代失败 五个核心展开

核心字段(JDK 17)

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    transient Object[] elementData; // 真正存元素的数组
    private int size;               // 当前元素个数,不是容量
    private static final int DEFAULT_CAPACITY = 10; // 默认容量
    private static final Object[] EMPTY_ELEMENTDATA = {};
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
}
  • elementData 长度 ≥ size,多余位置放 null

  • size 为 0 时数组可能是 {}new Object[10](懒初始化)

构造策略:懒初始化 + 零拷贝空数组

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

空构造第一次添加时才扩容到 10,避免 new ArrayList() 占 10 个引用

扩容逻辑:1.5 倍拷贝

private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = ArraysSupport.newLength(oldCapacity,
            minCapacity - oldCapacity, /* minimum growth */
            oldCapacity >> 1           /* preferred growth:0.5 倍 */);
    return elementData = Arrays.copyOf(elementData, newCapacity);
}
  • 新容量 = old + old/2(1.5 倍)

  • Arrays.copyOf 一次性 连续内存拷贝,触发 CPU 缓存友好的批量复制

  • 扩容后 旧数组立刻被 GC(无引用)

增删查设计——围绕“下标+拷贝”

随机访问(最快路径)

E elementData(int index) {
    return (E) elementData[index]; // O(1)
}

尾部添加(最快插入)

public boolean add(E e) {
    modCount++;
    add(e, elementData, size); // 直接放 elementData[size] 然后 size++
    return true;
}

中间插入/删除(代价操作)

public void add(int index, E element) {
    rangeCheckForAdd(index);
    modCount++;
    final int s = size;
    if (s == elementData.length) elementData = grow();
    // 核心:System.arraycopy 把 [index..end] 整体后移 1 位
    System.arraycopy(elementData, index, elementData, index + 1, s - index);
    elementData[index] = element;
    size = s + 1;
}
  • 插入:后移 → 赋值

  • 删除:前移 → 末尾置 null(帮助 GC)

Fail-Fast 迭代器

private class Itr implements Iterator<E> {
    int expectedModCount = modCount; // 创建时记录
    ...
    final void checkForEachComModification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}
  • 任何 结构修改(add/remove/trimToSize)都会 modCount++

  • 迭代器发现 expectedModCount ≠ modCount 立即抛异常,防止脏读

contains

顺序遍历数组 并调用 equals,直到找到匹配项

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

public int indexOf(Object o) {
    if (o == null) {                       // 支持 null 元素
        for (int i = 0; i < size; i++)
            if (elementData[i] == null) return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i])) return i; // 逐个 equals
    }
    return -1;                             // 未找到
}

内存与性能速览

get(index) O(1) 一次数组访问
add(E) 尾部 O(1) 均摊 偶尔触发 1.5 倍拷贝
add(index, E) O(n) 需整体搬移元素
remove(index) O(n) 同上
内存 连续 仅 elementData + 对象头,紧凑

用连续数组换随机访问速度,用 size 字段解耦逻辑长度与物理长度,用 1.5 倍拷贝平衡扩容次数与内存浪费,用 modCount 保迭代一致性;中间增删靠 System.arraycopy 批量搬移,CPU 缓存友好,代价是 O(n)。

LinkedList

LinkedList双向链表 实现的 ListDeque,插入/删除头尾快、随机访问慢,非线程安全。

创建

LinkedList<String> list = new LinkedList<>();           // 空链表
LinkedList<Integer> list2 = new LinkedList<>(Arrays.asList(1,2,3)); // 带初值

List 接口常用(随机访问性能差)

list.add("A");              // 尾插
list.add(1, "B");           // 按下标插入(需遍历到节点,O(n))
list.get(2);                // 按索引查(O(n))
list.set(0, "AA");          // 改(O(n))
list.remove("A");           // 按对象删
list.remove(0);             // 按下标删
list.size();                // 元素个数
list.clear();               // 清空

Deque 接口常用(头/尾操作 O(1))

// 头
list.addFirst("head");      // 头部插入
String h = list.removeFirst(); // 头部删除并返回
String peek = list.peekFirst(); // 偷看头

// 尾
list.addLast("tail");       // 尾插(等价 add)
String t = list.removeLast();
String peekTail = list.peekLast();

// 队列模式
list.offer("task");         // 尾插
String task = list.poll();  // 头删,空返回 null
String element = list.element(); // 头查,空抛异常

遍历(4 样)

// 1. for-each(最常用)
for(String s : list) System.out.println(s);

// 2. 下标(慢,O(n^2) 总耗时)
for (int i = 0; i < list.size(); i++) ...

// 3. 迭代器(遍历时删安全)
Iterator<String> it = list.iterator();
while(it.hasNext()) if (it.next().equals("A")) it.remove();

// 4. Lambda
list.forEach(System.out::println);

栈/队列 一行用法

// 当栈用
list.push("A");      // 压栈
String top = list.pop(); // 出栈

// 当队列用
list.offer("job");
String job = list.poll();

性能 & 使用提示

  • 随机访问 get(index) 需遍历,复杂度 O(n);大量按位置读写请用 ArrayList

  • 头/尾增删 O(1),适合 队列、栈 场景。

  • 需要线程安全时

Deque<String> syncDeque = Collections.synchronizedDeque(new LinkedList<>());
// 或 ConcurrentLinkedQueue / LinkedBlockingDeque

口诀背下来

LinkedList = 双向链
push/pop 当栈用
addFirst/removeFirst 当队列
随机 get 慢成狗

LinkedList源码

LinkedList 就是 “双向链表” ——
Node<E> 把元素串成两条链(前向 + 后向),头尾插入/删除 O(1),随机访问 O(n),内存 非连续、迭代安全,支持 队列、栈、双端队列 全套 API。

下面按 节点结构 → 头尾指针 → 增删查 → 队列/栈适配 → 内存与 fail-fast 6 个维度拆解

节点结构:一条记录存 3 个引用

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

每个元素额外 2 个指针 ≈ 24 byte(64 位压缩指针)开销。

transient Node<E> first;   // 头节点
transient Node<E> last;    // 尾节点
transient int size = 0;

空列表时 first = last = null;单节点时两者指向同一个 Node。

头尾增删:O(1) 只改邻居指针

头插 addFirst(E e)

private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f); // prev = null
    first = newNode;
    if (f == null)                   // 原列表空
        last = newNode;
    else
        f.prev = newNode;            // 原头的前驱指向新头
    size++;
    modCount++;
}

尾插 addLast(E e)(JDK 里叫 linkLast)同理,只改 last 和原尾节点的 next

中间插入 add(int index, E e)
→ 先 折半遍历找到节点(见第 4 点),再 unlinkBefore 改 4 个指针:

Node<E> succ = node(index);        // 目标位置原节点
Node<E> newNode = new Node<>(succ.prev, e, succ);
succ.prev.next = newNode;
succ.prev = newNode;

只改邻居引用,无需像 ArrayList 那样整体搬移数组。

 

随机访问:折半遍历 → O(n)

Node<E> node(int index) {
    // 下标 < size/2 从头扫;否则从尾扫
    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;
    }
}

最坏仍需 n/2 次指针跳转,缓存不友好,这也是 LinkedList 被随机访问嫌弃的根本原因

队列/栈/双端队列:一套链表,三套接口

// 当 FIFO 队列
Queue<String> q = new LinkedList<>();
q.offer("A");      // 尾插
q.poll();          // 头删

// 当 LIFO 栈
Deque<String> s = new LinkedList<>();
s.push("A");       // 头插
s.pop();           // 头删

// 当双端队列
Deque<String> dq = new LinkedList<>();
dq.addFirst("head");
dq.addLast("tail");

底层都是 linkFirst / linkLast / unlinkFirst / unlinkLast 4 个私有方法,O(1) 完成。

Fail-Fast & 内存

  • 同 ArrayList,modCount 计数结构修改;迭代器发现不一致立即抛 ConcurrentModificationException

  • 节点 分散在堆中不连续,CPU 缓存行利用率低;但 插入/删除不移动数据,适合 头/尾频繁变动 场景。

设计思想一句话

用 Node 前后指针把元素串成双向链,头尾 direct 指针保证 O(1) 增删,折半遍历应付随机 index;一套链表同时实现 List+Queue+Deque,迭代安全,内存分散换增删速度。

List如何线程安全

CopyOnWriteArrayList —— 读多写少无敌手

  • 原理:写时复制整个数组;读操作无锁。

  • 适用:遍历远多于修改,且数据量不大(复制成本可接受)。

List<String> list = new CopyOnWriteArrayList<>();

缺点:写频繁或数组巨大时 GC 压力大。

Collections.synchronizedList(new ArrayList<>()) —— 通用兜底

  • 原理:对每个方法加 synchronized (mutex),读写都串行。

  • 适用:写操作不少、数据量中等、需要随机访问。

List<String> list = Collections.synchronizedList(new ArrayList<>());

注意:

  • 迭代必须手动加锁,否则抛 ConcurrentModificationException

synchronized (list) {
    for (String s : list) { ... }
}

Vector —— 历史遗产,别再用

synchronizedList 类似,但所有方法直接加 synchronized,性能差、扩展性差;官方已不推荐使用。

手动外部锁 —— 特殊需求

自己控制锁粒度,例如读写锁 ReentrantReadWriteLock

ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
List<String> list = new ArrayList<>();

// 读
rwl.readLock().lock();
try { ... } finally { rwl.readLock().unlock(); }

// 写
rwl.writeLock().lock();
try { list.add("x"); } finally { rwl.writeLock().unlock(); }

复杂度最高,只有需要 细粒度控制批量读写 时才考虑。

选型口诀
“读远多于写” → CopyOnWriteArrayList
“读写差不多” → Collections.synchronizedList
“不要碰 Vector” → 历史遗迹
“极致性能” → 手动读写锁

CopyOnWriteArrayList写时加锁复制

同一时刻只能有一条线程写,读线程完全无锁
写操作先 加锁 → 复制数组 → 修改副本 → 原子替换引用,最后释放锁。

set(int index, E element) —— 替换指定位置元素

public E set(int index, E element) {
    synchronized (lock) {              // ① 全局写锁,保证只有一条线程进入
        Object[] es = getArray();      // ② 拿到当前 volatile 数组快照(读无锁)
        E oldValue = elementAt(es, index); // ③ 取旧值

        if (oldValue != element) {     // ④ 值真正变化才复制
            es = es.clone();           // ⑤ 整表复制一份新数组
            es[index] = element;       // ⑥ 在新数组上修改
        }
        // ⑦ 即使旧值相同,也做一次 volatile 写,保证“写-写”顺序语义
        setArray(es);                  // ⑧ 原子替换数组引用,读线程立即可见
        return oldValue;               // ⑨ 返回旧值
    }                                    // ⑩ 解锁,写完成
}

add(E e) —— 尾部追加元素

public boolean add(E e) {
    synchronized (lock) {              // ① 写锁
        Object[] es = getArray();      // ② 当前数组
        int len = es.length;           // ③ 旧长度
        es = Arrays.copyOf(es, len + 1); // ④ 复制并扩容 1 位
        es[len] = e;                   // ⑤ 在新数组尾部放入新元素
        setArray(es);                  // ⑥ volatile 替换引用,读线程无感知切换
        return true;                   // ⑦ 成功
    }                                    // ⑧ 解锁
}
要点 说明
锁范围 仅写操作加 synchronized (lock),读操作(get()/iterator())完全不抢锁,实现 读多写少 的高并发。
复制数组 任何修改都先 clone/copyOf 生成新数组,旧数组在迭代器手里仍是快照,遍历不会抛 ConcurrentModificationException。
volatile 替换 setArray(es) 把新数组赋给 volatile Object[] array;,保证 写-读 的可见性;读线程下次 getArray() 立刻看到新数据。
最小修改 set 方法里若旧值相同,仍执行一次 volatile 写,是为了维持“写-写” happens-before 语义(框架级需求)。

Arrays.copyOf

Collections.synchronizedList

Collections.synchronizedList的原理是在原有的List上套一层synchronized

Collections.synchronizedList 的实现原理就是 “装饰器模式 + 同步代码块”
它并不改动原 List 的内部结构,而是把 所有访问方法 包上一层 synchronized (mutex),让 同一时刻只有一个线程能读写

下面给出源码级注释,一眼看懂。

工厂方法入口

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :   // ArrayList 等
            new SynchronizedList<>(list));               // LinkedList 等
}

→ 根据是否支持随机访问,选两个装饰子类,但核心逻辑一样。

装饰类结构(以 SynchronizedList 为例)

static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
    private static final long serialVersionUID = -7754090372962977524L;
    final List<E> list;           // 被装饰的原始 List

    SynchronizedList(List<E> list) {
        super(list);              // 父类把 list 保存为 this.c
        this.list = list;
    }
    ...
}

方法级同步 —— 所有操作包 synchronized (mutex)

public E get(int index) {
    synchronized (mutex) {        // 同一时刻只有一个线程能读
        return list.get(index);
    }
}

public void add(int index, E element) {
    synchronized (mutex) {        // 写也串行
        list.add(index, element);
    }
}

public Iterator<E> iterator() {
    return list.iterator();       // ⚠️ 注意:迭代器本身 **不同步**!
}
  • mutex 在父类 SynchronizedCollection 里就是 当前装饰器实例 (this)。

  • 因此 读写互斥、写写互斥、读读也互斥 —— 完全串行。

迭代器 不同步 —— 必须手动加锁

List<Foo> syncList = Collections.synchronizedList(new ArrayList<>());
...
synchronized (syncList) {        // 整个遍历区间加锁
    for (Foo f : syncList) {
        // do something
    }
}

否则在多线程环境下会抛 ConcurrentModificationException

与 Vector 对比

维度 synchronizedList Vector
锁粒度 装饰器方法级 synchronized (mutex) 方法声明 synchronized
底层实现 复用现有 ArrayList/LinkedList 原生同步数组
迭代器 需外部手动同步 需外部手动同步
扩展性 可装饰任意 List 实现 固定实现

一句话总结
Collections.synchronizedList 就是 给原 List 套一层“方法级同步壳”
所有读写操作串行化迭代器必须额外手动加锁,简单但并发度低,适合 写少读少快速兜底 场景。

LinkedList和ArrayList使用场景

查询多、尾部增删多 → ArrayList;插入删除多、当队列/栈用 → LinkedList
下面把场景、量化数据、代码示例一次给全,面试直接背。

一、底层决定性格

维度 ArrayList LinkedList
数据结构 可扩容 Object[] 双向 链表(Node<E>
随机访问 O(1) O(n)
头/尾插入删除 O(1) 仅尾插;中间 O(n) O(1) 头尾;中间定位 O(n)
内存占用 连续数组,1 倍指针 每个元素 3 个指针(prev、next、item)≈ 3 倍
缓存友好 连续内存,命中高 节点分散,缓存行失效

二、量化场景阈值(经验值)

  1. 随机访问次数 > 1 000 / 每 1 次插入 → 用 ArrayList
    实测 10w 次 get(i)ArrayList ≈ 1 ms,LinkedList ≈ 200 ms

  2. 头/尾插入比例 > 30% 且几乎不查询 → LinkedList 开始反超
    10w 次头插:LinkedList ≈ 3 ms,ArrayList ≈ 200 ms(批量 add(0, e) 需整体拷贝)

三、典型业务场景对照

场景 推荐 原因
数据库结果集只读遍历 ArrayList 大量 get(i)
消息队列、栈、双端队列 LinkedList 头尾 O(1),可当队列/栈
中间频繁插入/删除(>30%)且数据量大 LinkedList 无需移动元素
缓存、随机查询密集 ArrayList 连续内存+O(1) 访问
Android RecyclerView 数据源 ArrayList 随机 get 多,内存紧凑

网站公告

今日签到

点亮在社区的每一天
去签到