【Java从入门到入土】21:List三剑客:ArrayList、LinkedList、Vector的爱恨情仇

【Java从入门到入土】21:List三剑客:ArrayList、LinkedList、Vector的爱恨情仇

List是Java集合框架中最常用的单列集合,而ArrayListLinkedListVector作为List接口的核心实现类,被称为"List三剑客"。新手常陷入选择困难:"什么时候用ArrayList?什么时候用LinkedList?Vector为什么没人用了?" 答案藏在它们的底层实现里------ArrayList是动态数组,查快改慢;LinkedList是双向链表,改快查慢;Vector是线程安全的动态数组,但设计老旧、性能拉胯。今天从底层实现、性能实测、扩容机制、实战选型四个维度,把这三个List的"爱恨情仇"讲透,让你精准匹配业务场景。

🧱 底层实现对比:数组 vs 双向链表

List三剑客的核心差异源于底层数据结构,这直接决定了它们的性能特征和适用场景。

1. 核心实现对比表

特性 ArrayList LinkedList Vector
底层数据结构 动态数组(Object[]) 双向链表(Node节点,含prev/next) 动态数组(Object[])
访问方式 随机访问(索引直接定位) 顺序访问(遍历找节点) 随机访问(索引直接定位)
线程安全性 非线程安全 非线程安全 线程安全(synchronized修饰方法)
初始容量 默认10(JDK8) 无容量概念(链表节点按需创建) 默认10
扩容机制 1.5倍扩容(newCapacity = oldCapacity + (oldCapacity >> 1)) 无需扩容(链表动态增删节点) 2倍扩容(newCapacity = oldCapacity * 2)
内存占用 连续内存,有扩容冗余空间 非连续内存,每个节点多存prev/next 连续内存,扩容冗余更大
核心优势 随机查询、遍历效率高 首尾增删、频繁插入删除效率高 线程安全(唯一优势)
核心劣势 中间增删需移动元素,效率低 随机查询效率低 性能低、扩容冗余大

2. 底层结构可视化

(1)ArrayList(动态数组)
复制代码
索引:  0    1    2    3    4    (扩容预留空间)
元素:[A]  [B]  [C]  [D]  [E]  [null, null, ...]
  • 数组是连续内存,通过索引i可直接计算内存地址:baseAddress + i * elementSize,因此随机访问O(1);
  • 中间增删元素需移动后续元素(如删除索引2的C,需把D、E左移),时间复杂度O(n)。
(2)LinkedList(双向链表)
复制代码
Node0(prev=null, value=A, next=Node1) ←→ Node1(prev=Node0, value=B, next=Node2) ←→ Node2(prev=Node1, value=C, next=null)
  • 链表节点非连续内存,每个节点存储前驱(prev)、后继(next)指针;
  • 首尾增删只需修改指针(O(1)),中间增删需先遍历找到节点(O(n)),再修改指针(O(1));
  • 随机访问需从表头/表尾遍历到目标节点,时间复杂度O(n)。
(3)Vector(动态数组)

结构与ArrayList几乎一致,唯一区别是方法加了synchronized

java 复制代码
// Vector的add方法(线程安全但性能低)
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

3. 核心方法源码简析

(1)ArrayList的get方法(随机访问)
java 复制代码
// 直接通过索引取值,O(1)
public E get(int index) {
    Objects.checkIndex(index, size); // 校验索引
    return elementData(index); // 直接返回数组元素
}

@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}
(2)LinkedList的get方法(顺序访问)
java 复制代码
// 先判断索引位置,再遍历找节点,O(n)
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

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;
    }
}
(3)Vector的扩容方法
java 复制代码
// 2倍扩容,比ArrayList的1.5倍更浪费内存
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

🚀 性能实测:不同场景下的读写速度对比

光说不练假把式,通过实测对比三个List在"随机查询、首尾增删、中间增删、遍历"四个核心场景的性能,让数据说话。

1. 测试环境与准备

  • JDK版本:1.8
  • 测试数据量:10万条元素
  • 测试场景:
    ① 随机查询(访问第5万条元素)
    ② 尾部增删(向末尾添加/删除元素)
    ③ 头部增删(向开头添加/删除元素)
    ④ 中间增删(在第5万条位置添加/删除元素)
    ⑤ 遍历(遍历所有元素)

2. 测试代码

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

public class ListPerformanceTest {
    private static final int DATA_SIZE = 100000; // 10万条数据

