Java集合(1)

Java集合(1)

作者:没有四次元口袋的蓝胖

日期:2026-06-30

标签:Java, 集合框架, ArrayList


一、三者总体对比

1.1 一句话总结

集合 底层结构 特点 线程安全
ArrayList Object 数组 查询快、增删慢
LinkedList 双向链表 查询慢、增删快
Vector Object 数组 老古董,效率低 ✅(全表加锁)

面试题高频: ArrayList 和 LinkedList 的区别是集合篇的"送分题",必须张口就来。


二、ArrayList

2.1 底层结构

ArrayList 底层是 Object 数组,支持动态扩容。

java 复制代码
// JDK 源码
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    // 存储元素的数组
    transient Object[] elementData;
    
    // 元素个数(不是数组长度!)
    private int size;
}

注意两个概念的区别:

  • elementData.length:数组容量(能装多少)
  • size:实际元素个数(已经装了多少)

2.2 构造方法

java 复制代码
// 1. 无参构造(默认空数组,第一次添加时才初始化容量为10)
ArrayList<String> list1 = new ArrayList<>();

// 2. 指定初始容量
ArrayList<String> list2 = new ArrayList<>(20);

// 3. 传入另一个集合
ArrayList<String> list3 = new ArrayList<>(anotherCollection);

JDK 7 vs JDK 8 的区别:

  • JDK 7:无参构造直接创建容量为 10 的数组(饿汉式)
  • JDK 8:无参构造创建空数组 {},第一次 add 时才扩容到 10(懒汉式,节省内存)

2.3 常用方法

java 复制代码
ArrayList<String> list = new ArrayList<>();

// 增
list.add("A");              // 末尾添加
list.add(0, "B");           // 指定位置插入
list.addAll(anotherList);   // 批量添加

// 删
list.remove("A");           // 按元素删除(删第一个匹配的)
list.remove(0);             // 按下标删除
list.clear();               // 清空

// 改
list.set(0, "C");           // 修改指定位置

// 查
list.get(0);                // 根据下标获取(O(1),很快)
list.size();                // 元素个数
list.contains("A");         // 是否包含
list.indexOf("A");          // 第一次出现的下标
list.isEmpty();             // 是否为空

// 遍历
for (int i = 0; i < list.size(); i++) {
    String s = list.get(i); // 支持 RandomAccess,普通for最快
}
for (String s : list) {     // 增强for(底层迭代器)
}
list.forEach(System.out::println); // Java 8+ forEach

2.4 ArrayList 的优缺点

优点:

  • 查询快(下标直接定位,O(1))
  • 支持随机访问(实现了 RandomAccess 接口)
  • 内存连续,缓存命中率高

缺点:

  • 增删慢(需要移动元素,O(n))
  • 容量不够时需要扩容,有开销
  • 中间插入/删除效率低,尾部操作还可以

三、ArrayList 扩容机制

3.1 扩容流程总览

复制代码
add(element)
    ↓
确保容量够不够?
    ├── 够 → 直接放,size++
    └── 不够 → 扩容
              ├── 新容量 = 旧容量 × 1.5(右移一位)
              ├── 创建新数组
              ├── 把旧数组元素复制到新数组(Arrays.copyOf)
              └── 再把新元素放进去

3.2 分析

第一步:add 方法入口

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

第二步:计算最小需要的容量

java 复制代码
private void ensureCapacityInternal(int minCapacity) {
    // 如果当前是空数组(无参构造刚创建),最小容量取默认10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        // DEFAULT_CAPACITY = 10
    }
    ensureExplicitCapacity(minCapacity);
}

第三步:判断是否需要扩容

java 复制代码
private void ensureExplicitCapacity(int minCapacity) {
    modCount++; // 记录修改次数(fail-fast机制)

    // 需要的最小容量 > 当前数组长度 → 需要扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

第四步:真正扩容 grow()

java 复制代码
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;      // 旧容量
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量 = 旧的 1.5 倍
    // 右移一位 = 除以2取整,所以 10→15,15→22,22→33...

    if (newCapacity - minCapacity < 0)         // 扩容后还不够?
        newCapacity = minCapacity;             // 直接用需要的容量

    if (newCapacity - MAX_ARRAY_SIZE > 0)      // 超过最大限制?
        newCapacity = hugeCapacity(minCapacity); // 特殊处理

    elementData = Arrays.copyOf(elementData, newCapacity); // 复制到新数组
}

3.3 扩容的关键点

