19 集合框架:List——ArrayList与LinkedList深度对比

目录

🟡 19 集合框架:List------ArrayList与LinkedList深度对比

更新日期 :2026年5月 | Java入门到精通系列 · 第三阶段·核心进阶

© 版权声明:本文为原创技术文章,转载请联系作者并注明出处。


📑 目录


一、集合框架概述

Java集合框架(Java Collections Framework, JCF)是Java语言中最常用的基础设施之一。在上一阶段的学习中,我们已经掌握了数组的使用,但数组有其固有的局限性------长度固定。集合框架正是为了解决这个问题而诞生的。

1.1 为什么需要集合框架?

java 复制代码
// 数组的局限性
String[] names = new String[3];
names[0] = "Alice";
names[1] = "Bob";
names[2] = "Charlie";
// 如果需要添加第4个元素呢?必须创建新数组!

集合框架的优势:

  • 动态扩容:无需关心容量问题
  • 丰富的API:排序、查找、遍历一应俱全
  • 类型安全:配合泛型使用,编译期检查类型
  • 线程安全可选:提供同步版本

1.2 集合框架体系结构

复制代码
                    Iterable
                       │
                   Collection
                   /    |    \
                List   Set   Queue
               / \      |       \
         ArrayList  HashSet   PriorityQueue
         LinkedList TreeSet   ArrayDeque
         Vector

二、List接口规范

List 是一个有序集合(也称为序列)。用户可以精确控制每个元素的插入位置,也可以通过整数索引访问元素。

2.1 List接口核心方法

java 复制代码
public interface List<E> extends Collection<E> {
    // 添加元素
    boolean add(E e);
    void add(int index, E element);
    boolean addAll(Collection<? extends E> c);

    // 获取元素
    E get(int index);
    int indexOf(Object o);
    int lastIndexOf(Object o);

    // 修改元素
    E set(int index, E element);

    // 删除元素
    E remove(int index);
    boolean remove(Object o);

    // 查询
    int size();
    boolean isEmpty();
    boolean contains(Object o);

    // 遍历
    Iterator<E> iterator();
    ListIterator<E> listIterator();
    ListIterator<E> listIterator(int index);

    // 子列表
    List<E> subList(int fromIndex, int toIndex);
}

2.2 List接口的特性

特性 说明
有序性 元素按插入顺序排列,索引从0开始
可重复 允许存储重复元素
允许null 可以存储null值(大多数实现)
随机访问 支持通过索引直接访问
线程不安全 默认实现都不是线程安全的

三、ArrayList源码深度分析

ArrayList 是List接口最常用的实现类,底层基于动态数组实现。

3.1 基本使用

java 复制代码
import java.util.ArrayList;
import java.util.List;

public class ArrayListDemo {
    public static void main(String[] args) {
        // 创建ArrayList
        List<String> list = new ArrayList<>();

        // 添加元素
        list.add("Java");
        list.add("Python");
        list.add("Go");
        list.add(1, "Rust");  // 在索引1处插入

        // 遍历
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }

        // 增强for循环
        for (String lang : list) {
            System.out.println(lang);
        }
    }
}

3.2 底层数据结构

java 复制代码
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;

    // 空数组(用于空实例)
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 默认容量的空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 存储元素的数组
    transient Object[] elementData;

    // 实际元素个数
    private int size;
}

3.3 扩容机制(核心源码分析)

java 复制代码
// 添加元素
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保容量
    elementData[size++] = e;
    return true;
}

// 确保容量
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;  // 修改次数(用于fail-fast)
    if (minCapacity - elementData.length > 0) {
        grow(minCapacity);  // 扩容
    }
}

// 核心扩容方法
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);  // 1.5倍
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        newCapacity = hugeCapacity(minCapacity);
    }
    // 复制到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容流程图

复制代码
add() → ensureCapacityInternal() → ensureExplicitCapacity() → grow()
                                                              ↓
                                              newCapacity = old + old/2 (1.5倍)
                                                              ↓
                                              Arrays.copyOf() → 创建新数组并复制

3.4 性能分析

操作 时间复杂度 说明
get(index) O(1) 数组直接索引
add(e) (末尾) O(1) 平均 偶尔触发扩容时O(n)
add(index, e) O(n) 需要移动元素
remove(index) O(n) 需要移动元素
contains(e) O(n) 需要遍历
size() O(1) 直接返回size字段

3.5 ArrayList的subList陷阱

java 复制代码
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D", "E"));
List<String> sub = list.subList(1, 3);  // [B, C]