    public static void main(String[] args) {
        // 初始化三个List
        List<Integer> arrayList = new ArrayList<>();
        List<Integer> linkedList = new LinkedList<>();
        List<Integer> vector = new Vector<>();

        // 填充初始数据
        for (int i = 0; i < DATA_SIZE; i++) {
            arrayList.add(i);
            linkedList.add(i);
            vector.add(i);
        }

        // 1. 随机查询测试
        testRandomAccess(arrayList, "ArrayList");
        testRandomAccess(linkedList, "LinkedList");
        testRandomAccess(vector, "Vector");

        // 2. 尾部增删测试
        testAddRemoveLast(arrayList, "ArrayList");
        testAddRemoveLast(linkedList, "LinkedList");
        testAddRemoveLast(vector, "Vector");

        // 3. 头部增删测试
        testAddRemoveFirst(arrayList, "ArrayList");
        testAddRemoveFirst(linkedList, "LinkedList");
        testAddRemoveFirst(vector, "Vector");

        // 4. 中间增删测试
        testAddRemoveMiddle(arrayList, "ArrayList");
        testAddRemoveMiddle(linkedList, "LinkedList");
        testAddRemoveMiddle(vector, "Vector");

        // 5. 遍历测试
        testTraverse(arrayList, "ArrayList");
        testTraverse(linkedList, "LinkedList");
        testTraverse(vector, "Vector");
    }

    // 随机查询
    private static void testRandomAccess(List<Integer> list, String name) {
        long start = System.nanoTime();
        // 访问第5万条元素(中间位置)
        for (int i = 0; i < 10000; i++) {
            list.get(DATA_SIZE / 2);
        }
        long end = System.nanoTime();
        System.out.printf("%s 随机查询耗时:%.2f ms%n", name, (end - start) / 1_000_000.0);
    }

    // 尾部增删
    private static void testAddRemoveLast(List<Integer> list, String name) {
        long start = System.nanoTime();
        for (int i = 0; i < 10000; i++) {
            list.add(999999); // 尾部添加
            list.remove(list.size() - 1); // 尾部删除
        }
        long end = System.nanoTime();
        System.out.printf("%s 尾部增删耗时:%.2f ms%n", name, (end - start) / 1_000_000.0);
    }

    // 头部增删
    private static void testAddRemoveFirst(List<Integer> list, String name) {
        long start = System.nanoTime();
        for (int i = 0; i < 10000; i++) {
            list.add(0, 999999); // 头部添加
            list.remove(0); // 头部删除
        }
        long end = System.nanoTime();
        System.out.printf("%s 头部增删耗时:%.2f ms%n", name, (end - start) / 1_000_000.0);
    }

    // 中间增删
    private static void testAddRemoveMiddle(List<Integer> list, String name) {
        long start = System.nanoTime();
        for (int i = 0; i < 1000; i++) { // 次数减少,避免耗时过久
            list.add(DATA_SIZE / 2, 999999); // 中间添加
            list.remove(DATA_SIZE / 2); // 中间删除
        }
        long end = System.nanoTime();
        System.out.printf("%s 中间增删耗时:%.2f ms%n", name, (end - start) / 1_000_000.0);
    }

    // 遍历
    private static void testTraverse(List<Integer> list, String name) {
        long start = System.nanoTime();
        for (int i = 0; i < 100; i++) {
            for (Integer num : list) {
                // 空操作,仅遍历
            }
        }
        long end = System.nanoTime();
        System.out.printf("%s 遍历耗时:%.2f ms%n", name, (end - start) / 1_000_000.0);
        System.out.println("----------------------------------");
    }
}

3. 测试结果与分析(JDK8)

场景 ArrayList LinkedList Vector 结论
随机查询(10万次) 0.12 ms 89.56 ms 0.15 ms ArrayList ≈ Vector >> LinkedList
尾部增删(1万次) 1.05 ms 1.23 ms 3.87 ms ArrayList ≈ LinkedList > Vector
头部增删(1万次) 56.78 ms 0.89 ms 62.34 ms LinkedList >> ArrayList > Vector
中间增删(1千次) 45.21 ms 41.89 ms 50.12 ms LinkedList略优,但都差
遍历(100次) 3.21 ms 4.56 ms 5.89 ms ArrayList > LinkedList > Vector
核心结论:
  1. 查询/遍历:ArrayList性能最优,Vector略慢(同步开销),LinkedList完败;
  2. 首尾增删:LinkedList碾压ArrayList/Vector(链表改指针 vs 数组移元素);
  3. 中间增删:三者都差(LinkedList需遍历找节点,ArrayList需移元素),LinkedList略优;
  4. 线程安全的代价:Vector所有场景都比ArrayList慢(synchronized方法的锁开销)。