问题 答案
默认初始容量 10(JDK 8 懒加载,第一次 add 才初始化为10)
扩容倍数 1.5 倍(oldCapacity + (oldCapacity >> 1)
扩容方式 Arrays.copyOf() → 底层 System.arraycopy() native 方法
扩容时机 添加元素时,容量不够就扩
最大容量 Integer.MAX_VALUE(或 MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8)

3.4 为什么是 1.5 倍?

  1. 1.5 倍的增长比较温和:不会一次扩太多浪费空间,也不会扩得太频繁
  2. 2倍的问题:增长太快,空间浪费严重。而且每次都是2倍,之前释放的空间都放不下新数组(内存碎片问题)
  3. 1.5倍可以复用之前的内存空间:扩到第4次时,前面释放的空间总和已经能装下新数组了

3.5 优化建议:指定初始容量

java 复制代码
// ❌ 不好:如果要放1000个元素,会触发多次扩容
ArrayList<Integer> list = new ArrayList<>();

// ✅ 好:预估一下大小,直接指定初始容量
ArrayList<Integer> list = new ArrayList<>(1000);

为什么重要? 扩容需要复制整个数组,元素多了开销很大。能预估大小就指定,避免频繁扩容。

📌 常见面试题

Q:ArrayList 怎么扩容的?

默认初始容量10(JDK8懒加载),不够时扩为原来的1.5倍,用 Arrays.copyOf 复制到新数组。

Q:ArrayList 初始容量是多少?

JDK 7 无参构造直接创建长度为10的数组;JDK 8 无参构造创建空数组,第一次 add 时才扩容到10。

Q:ArrayList 和数组的区别?

  • 数组大小固定,ArrayList 可以动态扩容
  • 数组可以存基本类型和引用类型,ArrayList 只能存引用类型(基本类型要装箱)
  • ArrayList 提供了更多方法(add/remove/contains 等)
  • 数组效率更高(没有自动装箱和扩容开销)

四、LinkedList 详解

4.1 底层结构

LinkedList 底层是双向链表,每个节点保存数据、前驱和后继指针。

java 复制代码
// JDK 源码
public class LinkedList<E> extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
    
    // 头节点
    transient Node<E> first;
    // 尾节点
    transient Node<E> last;
    // 元素个数
    transient int size;

    // 节点内部类
    private static class Node<E> {
        E item;        // 数据
        Node<E> next;  // 后继
        Node<E> prev;  // 前驱
    }
}

LinkedList 同时实现了 List 和 Deque 接口,所以它既是 List 也是队列(双端队列)。

4.2 常用方法

java 复制代码
LinkedList<String> list = new LinkedList<>();

// ---- List 相关 ----
list.add("A");                // 尾部添加
list.addFirst("B");           // 头部添加
list.addLast("C");            // 尾部添加
list.add(2, "D");             // 指定位置插入
list.get(0);                  // 获取(需要遍历,慢)
list.remove(0);               // 删除
list.set(0, "E");             // 修改

// ---- Deque 双端队列相关 ----
list.offer("X");              // 尾部入队
list.offerFirst("Y");         // 头部入队
list.offerLast("Z");          // 尾部入队
list.poll();                  // 头部出队
list.peek();                  // 查看头部

// ---- Stack 栈相关 ----
list.push("top");             // 压栈
list.pop();                   // 弹栈
list.peek();                  // 查看栈顶

4.3 增删真的快吗?

头插和尾插: O(1),因为有 first 和 last 指针,直接操作,很快 ✅

指定位置插入: 不一定快!

java 复制代码
// add(int index, E element)
// 1. 先遍历找到 index 位置的节点 → O(n)
// 2. 再修改指针插入 → O(1)
// 整体还是 O(n)!

真相: LinkedList 只有头尾增删是 O(1),中间位置增删要先找位置,时间复杂度还是 O(n),只不过比 ArrayList 少了数组复制的开销。

4.4 get(int index) 的优化 ------ 折半查找

java 复制代码
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;
    }
}

LinkedList 的查询做了优化:从近的那一头开始找,最坏情况找 n/2 次,但本质还是 O(n)。

4.5 LinkedList 的优缺点

优点:

  • 头尾增删快(O(1))
  • 不需要扩容,用多少 new 多少
  • 可以当队列/栈用(实现了 Deque 接口)

缺点:

  • 查询慢(不支持随机访问,O(n))
  • 每个节点要存前驱和后继指针,内存占用大
  • 内存不连续,缓存命中率低

五、Vector 详解

5.1 基本介绍

Vector 是 JDK 1.0 就有的老类,ArrayList 是 JDK 1.2 才出来的。

Vector 所有方法都加了 synchronized,所以线程安全但效率低,基本被淘汰了。

java 复制代码
public synchronized boolean add(E e) { ... }
public synchronized E get(int index) { ... }
public synchronized E remove(int index) { ... }

5.2 扩容机制

java 复制代码
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // capacityIncrement 默认是 0 → 扩容为 2 倍
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    // ...
}
特性 ArrayList Vector
线程安全 ✅(synchronized 全方法加锁)
扩容倍数 1.5 倍 2 倍(可设置)
效率 低(加锁开销)
出现版本 JDK 1.2 JDK 1.0
使用场景 单线程 多线程(但推荐用 CopyOnWriteArrayList)