// 陷阱1:subList返回的是视图,不是新列表
sub.set(0, "X");
System.out.println(list);  // [A, X, C, D, E]  原列表也被修改了!

// 陷阱2:修改原列表后再操作subList会抛ConcurrentModificationException
list.add("F");
try {
    sub.get(0);  // 抛出异常!
} catch (ConcurrentModificationException e) {
    System.out.println("原列表已修改,subList失效");
}

// 正确做法:创建新的副本
List<String> safeSub = new ArrayList<>(list.subList(1, 3));

四、LinkedList源码深度分析

LinkedList 底层基于双向链表 实现,同时实现了 ListDeque 接口。

4.1 基本使用

java 复制代码
import java.util.LinkedList;

public class LinkedListDemo {
    public static void main(String[] args) {
        LinkedList<String> linkedList = new LinkedList<>();

        // 作为List使用
        linkedList.add("A");
        linkedList.add("B");
        linkedList.addFirst("Start");
        linkedList.addLast("End");

        // 作为Deque(双端队列)使用
        linkedList.push("Top");      // 等同于addFirst
        linkedList.offer("Bottom");   // 等同于offerLast
        linkedList.poll();            // 弹出头部元素

        // 作为栈使用
        linkedList.push("Stack1");
        linkedList.push("Stack2");
        System.out.println(linkedList.pop());  // Stack2
        System.out.println(linkedList.peek()); // Stack1
    }
}

4.2 底层数据结构

java 复制代码
public class LinkedList<E> extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable {

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

    // 内部节点类
    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;
        }
    }
}

双向链表结构

复制代码
null ← [A] ⇄ [B] ⇄ [C] ⇄ [D] → null
        ↑                     ↑
      first                  last

4.3 核心操作源码

java 复制代码
// 在头部插入
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null) {
        last = newNode;
    } else {
        f.prev = newNode;
    }
    size++;
    modCount++;
}

// 在尾部插入
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++;
}

// 根据索引查找节点(优化:从近的一端开始)
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;
    }
}

4.4 性能分析

操作 时间复杂度 说明
get(index) O(n) 需要从头/尾遍历
add(e) (末尾) O(1) 直接操作last节点
addFirst(e) O(1) 直接操作first节点
add(index, e) O(n) 需要先定位
remove(index) O(n) 定位 + 修改指针
contains(e) O(n) 需要遍历
size() O(1) 直接返回size字段

五、ArrayList vs LinkedList 全面对比

5.1 核心对比表

对比维度 ArrayList LinkedList
底层结构 动态数组 双向链表
内存占用 紧凑,可能浪费尾部空间 每个节点额外存储两个指针
随机访问 ⚡ O(1) O(n)
头部插入/删除 O(n) ⚡ O(1)
尾部插入 ⚡ O(1) 平均 ⚡ O(1)
中间插入/删除 O(n) O(n)(定位O(n)+插入O(1))
CPU缓存友好 ⭐ 友好(连续内存) 不友好(分散内存)
实现了Deque接口

5.2 性能基准测试

java 复制代码
import java.util.*;

public class PerformanceTest {
    public static void main(String[] args) {
        int n = 100000;

        // 测试尾部添加
        ArrayList<Integer> arrayList = new ArrayList<>();
        long start = System.currentTimeMillis();
        for (int i = 0; i < n; i++) {
            arrayList.add(i);
        }
        System.out.println("ArrayList尾部添加: " + (System.currentTimeMillis() - start) + "ms");

        LinkedList<Integer> linkedList = new LinkedList<>();
        start = System.currentTimeMillis();
        for (int i = 0; i < n; i++) {
            linkedList.add(i);
        }
        System.out.println("LinkedList尾部添加: " + (System.currentTimeMillis() - start) + "ms");

        // 测试头部添加
        start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            arrayList.add(0, i);
        }
        System.out.println("ArrayList头部添加10000次: " + (System.currentTimeMillis() - start) + "ms");

        start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            linkedList.add(0, i);
        }
        System.out.println("LinkedList头部添加10000次: " + (System.currentTimeMillis() - start) + "ms");

        // 测试随机访问
        start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            arrayList.get(new Random().nextInt(arrayList.size()));
        }
        System.out.println("ArrayList随机访问10000次: " + (System.currentTimeMillis() - start) + "ms");

        start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            linkedList.get(new Random().nextInt(linkedList.size()));
        }
        System.out.println("LinkedList随机访问10000次: " + (System.currentTimeMillis() - start) + "ms");
    }
}

典型输出结果