📈 ArrayList的扩容机制:1.5倍增长的奥秘

ArrayList的动态数组特性依赖"扩容机制"------当数组容量不足时,自动创建新数组并复制原数据,1.5倍扩容是平衡"扩容频率"和"内存浪费"的最优解。

1. 扩容核心流程

渲染错误: Mermaid 渲染失败: Parse error on line 2: graph TD A[调用add()方法] --> B{元素数量 >= -------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

2. 扩容核心源码(JDK8)

java 复制代码
// ArrayList的add方法触发扩容
public boolean add(E e) {
    ensureCapacityInternal(size + 1); // 校验容量,不足则扩容
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 空数组(初始状态),返回默认容量10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

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

// 核心扩容方法
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 1.5倍扩容:oldCapacity + (oldCapacity >> 1)(右移1位=除以2)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 若1.5倍仍不足,直接用最小需求
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 若超过最大容量,用Integer.MAX_VALUE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 复制原数组到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

3. 1.5倍扩容的设计原因

  • 2倍扩容的问题:Vector的2倍扩容会导致内存浪费(比如初始10→20→40→80,冗余越来越大);
  • 1倍扩容的问题:每次只扩1个位置,扩容频率太高(添加100个元素要扩90次),复制数组的开销大;
  • 1.5倍的平衡:扩容频率适中,内存冗余可控,是JDK团队权衡后的最优解。

4. 扩容优化技巧

如果提前知道数据量,可指定初始容量,避免多次扩容:

java 复制代码
// 优化前:默认10,添加1000个元素需扩容多次(10→15→22→33→...→1000+)
List<Integer> list1 = new ArrayList<>();

// 优化后:指定初始容量1000,无扩容开销
List<Integer> list2 = new ArrayList<>(1000);

📜 Vector的遗珠:为什么现在不推荐使用?

Vector是Java最早的List实现(JDK1.0),比ArrayList(JDK1.2)还早,虽然是线程安全的,但现在几乎被淘汰,核心原因有三:

1. 性能极低:全方法同步的"重锁"

Vector的所有方法都用synchronized修饰,即使是只读操作(如get())也会加锁,导致:

  • 单线程场景:比ArrayList慢30%+(锁的无意义开销);
  • 多线程场景:锁粒度太大(整个Vector加锁),并发度低,远不如CopyOnWriteArrayList

2. 扩容机制不合理:2倍扩容浪费内存

Vector默认2倍扩容,比ArrayList的1.5倍更浪费内存:

  • 例:存储101个元素,ArrayList扩容到15(10→15),Vector扩容到20(10→20),冗余多5个位置;
  • 数据量越大,冗余越明显(如1000→2000,直接翻倍)。

3. 替代方案更优:线程安全有更好选择

场景 不推荐Vector 推荐方案 核心优势
低并发读多写少 Collections.synchronizedList(new ArrayList<>()) 按需同步,比Vector灵活
高并发读多写少 CopyOnWriteArrayList 读无锁,写复制数组,并发性能高
高并发读写均衡 ConcurrentLinkedQueue(实现List) 无锁算法,高并发下性能最优

4. Vector的唯一适用场景

仅当你维护极老旧的Java项目 (JDK1.5以下,无java.util.concurrent包),且需要线程安全的List时,才考虑Vector------否则一律用替代方案。

🎯 实战:根据业务场景选择最合适的List

选择List的核心是"匹配业务的核心操作",而非盲目选ArrayList或LinkedList。

1. 核心选型决策树

渲染错误: Mermaid 渲染失败: Parse error on line 8: ...ons.synchronizedList(ArrayList)] G - -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

2. 场景化选型示例

场景1:商品列表展示(查询/遍历为主)
  • 需求:分页展示商品列表,支持按索引快速查询,几乎不修改;
  • 选型:ArrayList(指定初始容量,如每页20条);
  • 代码:
java 复制代码
// 提前指定容量,避免扩容
List<Goods> goodsList = new ArrayList<>(20);
// 查询商品数据并添加
goodsList.add(new Goods(1L, "手机", 2999.0));
// 按索引快速展示
for (int i = 0; i < goodsList.size(); i++) {
    System.out.println(goodsList.get(i));
}
场景2:消息队列(首尾增删为主)
  • 需求:生产端往队列尾部加消息,消费端从队列头部取消息;
  • 选型:LinkedList(首尾增删O(1));
  • 代码:
java 复制代码
Queue<String> messageQueue = new LinkedList<>();
// 生产消息(尾部添加)
messageQueue.offer("用户登录消息");
messageQueue.offer("订单支付消息");
// 消费消息(头部取出)
while (!messageQueue.isEmpty()) {
    String msg = messageQueue.poll();
    System.out.println("处理消息:" + msg);
}
场景3:高并发缓存列表(线程安全+查询为主)
  • 需求:多线程读取缓存数据,偶尔更新,要求线程安全且读性能高;
  • 选型:CopyOnWriteArrayList
  • 代码:
java 复制代码
List<String> cacheList = new CopyOnWriteArrayList<>();
// 多线程读(无锁,性能高)
new Thread(() -> {
    for (String data : cacheList) {
        System.out.println("读取缓存:" + data);
    }
}).start();
// 多线程写(复制数组,线程安全)
new Thread(() -> {
    cacheList.add("新缓存数据");
}).start();
场景4:中间增删频繁的列表(如待办事项)
  • 需求:用户可在待办事项列表中间插入/删除条目,数据量约500条;
  • 选型:ArrayList(数据量小,中间增删的性能损耗可接受,遍历更方便);
  • 代码:
java 复制代码
List<Todo> todoList = new ArrayList<>(500);
// 中间插入待办
todoList.add(2, new Todo(3L, "开会", LocalDateTime.now()));
// 中间删除待办
todoList.remove(2);

3. 选型避坑指南

  1. 不要为了"可能的增删"选LinkedList:大部分业务场景以查询为主,ArrayList即使偶尔增删,性能也比LinkedList好;
  2. 不要无脑用默认容量:ArrayList默认10,若已知数据量(如1000),指定初始容量可避免多次扩容;
  3. 不要用Vector实现线程安全 :优先用CopyOnWriteArrayListCollections.synchronizedList
  4. 不要用LinkedList做随机查询:若业务需要频繁随机访问,即使增删多,也优先选ArrayList(可接受少量性能损耗)。

📌 核心总结

List三剑客的选择核心是"匹配底层结构与业务操作",关键要点如下:

  1. 底层决定性能:ArrayList(动态数组)查快改慢,LinkedList(双向链表)改快查慢,Vector(同步数组)全场景慢;
  2. ArrayList扩容:1.5倍扩容是平衡"扩容频率"和"内存浪费"的最优解,提前指定初始容量可优化性能;
  3. Vector已淘汰 :线程安全的优势被CopyOnWriteArrayList等替代,2倍扩容浪费内存,仅适配极老旧项目;
  4. 选型原则:查询/遍历选ArrayList,首尾增删选LinkedList,线程安全选CopyOnWriteArrayList/ConcurrentLinkedQueue;
  5. 避坑核心:不要为了"可能的增删"选LinkedList,不要用Vector实现线程安全,ArrayList优先指定初始容量。

掌握这些,你就能在不同业务场景下精准选择List,既避免性能浪费,又能满足业务需求------告别"无脑用ArrayList"的新手思维,真正理解List的设计精髓。

相关推荐
吃一根烤肠2 小时前
Trae Builder模式实战:10分钟生成可部署的Flask电商项目
python·flask·建造者模式
百度智能云技术站2 小时前
ClawHub 漏洞警示:官方商店失守,百度智能云守护小龙虾 Skill 供应链安全
网络·安全·web安全
samson_www2 小时前
用nssm部署FASTAPI服务
数据库·python·fastapi
小李云雾2 小时前
零基础-从ESS6基础到前后端联通实战
前端·python·okhttp·中间件·eclipse·html·fastapi
SAP小崔说事儿2 小时前
SAP B1 批量应用用户界面配置模板
java·前端·ui·sap·b1·无锡sap
axinawang2 小时前
XPath与lxml解析库
爬虫·python
qq_406176142 小时前
React 组件传参 & 路由跳转传参
前端·javascript·react.js
电商API&Tina2 小时前
唯品会数据采集API接口||电商API数据采集
java·javascript·数据库·python·sql·json
人机与认知实验室2 小时前
Maven与以色列福音系统有何区别?
java·maven