ArrayList 和 LinkedList

201 阅读4分钟

Java 中的 ArrayList 和 LinkedList 是两种常用的列表实现,

它们在底层结构性能特性适用场景上有显著差异。

一、底层数据结构

对比项ArrayListLinkedList
数据结构动态数组(可自动扩容)双向链表(每个节点包含前驱和后继指针)
内存布局连续内存空间存储元素非连续内存,通过指针关联元素
存储单元数组元素直接存储对象引用节点对象(Node)存储数据和指针

二、性能特性对比

1. 随机访问效率
  • ArrayList:O(1)
    通过数组下标直接访问元素,无需遍历。

  • LinkedList:O(n)
    需要从头 / 尾节点开始遍历到目标位置,平均时间复杂度为 O (n/2)。

示例场景

// ArrayList 随机访问
ArrayList<String> arrayList = new ArrayList<>(Arrays.asList("A", "B", "C"));
System.out.println(arrayList.get(1));  // 直接定位,O(1)

// LinkedList 随机访问
LinkedList<String> linkedList = new LinkedList<>(Arrays.asList("A", "B", "C"));
System.out.println(linkedList.get(1));  // 从头节点遍历,O(n)
2. 插入 / 删除效率
  • ArrayList

    • 尾部插入:平均 O (1)(扩容时 O (n))。
    • 中间 / 头部插入:O (n)(需移动后续元素)。
  • LinkedList

    • 指定位置插入 / 删除:O (n)(需先定位到目标位置)。

    • 头部 / 尾部插入 / 删除:O (1)(直接操作头尾节点)。

示例场景

    // ArrayList 中间插入
    arrayList.add(1, "X");  // 需移动后续元素,O(n)

    // LinkedList 头部插入
    linkedList.addFirst("X");  // 修改头指针,O(1)
3. 内存占用
  • ArrayList

    • 主要开销是数组预分配空间(可能存在空间浪费)。
    • 空 ArrayList 默认容量为 10,扩容因子为 1.5 倍。
  • LinkedList

    • 每个节点需额外存储前驱和后继指针,空间利用率较低。
    • 元素越多,指针占用的额外空间占比越大。

三、适用场景推荐

场景ArrayList 更适合LinkedList 更适合
频繁随机访问元素✅(O (1) 效率)❌(O (n) 效率)
频繁在尾部插入 / 删除元素✅(尾部操作接近 O (1))✅(O (1) 效率)
频繁在头部或中间插入 / 删除❌(需移动元素,O (n))✅(定位后 O (1) 操作)
存储大量元素且空间敏感✅(无额外指针开销)❌(指针占用额外空间)
需实现栈或队列功能❌(尾部操作但扩容可能浪费)✅(Deque 接口实现)

四、常见误区与注意事项

  1. 插入效率的误解

    • 很多人认为 LinkedList 的插入效率一定优于 ArrayList,但实际需分情况:

      • 若插入位置已知(如头部 / 尾部),LinkedList 更高效。
      • 若需先通过下标定位(如 add(5, element)),ArrayList 可能更高效(因定位 O (1) vs O (n))。
  2. 扩容机制的影响

    • ArrayList 扩容需复制数组,大数据量下可能影响性能。若初始容量预估合理(如 new ArrayList<>(1000)),可减少扩容次数。
  3. 遍历方式的选择

    • ArrayList 推荐使用普通 for 循环(随机访问 O (1))。

    • LinkedList 强制使用迭代器或 foreach(本质是迭代器),避免随机访问。

错误示例

java

// 错误:用普通 for 循环遍历 LinkedList
for (int i = 0; i < linkedList.size(); i++) {
    System.out.println(linkedList.get(i));  // 每次 get(i) 都需从头遍历,O(n²)
}

// 正确:用迭代器遍历 LinkedList
for (String element : linkedList) {
    System.out.println(element);  // O(n)
}

五、性能测试对比

以下是一个简单的性能测试代码,对比 ArrayList 和 LinkedList 在不同操作下的耗时:


    import java.util.ArrayList;
    import java.util.LinkedList;
    import java.util.List;

    public class ListPerformanceTest {
        public static void main(String[] args) {
            int size = 100000;
            
            // 测试随机访问
            testRandomAccess(size);
            
            // 测试头部插入
            testAddFirst(size);
            
            // 测试尾部插入
            testAddLast(size);
        }
        
        private static void testRandomAccess(int size) {
            List<Integer> arrayList = new ArrayList<>();
            List<Integer> linkedList = new LinkedList<>();
            
            // 填充数据
            for (int i = 0; i < size; i++) {
                arrayList.add(i);
                linkedList.add(i);
            }
            
            // 测试随机访问
            long startTime = System.nanoTime();
            for (int i = 0; i < size; i++) {
                arrayList.get(i);
            }
            long endTime = System.nanoTime();
            System.out.println("ArrayList 随机访问耗时: " + (endTime - startTime) / 1_000_000 + " ms");
            
            startTime = System.nanoTime();
            for (int i = 0; i < size; i++) {
                linkedList.get(i);
            }
            endTime = System.nanoTime();
            System.out.println("LinkedList 随机访问耗时: " + (endTime - startTime) / 1_000_000 + " ms");
        }
        
        private static void testAddFirst(int size) {
            List<Integer> arrayList = new ArrayList<>();
            List<Integer> linkedList = new LinkedList<>();
            
            // 测试头部插入
            long startTime = System.nanoTime();
            for (int i = 0; i < size; i++) {
                arrayList.add(0, i);
            }
            long endTime = System.nanoTime();
            System.out.println("ArrayList 头部插入耗时: " + (endTime - startTime) / 1_000_000 + " ms");
            
            startTime = System.nanoTime();
            for (int i = 0; i < size; i++) {
                linkedList.addFirst(i);
            }
            endTime = System.nanoTime();
            System.out.println("LinkedList 头部插入耗时: " + (endTime - startTime) / 1_000_000 + " ms");
        }
        
        private static void testAddLast(int size) {
            List<Integer> arrayList = new ArrayList<>();
            List<Integer> linkedList = new LinkedList<>();
            
            // 测试尾部插入
            long startTime = System.nanoTime();
            for (int i = 0; i < size; i++) {
                arrayList.add(i);
            }
            long endTime = System.nanoTime();
            System.out.println("ArrayList 尾部插入耗时: " + (endTime - startTime) / 1_000_000 + " ms");
            
            startTime = System.nanoTime();
            for (int i = 0; i < size; i++) {
                linkedList.addLast(i);
            }
            endTime = System.nanoTime();
            System.out.println("LinkedList 尾部插入耗时: " + (endTime - startTime) / 1_000_000 + " ms");
        }
    }

典型测试结果(数据量 10 万)

    ArrayList 随机访问耗时: 3 ms
    LinkedList 随机访问耗时: 5367 ms
    ArrayList 头部插入耗时: 1524 ms
    LinkedList 头部插入耗时: 5 ms
    ArrayList 尾部插入耗时: 11 ms
    LinkedList 尾部插入耗时: 13 ms

六、总结与选择建议

维度ArrayListLinkedList
随机访问极高效(O (1))低效(O (n))
插入 / 删除尾部高效,其他位置低效头尾高效,其他位置需先定位
内存占用无额外开销,可能预分配空间每个节点需额外两个指针
适用场景读多写少,随机访问频繁头尾操作频繁,需实现队列 / 栈

在实际开发中,若无特殊需求,优先使用 ArrayList,因其更符合常见场景。仅当明确需要频繁的头部 / 中间操作时,才考虑使用 LinkedList