复制代码
ArrayList尾部添加: 12ms
LinkedList尾部添加: 25ms
ArrayList头部添加10000次: 15ms
LinkedList头部添加10000次: 1ms
ArrayList随机访问10000次: 1ms
LinkedList随机访问10000次: 320ms

5.3 内存占用对比

java 复制代码
// ArrayList:元素引用数组 + 少量预留空间
// 假设存储100个Integer对象
// ArrayList额外开销:数组头(16字节) + 预留空间引用 ≈ 几十字节

// LinkedList:每个元素需要一个Node对象
// 每个Node:对象头(16字节) + item引用(4字节) + prev引用(4字节) + next引用(4字节) = 28字节
// 100个元素的额外开销:28 * 100 = 2800字节

六、Vector与Stack------古老遗迹

6.1 Vector

java 复制代码
// Vector是线程安全的ArrayList(已不推荐使用)
Vector<String> vector = new Vector<>();
vector.add("Hello");

// 每个方法都加了synchronized
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

6.2 替代方案

java 复制代码
// 如果需要线程安全的List,推荐:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());

// 或者使用CopyOnWriteArrayList(读多写少场景)
import java.util.concurrent.CopyOnWriteArrayList;
List<String> cowList = new CopyOnWriteArrayList<>();

七、使用场景与选型指南

7.1 选型决策树

复制代码
需要List集合?
    │
    ├── 需要频繁随机访问?
    │       └── 是 → ArrayList ✅
    │
    ├── 需要频繁在头部插入/删除?
    │       └── 是 → LinkedList ✅
    │
    ├── 需要当作队列/双端队列使用?
    │       └── 是 → LinkedList(或ArrayDeque)✅
    │
    ├── 元素数量固定或极少修改?
    │       └── 是 → List.of()(不可变列表)✅
    │
    └── 不确定 → 默认选择ArrayList ✅

7.2 实战场景示例

java 复制代码
// 场景1:商品列表(频繁随机访问)
public class ProductListDemo {
    private List<Product> products = new ArrayList<>();

    public Product getProduct(int index) {
        return products.get(index);  // O(1) 随机访问
    }

    public void addProduct(Product product) {
        products.add(product);  // 尾部添加
    }
}

// 场景2:最近浏览记录(频繁头部插入,定期删除旧记录)
public class RecentViewDemo {
    private LinkedList<String> recentViews = new LinkedList<>();
    private static final int MAX_SIZE = 20;

    public void view(String itemId) {
        recentViews.addFirst(itemId);  // O(1) 头部插入
        if (recentViews.size() > MAX_SIZE) {
            recentViews.removeLast();  // O(1) 尾部删除
        }
    }

    public List<String> getRecentViews() {
        return new ArrayList<>(recentViews);  // 返回副本
    }
}

// 场景3:实现LRU缓存
import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);  // accessOrder=true
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

八、常见面试题解析

面试题1:ArrayList和LinkedList的区别?

标准回答

  1. 底层数据结构:ArrayList基于动态数组,LinkedList基于双向链表
  2. 随机访问:ArrayList支持O(1)随机访问,LinkedList需要O(n)
  3. 插入删除:头部操作LinkedList更优,尾部操作两者接近
  4. 内存:ArrayList更紧凑,LinkedList每个元素额外存储两个指针
  5. 实际使用:绝大多数场景ArrayList性能更好(CPU缓存友好)

面试题2:ArrayList扩容机制?