为什么不用 Vector? 性能太差,所有方法都加锁。多线程场景推荐用 Collections.synchronizedListCopyOnWriteArrayList


六、三者综合对比

6.1 核心对比表

对比项 ArrayList LinkedList Vector
底层结构 Object 数组 双向链表 Object 数组
随机访问 ✅ O(1) ❌ O(n) ✅ O(1)
头部插入 ❌ O(n) ✅ O(1) ❌ O(n)
尾部插入 ✅ O(1)(扩容时O(n)) ✅ O(1) ✅ O(1)
中间插入 ❌ O(n) ⚠️ O(n)(要先找位置) ❌ O(n)
内存占用 少(数组连续) 多(节点+两个指针)
线程安全 ✅(synchronized)
扩容机制 1.5倍 不需要扩容 2倍
适用场景 查多增少、随机访问 头尾增删多、当队列/栈用 已废弃

6.2 怎么选?

复制代码
选 List 实现类
    ↓
需要线程安全吗?
    ├── 需要 → Collections.synchronizedList 或 CopyOnWriteArrayList
    └── 不需要
        ├── 查多、随机访问多 → ArrayList(绝大多数情况用它)
        ├── 头尾增删多、需要队列/栈 → LinkedList
        └── 都不确定 → 先选 ArrayList

6.3 遍历方式对比

遍历方式 ArrayList LinkedList
普通 for(get(i)) ✅ 最快(随机访问) ❌ 极慢(每次都从头/尾找)
增强 for(foreach) ✅ 快 ✅ 快(迭代器顺序访问)
迭代器 Iterator ✅ 快 ✅ 快
forEach(Lambda) ✅ 快 ✅ 快

结论:

  • ArrayList 用普通 for 最快
  • LinkedList 千万不要用普通 for 遍历!用迭代器或增强 for

七、fail-fast 机制(快速失败)

ArrayList、LinkedList、HashMap 等都有 fail-fast 机制。

是什么: 遍历集合时,如果集合结构被修改了(add/remove/clear),会立刻抛出 ConcurrentModificationException

原理:

java 复制代码
// modCount:记录集合被修改的次数
protected transient int modCount = 0;

// 每次 add/remove 都会 modCount++
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    modCount++;  // 修改次数+1
    elementData[size++] = e;
    return true;
}

// 迭代器初始化时记录 expectedModCount = modCount
// 每次 next() 时检查:if (modCount != expectedModCount) throw ...

注意:

  • 迭代器自己的 remove() 方法不会触发异常(它会同步更新 expectedModCount)
  • 并发修改才会触发,单线程用集合的 remove 也会触发
  • 这不是并发安全保证,只是一种"错误检测机制"

八、思维导图速览

复制代码
List 三剑客
├── ArrayList(数组实现,最常用)
│   ├── 底层:Object[] 数组
│   ├── 默认容量:10(JDK8懒加载)
│   ├── 扩容:1.5 倍(Arrays.copyOf)
│   ├── 查询 O(1)、增删 O(n)
│   ├── 支持随机访问(RandomAccess)
│   └── 线程不安全
├── LinkedList(双向链表)
│   ├── 底层:双向链表(Node节点 prev+next+item)
│   ├── 不需要扩容
│   ├── 头尾增删 O(1)、中间增删 O(n)(要先找位置)
│   ├── 查询 O(n)(折半查找优化)
│   ├── 实现了 Deque,可当队列/栈
│   └── 线程不安全
├── Vector(已废弃)
│   ├── 底层:Object[] 数组
│   ├── 所有方法 synchronized
│   ├── 扩容 2 倍
│   └── 效率低,不推荐使用
├── 扩容机制(ArrayList)
│   ├── 默认初始容量:10
│   ├── 扩容倍数:1.5 倍
│   ├── 扩容方式:Arrays.copyOf → System.arraycopy
│   └── 优化:预估大小,指定初始容量
├── 遍历方式
│   ├── ArrayList → 普通 for 最快
│   └── LinkedList → 不能用普通 for,用迭代器/增强for
└── fail-fast 机制
    ├── 并发修改抛 ConcurrentModificationException
    └── modCount 记录修改次数

九、写在最后

  1. ArrayList 是重点中的重点:日常开发 90% 用它,扩容机制面试必问
  2. LinkedList 了解特点和适用场景:知道它什么时候快什么时候慢,不要被"增删快"的口号忽悠(中间增删并不快)
  3. Vector 知道为什么被淘汰:全方法加锁效率低,有更好的替代品
  4. 源码值得看:ArrayList 的 grow()、LinkedList 的 node() 方法都不长,建议读一读

面试高频排序

  1. ArrayList 和 LinkedList 的区别(必问)
  2. ArrayList 扩容机制(必问)
  3. ArrayList 和 Vector 的区别
  4. ArrayList 遍历方式及性能对比
  5. fail-fast 机制