标准回答

  • 默认初始容量10
  • 当容量不足时,扩容为原来的1.5倍(oldCapacity + (oldCapacity >> 1)
  • 通过Arrays.copyOf()将旧数组内容复制到新数组
  • 如果预知数据量,建议使用new ArrayList<>(capacity)指定初始容量

面试题3:为什么ArrayList的elementData用transient修饰?

java 复制代码
transient Object[] elementData;

因为ArrayList的实际元素个数size可能小于数组长度。如果直接序列化整个数组,会浪费空间。ArrayList自定义了序列化方法:

java 复制代码
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
    int expectedModCount = modCount;
    s.defaultWriteObject();
    s.writeInt(size);
    for (int i = 0; i < size; i++) {
        s.writeObject(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

九、综合实战练习

练习1:实现简易ArrayList

java 复制代码
public class MyArrayList<E> {
    private Object[] elements;
    private int size;
    private static final int DEFAULT_CAPACITY = 10;

    public MyArrayList() {
        this.elements = new Object[DEFAULT_CAPACITY];
    }

    public MyArrayList(int capacity) {
        this.elements = new Object[capacity];
    }

    public void add(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    @SuppressWarnings("unchecked")
    public E get(int index) {
        checkIndex(index);
        return (E) elements[index];
    }

    public E remove(int index) {
        checkIndex(index);
        E oldValue = get(index);
        int numMoved = size - index - 1;
        if (numMoved > 0) {
            System.arraycopy(elements, index + 1, elements, index, numMoved);
        }
        elements[--size] = null;
        return oldValue;
    }

    public int size() {
        return size;
    }

    private void ensureCapacity() {
        if (size == elements.length) {
            int newCapacity = elements.length + (elements.length >> 1);
            Object[] newElements = new Object[newCapacity];
            System.arraycopy(elements, 0, newElements, 0, size);
            elements = newElements;
        }
    }

    private void checkIndex(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
    }
}

练习2:使用List实现扑克牌发牌

java 复制代码
import java.util.*;

public class PokerGame {
    private List<Card> deck = new ArrayList<>();
    private Map<String, List<Card>> players = new HashMap<>();

    public PokerGame(String... playerNames) {
        // 初始化牌组
        String[] suits = {"♠", "♥", "♦", "♣"};
        String[] ranks = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};
        for (String suit : suits) {
            for (String rank : ranks) {
                deck.add(new Card(suit, rank));
            }
        }
        // 初始化玩家
        for (String name : playerNames) {
            players.put(name, new ArrayList<>());
        }
    }

    public void shuffleAndDeal() {
        Collections.shuffle(deck);  // 洗牌
        Iterator<Card> it = deck.iterator();
        String[] names = players.keySet().toArray(new String[0]);
        int playerIndex = 0;
        while (it.hasNext()) {
            players.get(names[playerIndex % names.length]).add(it.next());
            playerIndex++;
        }
    }

    public void showHands() {
        for (Map.Entry<String, List<Card>> entry : players.entrySet()) {
            System.out.println(entry.getKey() + " 的手牌: " + entry.getValue());
        }
    }

    static class Card {
        String suit, rank;
        Card(String suit, String rank) {
            this.suit = suit;
            this.rank = rank;
        }
        @Override
        public String toString() { return suit + rank; }
    }

    public static void main(String[] args) {
        PokerGame game = new PokerGame("Alice", "Bob", "Charlie");
        game.shuffleAndDeal();
        game.showHands();
    }
}

十、总结与下篇预告

本篇核心要点

要点 说明
ArrayList底层 动态数组,扩容1.5倍
LinkedList底层 双向链表,额外实现Deque
随机访问 ArrayList O(1) vs LinkedList O(n)
默认选择 绝大多数场景选ArrayList
内存考虑 LinkedList每个元素多8-16字节指针开销
CPU缓存 ArrayList连续内存,CPU缓存命中率高

🤔 互动问题

  1. ArrayList的扩容倍数为什么选择1.5而不是2?
  2. LinkedList的node()方法是如何优化查找性能的?
  3. 如果你要实现一个消息队列,应该选择ArrayList还是LinkedList?为什么?

📖 下篇预告

下一篇我们将学习**《集合框架:Set与Map》**,深入探讨HashSet、TreeSet、HashMap、TreeMap的底层实现,包括哈希原理和红黑树的基本概念。


参考资料

相关推荐
程序员黑豆18 分钟前
Java中的字符串【AI全栈开发】
java
namexingyun34 分钟前
开源前端生态如何成为 AI UI 生成的“燃料“:shadcn/ui、Tailwind CSS、Storybook 技术价值全解剖
java·前端·人工智能·python·ui·开源·ai编程
终将老去的穷苦程序员1 小时前
基于SpringBoot的餐饮管理系统
java·spring boot·后端
心之伊始1 小时前
Spring AI Tool Calling 实战:让 Java Agent 调用本地 Bean 工具方法
java·spring boot·agent·spring ai·tool calling
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第110题】【并发篇】第10题:CAS 存在哪些问题?
java·开发语言·面试
瀚高PG实验室1 小时前
java中间件无法连接数据库
java·数据库·中间件·瀚高数据库
东南门吹雪1 小时前
JAVA TCP socket编程框架
java·高并发·socket·tcp·nio
xingyuzhisuan1 小时前
缓存命中率提升方案:从 30% 优化至 82% 全流程优化记录
java·开发语言·缓存·ai
一条泥憨鱼2 小时前
Java开发效率神器:Lombok从入门到精通!
java·后端·学习·开发·lombok
Jinkxs2 小时前
Python基础 - 初识内置函数 Python自带的便捷工具
android·java·python