Java集合篇———List

28 阅读1小时+

第1章:ArrayList深度剖析

1.1 动态数组实现原理

1.1.1 核心数据结构深度解析

ArrayList是Java集合框架中最常用的List实现类,它基于动态数组实现,提供了高效的随机访问能力。理解ArrayList的底层实现对于编写高性能代码至关重要。

核心字段详解
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    // 序列化版本号
    private static final long serialVersionUID = 8683452581122892189L;
    
    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
    
    // 空数组常量(用于空实例)
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    // 默认空数组常量(用于默认大小的实例)
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    // 核心:存储元素的数组
    // transient修饰:不参与默认序列化,ArrayList自定义了序列化逻辑
    transient Object[] elementData;
    
    // 实际元素数量
    private int size;
    
    // 继承自AbstractList,用于fail-fast机制
    protected transient int modCount = 0;
}
关键设计点深度解析

1. 为什么elementData用transient修饰?

transient关键字表示该字段不参与默认的序列化过程。ArrayList自定义了序列化逻辑,原因如下:

  • 节省空间: 只序列化实际存储的元素,不序列化整个数组(包括空位置)
  • 性能优化: 减少序列化数据量,提高序列化/反序列化速度
  • 精确控制: 可以根据size精确控制序列化的元素数量

2. 为什么使用Object[]而不是E[]?

这是Java泛型擦除机制的限制:

  • 泛型擦除: Java的泛型在编译后会被擦除,运行时无法创建泛型数组
  • 类型安全: 使用Object[]可以避免类型检查问题,通过类型转换保证类型安全
  • 兼容性: 与Java的类型系统兼容
// 错误:无法创建泛型数组
// E[] elementData = new E[capacity];  // 编译错误

// 正确:使用Object[],运行时转换
Object[] elementData = new Object[capacity];
E element = (E) elementData[index];  // 类型转换

3. 为什么维护size字段?

size字段记录实际元素数量,具有以下优势:

  • O(1)获取大小: size()方法直接返回size,不需要遍历数组
  • 快速判断空: isEmpty()方法只需判断size == 0
  • 精确控制: 可以精确控制数组的使用范围,避免访问空位置
public int size() {
    return size;  // O(1)时间复杂度
}

public boolean isEmpty() {
    return size == 0;  // O(1)时间复杂度
}
内存布局详细分析

ArrayList对象内存布局(64位JVM,压缩指针开启):

对象头(12字节)
├── Mark Word(8字节)
│   ├── 哈希码(25位)
│   ├── 分代年龄(4位)
│   ├── 偏向锁标志(1位)
│   └── 锁标志位(2位)
└── Klass Pointer(4字节):指向类元数据

实例数据(24字节)
├── size字段(4字节):实际元素数量
├── modCount字段(4字节):修改次数
└── elementData引用(8字节):指向数组对象
└── 对齐填充(8字节):对象对齐到8字节边界

总计:36字节(对象本身)

数组对象内存布局:

数组对象头(16字节)
├── Mark Word(8字节)
├── Klass Pointer(4字节)
└── 数组长度(4字节)

数组数据(n × 8字节,n为容量)
├── 引用槽08字节) → 元素0对象
├── 引用槽18字节) → 元素1对象
├── ...
└── 引用槽n-18字节) → 元素n-1对象(可能为null

内存占用计算示例:

存储1000个Integer对象的内存估算:

ArrayList对象:36字节
数组对象:16 + 1000 × 8 = 8016字节
Integer对象:1000 × 16 = 16000字节(假设每个Integer对象16字节)

总计:约24052字节(约23.5KB)

注意:
- 如果容量是1500(1.5倍扩容),数组对象是12016字节
- 空间浪费:约33%(1500个槽位,只用了1000个)

1.1.2 构造器实现策略深度分析

ArrayList提供三种构造器,采用延迟初始化策略优化内存使用:

构造器1:无参构造器(延迟初始化)
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;  // 共享空数组
}

设计要点:

  • 延迟初始化: 不立即分配10个容量的数组,而是使用共享空数组
  • 节省内存: 如果ArrayList创建后不使用,不会浪费内存
  • 首次添加时分配: 第一次调用add()时才分配默认容量10的数组

延迟初始化的优势:

// 场景:创建大量ArrayList,但可能不使用
List<String> list1 = new ArrayList<>();  // 不分配内存
List<String> list2 = new ArrayList<>();  // 不分配内存
// ... 创建1000个ArrayList,都不分配内存

// 只有实际使用时才分配
list1.add("item");  // 此时才分配10个容量的数组
构造器2:指定容量构造器(精确分配)
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;  // 共享空数组
    } else {
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}

使用场景:

  • 已知元素数量: 可以精确分配容量,避免扩容
  • 性能优化: 减少扩容次数,提升性能
  • 内存控制: 可以精确控制初始内存占用

实战建议:

// 场景1:从数据库查询,已知结果数量
int count = getCountFromDatabase();
List<Product> products = new ArrayList<>(count);  // 精确分配

// 场景2:批量处理,预估大小
int estimatedSize = calculateEstimatedSize();
List<Data> dataList = new ArrayList<>(estimatedSize);

// 场景3:内存敏感场景,精确控制
int maxSize = 1000;
List<String> limitedList = new ArrayList<>(maxSize);
构造器3:集合参数构造器(批量初始化)
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // 如果c是ArrayList,直接使用其数组
        if (c.getClass() == ArrayList.class) {
            elementData = c.toArray();
        } else {
            // 否则复制数组,确保类型正确
            elementData = Arrays.copyOf(elementData, size, Object[].class);
        }
    } else {
        elementData = EMPTY_ELEMENTDATA;
    }
}

优化点:

  • 类型检查优化: 如果源集合是ArrayList,直接使用其数组(避免不必要的复制)
  • 类型转换: 确保数组类型是Object[],兼容泛型擦除

使用场景:

// 场景1:从其他集合创建
Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
List<String> list = new ArrayList<>(set);  // 批量初始化

// 场景2:集合转换
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Integer> arrayList = new ArrayList<>(intList);

// 场景3:集合复制
List<String> original = new ArrayList<>();
List<String> copy = new ArrayList<>(original);  // 深拷贝(元素引用)

1.1.3 核心方法实现原理

add方法实现详解
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保容量足够
    elementData[size++] = e;           // 在末尾添加元素
    return true;
}

执行流程:

  1. 容量检查: 调用ensureCapacityInternal(size + 1)确保容量足够
  2. 添加元素: 在数组末尾(size位置)添加元素
  3. 更新size: size自增
  4. 返回true: ArrayList的add方法总是返回true(允许重复元素)

时间复杂度分析:

  • 最好情况: O(1) - 容量足够,直接添加
  • 平均情况: O(1)摊销 - 考虑扩容,分摊时间复杂度是O(1)
  • 最坏情况: O(n) - 需要扩容,复制所有元素
add(int index, E element)方法实现详解
public void add(int index, E element) {
    rangeCheckForAdd(index);           // 检查索引范围
    ensureCapacityInternal(size + 1);   // 确保容量足够
    
    // 将index及其后面的元素后移一位
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    
    elementData[index] = element;       // 在index位置插入元素
    size++;                            // 更新size
}

执行流程:

  1. 索引检查: 检查index是否在[0, size]范围内
  2. 容量检查: 确保容量足够
  3. 元素后移: 使用System.arraycopy将index及其后面的元素后移一位
  4. 插入元素: 在index位置插入新元素
  5. 更新size: size自增

时间复杂度: O(n) - 需要移动n-index个元素

性能分析:

// 场景1:在头部插入
list.add(0, "new");  // 需要移动所有元素,O(n)

// 场景2:在尾部插入
list.add(list.size(), "new");  // 等价于add("new"),O(1)摊销

// 场景3:在中间插入
list.add(size/2, "new");  // 需要移动一半元素,O(n)
remove方法实现详解
public E remove(int index) {
    rangeCheck(index);                 // 检查索引范围
    
    modCount++;                        // 修改计数增加
    E oldValue = elementData(index);   // 获取要删除的元素
    
    int numMoved = size - index - 1;   // 需要移动的元素数量
    if (numMoved > 0) {
        // 将后面的元素前移
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    }
    
    elementData[--size] = null;        // 将最后一个位置置null,帮助GC
    return oldValue;
}

关键设计点:

  1. modCount增加: 这是结构性修改,modCount必须增加
  2. 元素前移: 使用System.arraycopy高效移动元素
  3. 置null帮助GC: 将最后一个位置置null,断开引用,帮助垃圾回收

为什么将最后一个位置置null?

elementData[--size] = null;  // 帮助GC

// 如果不置null:
// - 数组仍然持有对该对象的引用
// - 即使对象不再使用,也无法被GC回收
// - 可能导致内存泄漏
get和set方法实现
public E get(int index) {
    rangeCheck(index);                 // 检查索引范围
    return elementData(index);         // 直接数组访问
}

E elementData(int index) {
    return (E) elementData[index];      // 类型转换
}

public E set(int index, E element) {
    rangeCheck(index);                 // 检查索引范围
    E oldValue = elementData(index);   // 获取旧值
    elementData[index] = element;      // 直接数组赋值
    return oldValue;                   // 返回旧值
}

特点:

  • O(1)时间复杂度: 直接数组索引访问,常量时间
  • 不修改modCount: set操作不改变列表大小,不是结构性修改
  • 类型转换: 从Object[]转换为E,由泛型擦除机制保证类型安全

1.2 扩容机制深度详解

1.2.1 扩容触发条件与完整流程

ArrayList的扩容机制是其核心特性之一,理解扩容过程对于性能优化至关重要。

扩容触发条件

扩容在以下情况下触发:

  1. 添加元素时: add(E e)add(int index, E e)
  2. 批量添加时: addAll(Collection<? extends E> c)
  3. 容量不足时: size + 1 > elementData.length
完整扩容流程
// 1. 添加元素时的入口
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保容量足够
    elementData[size++] = e;
    return true;
}

// 2. 内部容量检查
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 第一次添加元素,使用默认容量10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

// 3. 显式容量检查
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;  // 修改计数增加
    
    // 如果所需容量大于当前容量,需要扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);  // 执行扩容
}

// 4. 核心扩容算法
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    
    // 计算新容量:1.5倍增长
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 如果1.5倍仍不足,使用minCapacity
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
    // 处理超大容量情况
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    
    // 创建新数组并复制元素
    elementData = Arrays.copyOf(elementData, newCapacity);
}

// 5. 处理超大容量
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0)  // 溢出检查
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
扩容序列示例
初始状态:elementData.length = 0(延迟初始化)

添加第1个元素:
  - minCapacity = 1
  - 第一次添加,使用默认容量10
  - elementData.length = 10

添加第11个元素:
  - minCapacity = 11
  - oldCapacity = 10
  - newCapacity = 10 + (10 >> 1) = 15
  - elementData.length = 15

添加第16个元素:
  - minCapacity = 16
  - oldCapacity = 15
  - newCapacity = 15 + (15 >> 1) = 22
  - elementData.length = 22

添加第23个元素:
  - minCapacity = 23
  - oldCapacity = 22
  - newCapacity = 22 + (22 >> 1) = 33
  - elementData.length = 33

... 以此类推

1.2.2 为什么选择1.5倍扩容因子?

数学分析

1. 空间利用率分析:

假设从容量10增长到最终容量N,分析不同扩容因子的空间浪费:

1.5倍增长:

  • 平均负载因子:约0.75
  • 空间浪费:约33%
  • 扩容次数:log₁.₅(N/10) ≈ 8次(N=1000时)

2倍增长:

  • 平均负载因子:约0.67
  • 空间浪费:约50%
  • 扩容次数:log₂(N/10) ≈ 7次(N=1000时)

1.2倍增长:

  • 平均负载因子:约0.83
  • 空间浪费:约17%
  • 扩容次数:log₁.₂(N/10) ≈ 20次(N=1000时)

结论: 1.5倍在空间利用率和扩容频率之间取得最佳平衡。

性能影响分析

扩容成本:

  • 时间成本: O(n) - 需要复制所有元素
  • 空间成本: O(n) - 新旧数组同时存在
  • GC压力: 旧数组成为垃圾,增加GC压力

不同扩容因子的性能对比:

扩容因子扩容次数(到1000)总复制元素数空间浪费综合评价
1.2倍20次约2000017%扩容频繁,性能差
1.5倍8次约800033%平衡最佳
2倍7次约700050%空间浪费大
实际测试数据
// 测试:添加100万个元素
// 1.5倍扩容:约20次扩容,总复制约2000万个元素
// 2倍扩容:约20次扩容,总复制约2000万个元素
// 但1.5倍的空间利用率更高

// 性能测试结果(添加100万元素):
// 1.5倍:约150ms
// 2倍:约145ms(稍快,但空间浪费大)
// 1.2倍:约200ms(频繁扩容,性能差)

1.2.3 扩容优化策略

优化1:预分配容量(最重要)
// ❌ 错误做法:频繁扩容
List<Integer> badList = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
    badList.add(i);  // 多次扩容:10→15→22→33→...
    // 扩容次数:约20次
    // 总复制元素数:约2000万个
    // 耗时:约200ms
}

// ✅ 正确做法:预分配容量
List<Integer> goodList = new ArrayList<>(1000000);
for (int i = 0; i < 1000000; i++) {
    goodList.add(i);  // 无扩容开销
    // 扩容次数:0次
    // 总复制元素数:0个
    // 耗时:约50ms
}

// 性能提升:约4倍
优化2:批量操作使用addAll
// ❌ 错误做法:逐个添加
List<String> target = new ArrayList<>();
for (String item : source) {
    target.add(item);  // 可能多次触发扩容
    // 如果source有1000个元素,可能扩容多次
}

// ✅ 正确做法:批量添加
List<String> target = new ArrayList<>(source.size());
target.addAll(source);  // 一次扩容,一次复制
// 性能提升:约2-5倍
优化3:使用ensureCapacity预扩容
List<String> list = new ArrayList<>();
list.ensureCapacity(1000000);  // 手动预扩容

for (int i = 0; i < 1000000; i++) {
    list.add("item" + i);  // 无扩容开销
}
优化4:适时使用trimToSize释放空间
// 场景:添加大量数据后删除大部分
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
    list.add("data" + i);
}
// 此时容量可能是1500000

// 删除大部分数据
list.subList(1000, list.size()).clear();
// 此时size=1000,但容量仍是1500000

// 释放多余空间
list.trimToSize();  // 容量调整为1000,释放约12MB内存

1.3 modCount与fail-fast机制完全解析

1.3.1 modCount的设计目的与作用

modCount是什么?

modCount(modification count)是ArrayList继承自AbstractList的字段,用于记录集合被结构性修改的次数。

// 继承自AbstractList
protected transient int modCount = 0;
modCount的作用

1. 实现fail-fast机制

modCount是fail-fast机制的核心,用于检测并发修改:

// 迭代器创建时记录modCount
int expectedModCount = modCount;

// 每次操作前检查
if (modCount != expectedModCount)
    throw new ConcurrentModificationException();

2. 区分结构性修改和非结构性修改

结构性修改(modCount增加):

  • add(E e) - 添加元素
  • add(int index, E element) - 在指定位置添加
  • remove(int index) - 删除指定位置元素
  • remove(Object o) - 删除指定元素
  • clear() - 清空集合
  • addAll(Collection<? extends E> c) - 批量添加
  • removeAll(Collection<?> c) - 批量删除

非结构性修改(modCount不变):

  • set(int index, E element) - 设置元素(不改变大小)
  • get(int index) - 获取元素(只读操作)
  • contains(Object o) - 查找元素(只读操作)
  • indexOf(Object o) - 查找索引(只读操作)

代码示例:

List<String> list = new ArrayList<>();
list.add("A");
System.out.println(list.modCount);  // 1

list.add("B");
System.out.println(list.modCount);  // 2

list.set(0, "C");  // 非结构性修改
System.out.println(list.modCount);  // 2(不变)

list.remove(0);
System.out.println(list.modCount);  // 3
为什么modCount用transient修饰?

transient表示该字段不参与序列化:

  • 运行时状态: modCount是运行时状态,序列化后恢复没有意义
  • 序列化优化: 不序列化可以节省空间
  • 反序列化重置: 反序列化时modCount会重置为0
modCount的溢出问题

modCount是int类型,范围是-2³¹到2³¹-1(约21亿)。

溢出处理:

// modCount可能溢出回绕
modCount = Integer.MAX_VALUE;
modCount++;  // 溢出,变为Integer.MIN_VALUE

// 但使用!=比较仍能正确工作
if (modCount != expectedModCount)  // 即使溢出也能检测到变化
    throw new ConcurrentModificationException();

为什么使用!=而不是>?

  • 即使modCount溢出回绕,!=比较仍能正确检测到变化
  • 如果使用>,溢出后可能无法检测到修改

1.3.2 fail-fast机制完全解析

fail-fast是什么?

**fail-fast(快速失败)**是Java集合框架的重要特性,它是一种错误检测机制:

  • 设计目的: 在检测到并发修改时立即抛出异常,而不是继续执行可能产生错误结果的操作
  • 核心思想: 快速失败,避免数据不一致
  • 实现方式: 通过modCount检测并发修改
fail-fast的实现原理

1. 迭代器创建时记录modCount

private class Itr implements Iterator<E> {
    int cursor;       // 下一个要返回的元素的索引
    int lastRet = -1; // 最近返回的元素的索引
    
    // 关键:记录迭代器创建时的modCount
    int expectedModCount = modCount;
    
    // 构造方法(隐式)
    Itr() {
        expectedModCount = modCount;  // 记录创建时的modCount
    }
}

2. 每次操作前检查modCount

public E next() {
    checkForComodification();  // 检查并发修改
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

3. 迭代器的remove方法更新expectedModCount

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();  // 检查并发修改
    
    try {
        ArrayList.this.remove(lastRet);  // 调用集合的remove方法
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;  // 关键:更新expectedModCount
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

关键点: 迭代器的remove方法会更新expectedModCount,使其与modCount保持一致,所以不会抛出异常。

fail-fast触发场景详解

场景1:单线程迭代时修改集合

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));

// ❌ 错误:在迭代时修改集合
for (String item : list) {
    if ("B".equals(item)) {
        list.remove(item);  // 抛出ConcurrentModificationException
        // 原因:
        // 1. 迭代器创建时:expectedModCount = 0
        // 2. list.remove()执行:modCount = 1
        // 3. 下一次next()调用:modCount(1) != expectedModCount(0)
        // 4. 抛出ConcurrentModificationException
    }
}

场景2:多线程并发修改

List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    list.add(i);
}

// 线程1:迭代列表
Thread reader = new Thread(() -> {
    try {
        for (Integer num : list) {
            System.out.println("读取: " + num);
            Thread.sleep(1);
        }
    } catch (ConcurrentModificationException e) {
        System.out.println("捕获到并发修改异常!");
    }
});

// 线程2:修改列表
Thread writer = new Thread(() -> {
    try {
        Thread.sleep(50);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    for (int i = 0; i < 100; i++) {
        list.add(1000 + i);  // 修改modCount
        list.remove(0);
    }
});

reader.start();
writer.start();
// 很可能抛出ConcurrentModificationException

场景3:使用多个迭代器

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));

// 创建两个迭代器
Iterator<String> it1 = list.iterator();
Iterator<String> it2 = list.iterator();

// 第一个迭代器删除元素
it1.next();
it1.remove();  // modCount = 1

// 第二个迭代器继续操作
try {
    it2.next();  // 抛出ConcurrentModificationException
    // 原因:it2的expectedModCount = 0,但modCount = 1
} catch (ConcurrentModificationException e) {
    System.out.println("多个迭代器间的并发修改检测");
}
fail-fast的正确处理方式

方式1:使用迭代器的remove方法(推荐)

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String item = it.next();
    if ("B".equals(item)) {
        it.remove();  // ✅ 安全删除,会更新expectedModCount
    }
}

方式2:使用Java 8的removeIf(最简洁)

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));

list.removeIf(item -> "B".equals(item));  // ✅ 简洁安全

方式3:收集要删除的元素,最后批量删除

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));

List<String> toRemove = new ArrayList<>();
for (String item : list) {
    if (shouldRemove(item)) {
        toRemove.add(item);
    }
}
list.removeAll(toRemove);  // ✅ 批量删除

方式4:倒序遍历删除

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));

for (int i = list.size() - 1; i >= 0; i--) {
    if (shouldRemove(list.get(i))) {
        list.remove(i);  // ✅ 倒序删除,不会影响未遍历的索引
    }
}

方式5:使用Stream API过滤

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));

List<String> filtered = list.stream()
    .filter(item -> !shouldRemove(item))
    .collect(Collectors.toList());
list.clear();
list.addAll(filtered);
fail-fast的局限性

1. 非原子性检查

fail-fast检查发生在操作之前,但不保证检查后不被修改:

Iterator<String> it = list.iterator();
if (it.hasNext()) {
    // 在这里,另一个线程可能修改了list
    String item = it.next();  // 可能成功,也可能失败,不一致
}

2. 不保证100%检测

某些极端并发情况下可能错过检测:

// 线程A:检查modCount
if (modCount != expectedModCount) {  // 检查通过
    // 线程B:修改list(modCount++)
    // 线程A:继续执行(可能看到不一致的数据)
}

3. 性能开销

每次迭代操作都要检查,有一定性能开销:

public E next() {
    checkForComodification();  // 每次都要检查
    // ...
}

4. 误报可能

某些合法操作也可能触发异常:

// 使用多个迭代器
Iterator<String> it1 = list.iterator();
Iterator<String> it2 = list.iterator();

it1.next();
it1.remove();  // 合法操作

// it2认为这是并发修改
it2.next();  // 抛出ConcurrentModificationException(误报)
fail-fast vs fail-safe

fail-fast(快速失败):

  • 检测到并发修改立即抛出异常
  • 实现:ArrayList、HashMap、HashSet等
  • 优点:能及时发现错误
  • 缺点:可能误报,性能有开销

fail-safe(安全失败):

  • 允许并发修改,迭代器基于快照工作
  • 实现:CopyOnWriteArrayList、ConcurrentLinkedQueue等
  • 优点:不会抛出异常
  • 缺点:可能看到过期数据(弱一致性)

对比示例:

// fail-fast:ArrayList
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> it = list.iterator();
list.add("D");  // 修改集合
it.next();  // 抛出ConcurrentModificationException

// fail-safe:CopyOnWriteArrayList
List<String> cowList = new CopyOnWriteArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> it2 = cowList.iterator();
cowList.add("D");  // 修改集合
it2.next();  // 正常,不会抛出异常(基于快照)

1.4 序列化机制深度解析

1.4.1 为什么elementData用transient修饰?

ArrayList的elementData字段用transient修饰,这意味着它不参与默认的序列化过程。ArrayList自定义了序列化逻辑,原因如下:

原因1:节省序列化空间

默认序列化的问题:

// 如果使用默认序列化
ArrayList<String> list = new ArrayList<>(1000);
list.add("A");
list.add("B");
// 实际只有2个元素,但数组容量是1000

// 默认序列化会序列化整个数组(包括998个null)
// 序列化数据:1000个槽位(大部分是null)

自定义序列化的优势:

// 自定义序列化只序列化实际元素
// 序列化数据:2个元素
// 空间节省:约99.8%
原因2:提高序列化性能

性能对比:

  • 默认序列化: 需要序列化1000个槽位(包括null)
  • 自定义序列化: 只序列化2个实际元素
  • 性能提升: 序列化速度提升约500倍,反序列化速度提升约500倍
原因3:精确控制序列化内容

自定义序列化可以精确控制序列化的内容,只序列化实际存储的元素。

1.4.2 自定义序列化实现

writeObject方法(序列化)
private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
    // 写入非transient和非static字段(size、modCount等)
    s.defaultWriteObject();
    
    // 写入size(实际元素数量)
    s.writeInt(size);
    
    // 只写入实际元素,不写入空位置
    for (int i = 0; i < size; i++) {
        s.writeObject(elementData[i]);
    }
}

执行流程:

  1. 调用defaultWriteObject()写入非transient字段(size等)
  2. 写入size(实际元素数量)
  3. 遍历数组,只写入实际元素(0到size-1)
readObject方法(反序列化)
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    // 初始化elementData为空数组
    elementData = EMPTY_ELEMENTDATA;
    
    // 读取非transient和非static字段
    s.defaultReadObject();
    
    // 读取size
    s.readInt();  // 读取size,但这里似乎有问题,应该是size = s.readInt()
    
    if (size > 0) {
        // 根据size重新分配数组
        elementData = new Object[size];
        
        // 读取元素
        for (int i = 0; i < size; i++) {
            elementData[i] = s.readObject();
        }
    }
}

执行流程:

  1. 初始化elementData为空数组
  2. 调用defaultReadObject()读取非transient字段
  3. 读取size
  4. 根据size重新分配数组(精确大小,无浪费)
  5. 读取元素并填充数组

优势:

  • 反序列化后的数组大小正好等于实际元素数量
  • 没有空间浪费
  • 性能优异

1.5 性能特征全面分析

1.5.1 时间复杂度完整表

操作最好情况平均情况最坏情况详细说明
add(E e)O(1)O(1)摊销O(n)尾部添加,分摊O(1)(考虑扩容)
add(int index, E e)O(n)O(n)O(n)需要移动n-index个元素
remove(int index)O(n)O(n)O(n)需要移动size-index-1个元素
remove(Object o)O(1)O(n)O(n)可能很快找到(第一个),也可能遍历全部
get(int index)O(1)O(1)O(1)直接数组索引,常量时间
set(int index, E e)O(1)O(1)O(1)直接数组赋值,常量时间
contains(Object o)O(1)O(n)O(n)需要遍历,最好情况是第一个元素
indexOf(Object o)O(1)O(n)O(n)需要遍历查找
lastIndexOf(Object o)O(1)O(n)O(n)从后往前遍历查找
clear()O(n)O(n)O(n)需要将所有引用置null,帮助GC
isEmpty()O(1)O(1)O(1)直接判断size == 0
size()O(1)O(1)O(1)直接返回size字段

1.5.2 空间复杂度分析

空间复杂度: O(n)

  • n个元素需要O(n)空间
  • 考虑扩容,实际空间可能是1.5n(平均负载因子0.75)

空间浪费:

  • 平均空间浪费:约33%(1.5倍扩容)
  • 可以通过trimToSize()释放多余空间

1.5.3 内存访问模式优势

ArrayList内存连续,CPU缓存友好,这是其性能优势的重要来源。

CPU缓存层次结构
L1缓存:32KB,访问延迟约1ns
L2缓存:256KB,访问延迟约3ns
L3缓存:8MB,访问延迟约12ns
主内存:访问延迟约100ns
ArrayList的缓存优势
// ArrayList:顺序访问,缓存命中率高
for (int i = 0; i < list.size(); i++) {
    Object obj = list.get(i);
    // CPU缓存预取机制有效
    // 连续内存访问,缓存命中率高
    // 性能优异
}

缓存预取机制:

  • CPU会预取连续内存的数据到缓存
  • ArrayList顺序访问时,后续数据很可能已经在缓存中
  • 缓存命中率:通常>90%
对比LinkedList的缓存劣势
// LinkedList:随机内存访问,缓存不友好
for (Object obj : linkedList) {
    // 每个节点访问都可能缓存缺失
    // 节点分散在堆内存中
    // 缓存命中率:通常<50%
}

性能差异:

  • ArrayList顺序访问:缓存命中率>90%,性能优异
  • LinkedList顺序访问:缓存命中率<50%,性能较差
  • 实际性能差异:ArrayList快30-50%

1.5.4 实际性能测试

测试1:尾部添加性能
public class ArrayListPerformanceTest {
    public static void main(String[] args) {
        int size = 1000000;
        
        // 测试1:默认初始化
        long start = System.nanoTime();
        List<Integer> list1 = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            list1.add(i);
        }
        long time1 = System.nanoTime() - start;
        System.out.println("默认初始化: " + time1 / 1_000_000 + "ms");
        
        // 测试2:预分配容量
        start = System.nanoTime();
        List<Integer> list2 = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            list2.add(i);
        }
        long time2 = System.nanoTime() - start;
        System.out.println("预分配容量: " + time2 / 1_000_000 + "ms");
        System.out.println("性能提升: " + (time1 - time2) * 100.0 / time1 + "%");
    }
}

// 典型输出:
// 默认初始化: 150ms
// 预分配容量: 50ms
// 性能提升: 66.7%
测试2:随机访问性能
// ArrayList随机访问:O(1)
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
    list.add(i);
}

long start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
    int value = list.get(i);  // O(1)
}
long time = System.nanoTime() - start;
System.out.println("随机访问100万次: " + time / 1_000_000 + "ms");
// 典型输出:约10ms(极快)
测试3:头部插入性能
// ArrayList头部插入:O(n)
List<Integer> list = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 10000; i++) {
    list.add(0, i);  // 头部插入,需要移动所有元素
}
long time = System.nanoTime() - start;
System.out.println("头部插入10000次: " + time / 1_000_000 + "ms");
// 典型输出:约500ms(较慢)

1.6 ArrayList的常见陷阱与最佳实践

1.6.1 常见陷阱

陷阱1:在迭代时修改集合
// ❌ 错误:抛出ConcurrentModificationException
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
    if ("B".equals(item)) {
        list.remove(item);  // 抛出异常
    }
}

// ✅ 正确:使用迭代器的remove方法
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String item = it.next();
    if ("B".equals(item)) {
        it.remove();  // 安全删除
    }
}
陷阱2:频繁扩容导致性能下降
// ❌ 错误:频繁扩容
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
    list.add(i);  // 多次扩容,性能差
}

// ✅ 正确:预分配容量
List<Integer> list = new ArrayList<>(1000000);
for (int i = 0; i < 1000000; i++) {
    list.add(i);  // 无扩容开销
}
陷阱3:使用subList后修改原列表
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> subList = list.subList(1, 4);  // [2, 3, 4]

// 修改原列表
list.add(6);  // 结构性修改

// 访问子列表会抛出异常
try {
    subList.get(0);  // 抛出ConcurrentModificationException
} catch (ConcurrentModificationException e) {
    System.out.println("原列表修改使子列表失效");
}

// ✅ 正确:如果需要独立副本
List<Integer> independentCopy = new ArrayList<>(list.subList(1, 4));
list.add(7);  // 不影响independentCopy
陷阱4:忽略null元素
List<String> list = new ArrayList<>();
list.add("A");
list.add(null);  // ArrayList允许null
list.add("B");

// 注意:contains(null)返回true
System.out.println(list.contains(null));  // true

// 注意:indexOf(null)返回第一个null的索引
System.out.println(list.indexOf(null));  // 1

1.6.2 最佳实践

实践1:预分配容量
// 已知元素数量时,预分配容量
int expectedSize = 10000;
List<String> list = new ArrayList<>(expectedSize);
实践2:批量操作使用addAll
// 批量添加,一次扩容
list.addAll(anotherList);
实践3:适时使用trimToSize
// 如果列表大小固定不再变化
list.trimToSize();  // 释放多余空间
实践4:选择合适的遍历方式
// ArrayList三种方式性能接近
// 方式1:普通for循环(需要索引时)
for (int i = 0; i < list.size(); i++) {
    String item = list.get(i);
}

// 方式2:增强for循环(推荐,简洁)
for (String item : list) {
    // 处理item
}

// 方式3:forEach方法(Java 8+,函数式风格)
list.forEach(item -> {
    // 处理item
});
实践5:使用Stream API进行复杂操作
// 过滤、转换、聚合等操作
List<String> result = list.stream()
    .filter(s -> s != null && s.length() > 3)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

📊 总结

核心要点

  1. 数据结构: 动态数组,内存连续分配
  2. 扩容机制: 1.5倍增长,平衡性能和空间
  3. fail-fast: 通过modCount检测并发修改
  4. 序列化: 自定义序列化,只序列化实际元素
  5. 性能: 随机访问O(1),尾部添加O(1)摊销

第2章:LinkedList深度剖析

2.1 双向链表数据结构深度解析

2.1.1 节点结构设计详解

LinkedList是Java集合框架中唯一基于链表实现的List,它使用双向链表结构,支持高效的插入和删除操作。

节点类定义
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;
    }
}
为什么使用双向链表而不是单向链表?

1. 支持双向遍历

双向链表可以从两个方向遍历,这对于实现ListIterator的前向和后向遍历至关重要:

// 前向遍历
Node<E> x = first;
while (x != null) {
    // 处理x.item
    x = x.next;
}

// 后向遍历
Node<E> x = last;
while (x != null) {
    // 处理x.item
    x = x.prev;
}

2. 高效删除节点

删除节点时,可以直接通过prev找到前驱节点,无需从头遍历:

// 双向链表删除:O(1)
void unlink(Node<E> x) {
    x.prev.next = x.next;  // 直接通过prev找到前驱
    x.next.prev = x.prev;  // 直接通过next找到后继
}

// 单向链表删除:需要O(n)找到前驱
void remove(Node<E> x) {
    // 需要从头遍历找到x的前驱节点
    Node<E> prev = first;
    while (prev.next != x) {
        prev = prev.next;  // O(n)时间复杂度
    }
    prev.next = x.next;
}

3. ListIterator支持

ListIterator需要支持前向和后向遍历,双向链表是必要条件:

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;
    private Node<E> next;
    private int nextIndex;
    
    // 支持前向遍历
    public E next() {
        // ...
        next = next.next;
        return lastReturned.item;
    }
    
    // 支持后向遍历
    public E previous() {
        // ...
        next = next.prev;
        return lastReturned.item;
    }
}

4. Deque接口实现

双端队列(Deque)需要在两端进行操作,双向链表是理想选择:

// 头部操作
public void addFirst(E e) {
    linkFirst(e);  // 需要修改first和原first的prev
}

// 尾部操作
public void addLast(E e) {
    linkLast(e);  // 需要修改last和原last的next
}
节点内存布局详细分析

Node对象内存布局(64位JVM,压缩指针开启):

对象头(12字节)
├── Mark Word(8字节)
│   ├── 哈希码(25位)
│   ├── 分代年龄(4位)
│   ├── 偏向锁标志(1位)
│   └── 锁标志位(2位)
└── Klass Pointer(4字节):指向类元数据

实例数据(12字节)
├── item引用(4字节)   → 实际元素对象
├── next引用(4字节)   → 下一个Node对象
├── prev引用(4字节)   → 上一个Node对象

对齐填充(4字节)
总计:28字节(加上实际元素对象的内存)

内存占用计算示例:

存储1000个Integer对象的内存估算:

LinkedList对象:36字节
Node对象:1000 × 28 = 28000字节
Integer对象:1000 × 16 = 16000字节(假设每个Integer对象16字节)

总计:约44036字节(约43KB)

对比ArrayList:
- ArrayList:约24052字节(约23.5KB)
- LinkedList:约44036字节(约43KB)
- 内存差异:LinkedList是ArrayList的1.83倍
链表结构状态管理
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;       // 尾节点
    
    // 构造方法
    public LinkedList() { }  // 空链表
    
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);  // 批量添加
    }
}

链表状态详解:

1. 空链表:

first = null
last = null
size = 0

2. 单节点链表:

first = last = node
node.prev = null
node.next = null

3. 多节点链表:

first → Node1 ↔ Node2 ↔ Node3 ↔ ... ↔ NodeN ← last
        ↑         ↑         ↑               ↑
      prev=null  prev      prev           next=null

2.1.2 链表操作的核心方法

linkLast方法(尾部添加)
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;             // 原尾节点的next指向新节点
    }
    size++;                           // 链表大小增加
    modCount++;                       // 修改计数增加
}

执行流程详解:

  1. 保存当前尾节点引用(l)
  2. 创建新节点,prev指向原尾节点,next为null
  3. 更新last引用指向新节点
  4. 判断原链表是否为空:
    • 如果为空(l == null),新节点也是头节点,更新first
    • 如果不为空,原尾节点的next指向新节点
  5. 更新size和modCount

边界情况处理:

  • 空链表:first和last都指向新节点
  • 非空链表:只更新last和原尾节点的next
linkFirst方法(头部添加)
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;             // 原头节点的prev指向新节点
    }
    size++;
    modCount++;
}

执行流程:

  1. 保存当前头节点引用(f)
  2. 创建新节点,prev为null,next指向原头节点
  3. 更新first引用指向新节点
  4. 判断原链表是否为空:
    • 如果为空,新节点也是尾节点,更新last
    • 如果不为空,原头节点的prev指向新节点
  5. 更新size和modCount
linkBefore方法(指定节点前插入)
void linkBefore(E e, Node<E> succ) {
    // 在非空节点succ前插入元素e
    final Node<E> pred = succ.prev;   // succ的前驱节点
    final Node<E> newNode = new Node<>(pred, e, succ);  // 创建新节点
    succ.prev = newNode;              // succ的前驱改为新节点
    
    if (pred == null) {               // 如果succ原是头节点
        first = newNode;              // 新节点成为头节点
    } else {
        pred.next = newNode;          // 原前驱节点的next指向新节点
    }
    size++;
    modCount++;
}

执行流程:

  1. 获取succ的前驱节点(pred)
  2. 创建新节点,prev指向pred,next指向succ
  3. 更新succ的prev指向新节点
  4. 判断pred是否为空:
    • 如果为空(succ原是头节点),新节点成为头节点
    • 如果不为空,pred的next指向新节点
  5. 更新size和modCount
unlink方法(删除节点)
E unlink(Node<E> x) {
    // 删除非空节点x
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;
    
    if (prev == null) {               // x是头节点
        first = next;
    } else {
        prev.next = next;
        x.prev = null;                // 帮助GC
    }
    
    if (next == null) {               // x是尾节点
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;                // 帮助GC
    }
    
    x.item = null;                    // 帮助GC
    size--;
    modCount++;
    return element;
}

执行流程:

  1. 保存节点的元素、前驱和后继
  2. 处理前驱节点:
    • 如果prev为null(x是头节点),更新first指向next
    • 否则,prev的next指向next,断开x的prev引用
  3. 处理后继节点:
    • 如果next为null(x是尾节点),更新last指向prev
    • 否则,next的prev指向prev,断开x的next引用
  4. 将x的item、prev、next都置null,帮助GC
  5. 更新size和modCount

为什么将引用置null?

  • 断开节点与链表的连接
  • 帮助垃圾回收,避免内存泄漏
  • 即使节点对象还在,也不会影响链表结构

2.2 核心操作实现原理

2.2.1 添加操作详解

add(E e)方法
public boolean add(E e) {
    linkLast(e);
    return true;
}

特点:

  • 时间复杂度:O(1)
  • 在链表尾部添加
  • 总是返回true(允许重复元素)
add(int index, E element)方法
public void add(int index, E element) {
    checkPositionIndex(index);        // 检查索引范围[0, size]
    
    if (index == size) {
        linkLast(element);            // 尾部插入
    } else {
        linkBefore(element, node(index));  // 在指定节点前插入
    }
}

执行流程:

  1. 检查索引范围:[0, size]
  2. 如果index == size(尾部插入),调用linkLast
  3. 否则,先找到index位置的节点,然后在该节点前插入

时间复杂度:

  • 最好情况:O(1) - 尾部插入
  • 平均情况:O(n) - 需要找到插入位置
  • 最坏情况:O(n) - 头部插入
addFirst(E e)和addLast(E e)方法
public void addFirst(E e) {
    linkFirst(e);
}

public void addLast(E e) {
    linkLast(e);
}

特点:

  • 时间复杂度:O(1)
  • 直接操作头尾节点,无需遍历
  • 性能优异

2.2.2 删除操作详解

remove(int index)方法
public E remove(int index) {
    checkElementIndex(index);         // 检查索引范围[0, size)
    return unlink(node(index));       // 找到节点并删除
}

执行流程:

  1. 检查索引范围:[0, size)
  2. 调用node(index)找到要删除的节点
  3. 调用unlink删除节点

时间复杂度: O(n) - 需要找到节点

remove(Object o)方法
public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

特点:

  • 时间复杂度:O(n) - 需要遍历查找
  • 删除第一个匹配的元素
  • 区分null元素(使用==)和非null元素(使用equals)
removeFirst()和removeLast()方法
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

特点:

  • 时间复杂度:O(1)
  • 直接操作头尾节点,性能优异
  • 空链表时抛出NoSuchElementException

2.2.3 查找操作详解

get(int index)方法
public E get(int index) {
    checkElementIndex(index);         // 检查索引范围[0, size)
    return node(index).item;          // 获取节点后返回元素
}

关键: 需要调用node(index)找到节点,这是性能瓶颈。

node方法(优化版)
Node<E> node(int index) {
    // 根据索引位置决定从头还是从尾开始遍历
    if (index < (size >> 1)) {        // 索引小于size/2
        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;
    }
}

优化效果:

  • 未优化: 总是从头开始遍历,最坏情况O(n)
  • 优化后: 根据索引位置选择遍历方向,平均O(n/2)
  • 性能提升: 对于中间位置的元素,性能提升约50%

优化原理:

  • 如果索引在前半部分(index < size/2),从头开始遍历
  • 如果索引在后半部分(index >= size/2),从尾开始遍历
  • 这样最多只需要遍历一半的节点
contains(Object o)方法
public boolean contains(Object o) {
    return indexOf(o) != -1;
}

public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

特点:

  • 时间复杂度:O(n)
  • 从前往后查找第一个匹配的元素
  • 区分null元素和非null元素

2.2.4 修改操作详解

set(int index, E element)方法
public E set(int index, E element) {
    checkElementIndex(index);         // 检查索引范围[0, size)
    Node<E> x = node(index);          // 找到节点
    E oldVal = x.item;
    x.item = element;                 // 修改节点的item
    return oldVal;
}

特点:

  • 时间复杂度:O(n) - 需要找到节点
  • 不修改modCount(非结构性修改)
  • 返回旧值

2.3 多角色使用特性详解

LinkedList实现了List、Deque、Queue等多个接口,可以扮演不同角色。这是LinkedList相比ArrayList的一个重要优势。

2.3.1 作为List使用

基本List操作
// 创建LinkedList作为List
List<String> list = new LinkedList<>();

// 添加元素
list.add("A");
list.add("B");
list.add("C");

// 随机访问(效率低,O(n))
String element = list.get(1);  // 需要遍历到索引1

// 索引插入(需要先找到位置,O(n))
list.add(1, "B1");

// 索引删除(需要先找到节点,O(n))
list.remove(1);

// 遍历(推荐使用迭代器)
for (String item : list) {
    System.out.println(item);
}

// ❌ 错误:使用普通for循环+get(i)(性能极差,O(n²))
for (int i = 0; i < list.size(); i++) {
    String item = list.get(i);  // 每次get都是O(n)
}
List接口的核心方法
// 基本操作
boolean add(E e);                    // 添加元素
E get(int index);                    // 获取指定位置元素(O(n))
E set(int index, E element);         // 设置指定位置元素(O(n))
E remove(int index);                 // 删除指定位置元素(O(n))
int size();                          // 获取大小
boolean isEmpty();                   // 判断是否为空

// 查找操作
int indexOf(Object o);               // 查找元素首次出现位置(O(n))
int lastIndexOf(Object o);           // 查找元素最后出现位置(O(n))
boolean contains(Object o);           // 判断是否包含元素(O(n))

// 视图操作
List<E> subList(int fromIndex, int toIndex);  // 获取子列表视图

2.3.2 作为Queue使用(FIFO)

Queue接口操作
// 创建LinkedList作为Queue
Queue<String> queue = new LinkedList<>();

// 入队(添加到队尾)
queue.offer("A");  // 队列满时返回false(LinkedList不会满)
queue.add("B");    // 队列满时抛异常(LinkedList不会满)

// 查看队头(不删除)
String head1 = queue.peek();    // 队列空时返回null
String head2 = queue.element(); // 队列空时抛异常

// 出队(移除队头)
String removed1 = queue.poll();  // 队列空时返回null
String removed2 = queue.remove(); // 队列空时抛异常

// 队列大小
int size = queue.size();
boolean isEmpty = queue.isEmpty();

// 检查元素
boolean contains = queue.contains("A");
Queue操作的特点
操作方法队列空/满时的行为时间复杂度
入队offer/addadd抛异常,offer返回falseO(1)
出队poll/removeremove抛异常,poll返回nullO(1)
查看队头peek/elementelement抛异常,peek返回nullO(1)

2.3.3 作为Deque使用(双端队列)

Deque接口操作
// 创建LinkedList作为Deque
Deque<String> deque = new LinkedList<>();

// 添加到队头
deque.addFirst("A");
deque.offerFirst("B");

// 添加到队尾
deque.addLast("C");
deque.offerLast("D");

// 查看队头
String first1 = deque.getFirst();
String first2 = deque.peekFirst();

// 查看队尾
String last1 = deque.getLast();
String last2 = deque.peekLast();

// 移除队头
String removedFirst1 = deque.removeFirst();
String removedFirst2 = deque.pollFirst();

// 移除队尾
String removedLast1 = deque.removeLast();
String removedLast2 = deque.pollLast();

// 其他操作
boolean empty = deque.isEmpty();
int size = deque.size();
boolean contains = deque.contains("A");
Deque操作的特点
操作类型方法时间复杂度说明
头部添加addFirst/offerFirstO(1)直接修改头节点
尾部添加addLast/offerLastO(1)直接修改尾节点
头部移除removeFirst/pollFirstO(1)直接修改头节点
尾部移除removeLast/pollLastO(1)直接修改尾节点
查看头部getFirst/peekFirstO(1)直接返回头节点
查看尾部getLast/peekLastO(1)直接返回尾节点

2.3.4 作为Stack使用(LIFO)

Stack操作
// 创建LinkedList作为Stack(推荐代替Stack类)
Deque<String> stack = new LinkedList<>();

// 入栈
stack.push("A");   // 等价于addFirst
stack.push("B");

// 查看栈顶
String top = stack.peek();  // 等价于peekFirst

// 出栈
String popped = stack.pop();  // 等价于removeFirst

// 栈大小
int size = stack.size();
boolean empty = stack.isEmpty();

// 搜索元素(从栈顶开始,1-based)
int position = 0;
for (String item : stack) {
    position++;
    if ("A".equals(item)) {
        break;
    }
}
// 注意:LinkedList没有专门的search方法,需要自己遍历
与传统Stack类的对比
// 传统Stack类(不推荐)
Stack<String> oldStack = new Stack<>();
oldStack.push("A");
oldStack.pop();

// 问题:
// 1. Stack继承自Vector,所有方法都同步,性能差
// 2. 设计不佳,违反单一职责原则
// 3. 官方推荐使用Deque

// 现代Stack实现(推荐)
Deque<String> newStack = new LinkedList<>();  // 或ArrayDeque
newStack.push("A");
newStack.pop();

⚠️ 重要提示: Java官方推荐使用Deque的实现类(如LinkedList、ArrayDeque)代替旧的Stack类,因为Stack是基于Vector实现的,有性能问题且设计不佳。


2.4 性能特征全面分析

2.4.1 时间复杂度完整表

操作时间复杂度详细说明
add(E e)O(1)添加到链表尾部,直接修改last引用
add(int index, E element)O(n)需要先找到插入位置,平均O(n/2)
addFirst(E e)O(1)直接修改头节点,常量时间
addLast(E e)O(1)直接修改尾节点,常量时间
remove(int index)O(n)需要先找到节点,然后修改引用
remove(Object o)O(n)需要遍历找到元素
removeFirst()O(1)直接修改头节点引用
removeLast()O(1)直接修改尾节点引用
get(int index)O(n)需要遍历到指定位置,平均O(n/2)
set(int index, E element)O(n)需要先找到节点,然后修改item
contains(Object o)O(n)需要遍历查找
indexOf(Object o)O(n)需要遍历查找
clear()O(n)需要遍历所有节点置null
size()O(1)直接返回size字段
isEmpty()O(1)直接判断size == 0

2.4.2 与ArrayList性能全面对比

操作ArrayListLinkedList性能差异选择建议
随机访问(get/set)O(1)O(n)ArrayList快100-1000倍频繁随机访问选ArrayList
头部插入删除O(n)O(1)LinkedList快100-1000倍频繁头部操作选LinkedList
尾部插入删除O(1)摊销O(1)两者接近,ArrayList稍快都可以,ArrayList更省内存
中间插入删除O(n)O(n)都需要找到位置,但常数因子不同根据实际测试选择
内存占用较小较大LinkedList是ArrayList的2-5倍内存紧张选ArrayList
遍历性能较慢ArrayList缓存友好,快30-50%频繁遍历选ArrayList
缓存友好性ArrayList顺序访问,缓存命中率高需要缓存友好选ArrayList

2.4.3 遍历方式性能对比

❌ 绝对错误:使用普通for循环
// 时间复杂度O(n²)!
List<String> linkedList = new LinkedList<>();
for (int i = 0; i < 10000; i++) {
    linkedList.add("item" + i);
}

long start = System.nanoTime();
for (int i = 0; i < linkedList.size(); i++) {
    String item = linkedList.get(i);  // 每次get都是O(n)
}
long time = System.nanoTime() - start;
System.out.println("普通for循环: " + time / 1_000_000 + "ms");
// 典型输出:约500ms(极慢)

性能分析:

  • get(0):遍历0个节点
  • get(1):遍历1个节点
  • get(2):遍历2个节点
  • ...
  • get(n-1):遍历n-1个节点
  • 总时间复杂度: 0 + 1 + 2 + ... + (n-1) = n(n-1)/2 = O(n²)
✅ 正确方式:使用迭代器或增强for循环
// 时间复杂度O(n)
long start = System.nanoTime();
for (String item : linkedList) {
    // 使用迭代器,每次next()是O(1)
}
long time = System.nanoTime() - start;
System.out.println("增强for循环: " + time / 1_000_000 + "ms");
// 典型输出:约5ms(快100倍)

性能分析:

  • 迭代器创建:O(1)
  • 每次next():O(1)
  • 总时间复杂度: O(n)

性能差异实测:

  • 10000个元素,普通for循环:约500ms
  • 10000个元素,迭代器遍历:约5ms
  • 性能差异:约100倍!

2.4.4 内存占用详细分析

内存占用对比

存储1000个Integer对象的内存估算:

ArrayList:

ArrayList对象:36字节
数组对象:16 + 1000 × 8 = 8016字节
Integer对象:1000 × 16 = 16000字节
总计:约24052字节(约23.5KB)

LinkedList:

LinkedList对象:36字节
Node对象:1000 × 28 = 28000字节
Integer对象:1000 × 16 = 16000字节
总计:约44036字节(约43KB)

内存差异: LinkedList是ArrayList的1.83倍

影响因素
  1. 元素数量: 元素越多,差距越大
  2. 元素大小: 存储的对象越大,相对差距越小
  3. JVM配置: 64位JVM开启指针压缩可以减小差距
  4. 内存碎片: LinkedList节点分散,可能产生更多内存碎片

2.4.5 CPU缓存影响分析

缓存层次结构
L1缓存:32KB,访问延迟约1ns
L2缓存:256KB,访问延迟约3ns
L3缓存:8MB,访问延迟约12ns
主内存:访问延迟约100ns
LinkedList的缓存劣势
// LinkedList:随机内存访问,缓存不友好
for (Object obj : linkedList) {
    // 每个节点访问都可能缓存缺失
    // 节点分散在堆内存中
    // 缓存命中率:通常<50%
}

缓存访问模式:

  • 节点1在内存地址0x1000
  • 节点2在内存地址0x5000(不连续)
  • 节点3在内存地址0x2000(不连续)
  • 每次访问都可能缓存缺失,需要从主内存加载

性能影响:

  • 缓存命中率:<50%
  • 平均访问延迟:约50ns(考虑缓存缺失)
  • 性能较差
对比ArrayList的缓存优势
// ArrayList:顺序访问,缓存友好
for (int i = 0; i < list.size(); i++) {
    Object obj = list.get(i);
    // CPU缓存预取机制有效
    // 连续内存访问,缓存命中率高
    // 缓存命中率:通常>90%
}

缓存访问模式:

  • 元素0在内存地址0x1000
  • 元素1在内存地址0x1008(连续)
  • 元素2在内存地址0x1010(连续)
  • CPU可以预取后续数据到缓存

性能影响:

  • 缓存命中率:>90%
  • 平均访问延迟:约5ns(大部分缓存命中)
  • 性能优异

性能差异: ArrayList遍历比LinkedList快30-50%


2.5 LinkedList的常见陷阱与最佳实践

2.5.1 常见陷阱

陷阱1:使用普通for循环遍历(最严重)
// ❌ 绝对错误:O(n²)时间复杂度
List<String> list = new LinkedList<>();
for (int i = 0; i < list.size(); i++) {
    String item = list.get(i);  // 每次get都是O(n)
    // 对于10000个元素,需要约5000万次操作
}

// ✅ 正确:使用迭代器或增强for循环
for (String item : list) {
    // O(n)时间复杂度
}

性能影响:

  • 1000个元素:普通for循环约5ms,迭代器约0.5ms(10倍差异)
  • 10000个元素:普通for循环约500ms,迭代器约5ms(100倍差异)
  • 100000个元素:普通for循环约50000ms,迭代器约50ms(1000倍差异)
陷阱2:频繁随机访问
// ❌ 错误:频繁使用get(index)
List<String> list = new LinkedList<>();
for (int i = 0; i < 1000; i++) {
    String item = list.get(i);  // 每次都是O(n)
}

// ✅ 正确:使用迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String item = it.next();  // O(1)
}
陷阱3:在迭代时修改集合
// ❌ 错误:在迭代时修改集合
List<String> list = new LinkedList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
    if ("B".equals(item)) {
        list.remove(item);  // 抛出ConcurrentModificationException
    }
}

// ✅ 正确:使用迭代器的remove方法
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String item = it.next();
    if ("B".equals(item)) {
        it.remove();  // 安全删除
    }
}
陷阱4:忽略内存占用
// ❌ 错误:大数据量时使用LinkedList
List<BigObject> bigList = new LinkedList<>();
for (int i = 0; i < 1000000; i++) {
    bigList.add(new BigObject());  // 内存占用是ArrayList的2-5倍
}

// ✅ 正确:根据场景选择
// 如果内存紧张,即使需要频繁插入删除,也要考虑ArrayList
List<BigObject> list = new ArrayList<>(1000000);

2.5.2 最佳实践

实践1:绝对避免随机访问
// ❌ 绝对错误:O(n²)
for (int i = 0; i < list.size(); i++) {
    list.get(i);
}

// ✅ 正确:O(n)
for (String item : list) {
    // 处理item
}
实践2:利用ListIterator进行高效操作
// 场景:在遍历过程中插入元素
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
    String item = it.next();
    if (needInsertBefore(item)) {
        it.add("new item");  // O(1),在当前位置插入
    }
}

// 场景:双向遍历
ListIterator<String> it = list.listIterator(list.size());
while (it.hasPrevious()) {
    String item = it.previous();  // 从后往前遍历
}
实践3:选择合适的头部/尾部操作
// 正确使用头部操作
public void useAsStack() {
    // 栈:后进先出
    list.push("item1");  // addFirst
    list.push("item2");
    String top = list.pop();  // removeFirst
}

// 正确使用尾部操作
public void useAsQueue() {
    // 队列:先进先出
    list.offer("item1");  // addLast
    list.offer("item2");
    String first = list.poll();  // removeFirst
}

// 双端队列
public void useAsDeque() {
    // 可以在两端操作
    list.addFirst("urgent");
    list.addLast("normal");
    
    String first = list.removeFirst();
    String last = list.removeLast();
}
实践4:根据场景选择合适的数据结构
// 场景1:需要频繁随机访问
List<Product> catalog = new ArrayList<>();  // 正确选择
Product p = catalog.get(1234);  // O(1)

// 场景2:需要频繁在头尾操作
Deque<Message> messageQueue = new LinkedList<>();  // 正确选择
messageQueue.addFirst(newMessage);  // O(1)
Message m = messageQueue.pollLast();  // O(1)

// 场景3:需要实现栈
Deque<StackFrame> stack = new LinkedList<>();  // 正确选择
stack.push(newFrame);  // O(1)
StackFrame f = stack.pop();  // O(1)
实践5:使用Java 8的Stream API
// 过滤和转换
List<String> result = linkedList.stream()
    .filter(s -> s != null && s.length() > 3)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

// 并行处理(大数据量)
linkedList.parallelStream()
    .forEach(item -> process(item));

📊 总结

核心要点

  1. 数据结构: 双向链表,内存分散分配
  2. 核心优势: 头尾操作O(1),支持多角色使用
  3. 核心劣势: 随机访问O(n),内存占用大
  4. 遍历方式: 必须使用迭代器,不能用普通for循环
  5. 适用场景: 频繁头尾操作,需要队列/栈功能

性能要点

  • 随机访问: O(n),性能差,应避免
  • 头尾操作: O(1),性能优异
  • 遍历: O(n),但必须用迭代器
  • 内存: 是ArrayList的2-5倍

第3章:List集合对比与选择

3.1 ArrayList vs LinkedList全面深度对比

3.1.1 底层数据结构本质差异

数据结构对比
维度ArrayListLinkedList
数据结构动态数组双向链表
内存布局连续分配分散分配
节点开销无额外开销每个元素约28字节额外开销
缓存友好性高(顺序访问)低(随机访问)
内存碎片
GC压力
内存结构详细对比

ArrayList内存布局:

内存地址:0x1000 - [引用1][引用2][引用3]...(连续数组)
          0x2000 - 实际对象1
          0x3000 - 实际对象2
          0x4000 - 实际对象3
          ...

LinkedList内存布局:

内存地址:0x1000 - Node1 (item, prev, next)
          0x2000 - 实际对象1
          0x3000 - Node2 (item, prev, next)
          0x4000 - 实际对象2
          0x5000 - Node3 (item, prev, next)
          ...(节点分散在堆内存中)

关键差异:

  • ArrayList:内存连续,CPU缓存预取有效
  • LinkedList:内存分散,每次访问都可能缓存缺失

3.1.2 性能对比详细分析

时间复杂度完整对比表
操作ArrayListLinkedList差异分析实际性能差异
add(E e)O(1)摊销O(1)ArrayList可能触发扩容ArrayList稍快(无对象创建)
add(int index, E e)O(n)O(n)ArrayList需要移动元素,LinkedList需要遍历两者接近,但常数因子不同
addFirst(E e)O(n)O(1)ArrayList需要移动所有元素LinkedList快100-1000倍
addLast(E e)O(1)摊销O(1)两者都是O(1)ArrayList稍快(无对象创建)
get(int index)O(1)O(n)ArrayList直接索引访问ArrayList快100-1000倍
set(int index, E e)O(1)O(n)ArrayList直接数组赋值ArrayList快100-1000倍
remove(int index)O(n)O(n)ArrayList需要移动元素,LinkedList需要遍历两者接近,但ArrayList缓存友好
removeFirst()O(n)O(1)ArrayList需要移动所有元素LinkedList快100-1000倍
removeLast()O(1)O(1)两者都是O(1)ArrayList稍快
contains(Object)O(n)O(n)都需要遍历ArrayList稍快(缓存友好)
indexOf(Object)O(n)O(n)都需要遍历ArrayList稍快(缓存友好)
遍历O(n)O(n)都是O(n)ArrayList快30-50%(缓存友好)
实际性能测试数据

测试环境: JDK 11, Intel i7, 16GB RAM

测试1:尾部添加100万个元素

// ArrayList(预分配容量)
List<Integer> arrayList = new ArrayList<>(1000000);
long start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
    arrayList.add(i);
}
long time1 = System.nanoTime() - start;
// 结果:约50ms

// LinkedList
List<Integer> linkedList = new LinkedList<>();
start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
    linkedList.add(i);
}
long time2 = System.nanoTime() - start;
// 结果:约80ms

// 结论:ArrayList稍快(无对象创建开销)

测试2:头部添加10000个元素

// ArrayList
List<Integer> arrayList = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 10000; i++) {
    arrayList.add(0, i);  // 头部插入
}
long time1 = System.nanoTime() - start;
// 结果:约500ms

// LinkedList
List<Integer> linkedList = new LinkedList<>();
start = System.nanoTime();
for (int i = 0; i < 10000; i++) {
    linkedList.addFirst(i);  // 头部插入
}
long time2 = System.nanoTime() - start;
// 结果:约5ms

// 结论:LinkedList快100倍

测试3:随机访问1000次

// ArrayList
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
    arrayList.add(i);
}
Random random = new Random();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
    int index = random.nextInt(100000);
    int value = arrayList.get(index);
}
long time1 = System.nanoTime() - start;
// 结果:约0.1ms

// LinkedList
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < 100000; i++) {
    linkedList.add(i);
}
start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
    int index = random.nextInt(100000);
    int value = linkedList.get(index);
}
long time2 = System.nanoTime() - start;
// 结果:约100ms

// 结论:ArrayList快1000倍

测试4:遍历100万个元素

// ArrayList
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
    arrayList.add(i);
}
long start = System.nanoTime();
for (Integer num : arrayList) {
    // 空循环
}
long time1 = System.nanoTime() - start;
// 结果:约10ms

// LinkedList
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < 1000000; i++) {
    linkedList.add(i);
}
start = System.nanoTime();
for (Integer num : linkedList) {
    // 空循环
}
long time2 = System.nanoTime() - start;
// 结果:约15ms

// 结论:ArrayList快50%(缓存友好)

3.1.3 内存占用详细对比

内存占用计算

存储1000个Integer对象的内存估算:

ArrayList:

ArrayList对象:36字节
数组对象:16 + 1000 × 8 = 8016字节
Integer对象:1000 × 16 = 16000字节(假设每个Integer对象16字节)
总计:约24052字节(约23.5KB)

如果容量是1500(1.5倍扩容):
数组对象:16 + 1500 × 8 = 12016字节
空间浪费:约33%(1500个槽位,只用了1000个)

LinkedList:

LinkedList对象:36字节
Node对象:1000 × 28 = 28000字节
Integer对象:1000 × 16 = 16000字节
总计:约44036字节(约43KB)

内存差异: LinkedList是ArrayList的1.83倍

影响因素分析

1. 元素数量:

  • 元素越多,差距越大
  • 1000个元素:差异1.83倍
  • 10000个元素:差异约2倍
  • 100000个元素:差异约2.5倍

2. 元素大小:

  • 存储的对象越大,相对差距越小
  • 存储Integer:差异约2倍
  • 存储大对象(1KB):差异约1.1倍

3. JVM配置:

  • 64位JVM开启指针压缩:可以减小差距
  • 指针压缩:引用从8字节变为4字节
  • Node对象:从28字节变为24字节

4. 内存碎片:

  • LinkedList节点分散,可能产生更多内存碎片
  • ArrayList内存连续,碎片较少

3.1.4 CPU缓存影响深度分析

缓存层次结构
L1缓存:32KB,访问延迟约1ns
L2缓存:256KB,访问延迟约3ns
L3缓存:8MB,访问延迟约12ns
主内存:访问延迟约100ns
ArrayList的缓存优势
// ArrayList:顺序访问,缓存命中率高
for (int i = 0; i < list.size(); i++) {
    Object obj = list.get(i);
    // CPU缓存预取机制有效
    // 连续内存访问,缓存命中率高
    // 缓存命中率:通常>90%
}

缓存访问模式:

  • 元素0在内存地址0x1000
  • 元素1在内存地址0x1008(连续)
  • 元素2在内存地址0x1010(连续)
  • CPU可以预取后续数据到缓存

性能影响:

  • 缓存命中率:>90%
  • 平均访问延迟:约5ns(大部分缓存命中)
  • 性能优异
LinkedList的缓存劣势
// LinkedList:随机内存访问,缓存不友好
for (Object obj : linkedList) {
    // 每个节点访问都可能缓存缺失
    // 节点分散在堆内存中
    // 缓存命中率:通常<50%
}

缓存访问模式:

  • 节点1在内存地址0x1000
  • 节点2在内存地址0x5000(不连续)
  • 节点3在内存地址0x2000(不连续)
  • 每次访问都可能缓存缺失

性能影响:

  • 缓存命中率:<50%
  • 平均访问延迟:约50ns(考虑缓存缺失)
  • 性能较差

性能差异: ArrayList遍历比LinkedList快30-50%

3.1.5 GC压力对比

GC影响分析

ArrayList:

  • 对象数量少:只有1个数组对象
  • 内存连续:GC效率高
  • GC压力:小

LinkedList:

  • 对象数量多:n个Node对象
  • 内存分散:可能产生碎片
  • GC压力:大

实际测试:

// 创建100万个元素的集合
// ArrayList:产生1个数组对象
// LinkedList:产生100万个Node对象

// GC时间对比(Full GC):
// ArrayList:约50ms
// LinkedList:约200ms(4倍差异)

3.2 使用场景选择详细指南

3.2.1 选择ArrayList的场景详解

场景1:频繁随机访问

特征: 需要频繁使用get(index)set(index, element)

选择理由:

  • ArrayList的get/set是O(1),性能优异
  • LinkedList的get/set是O(n),性能差
  • 性能差异:100-1000倍
场景2:遍历操作为主

特征: 主要进行顺序遍历,很少随机访问

选择理由:

  • ArrayList顺序访问,缓存命中率高
  • LinkedList随机内存访问,缓存命中率低
  • 性能差异:30-50%
场景3:内存敏感的应用

特征: 内存有限,需要节省内存

选择理由:

  • ArrayList内存占用小
  • LinkedList内存占用大(2-5倍)
  • 内存差异:对于1000个元素,差异约20KB
场景4:数据基本不变或只在尾部添加

特征: 数据创建后很少修改,或只在尾部追加

选择理由:

  • 尾部添加,两者都是O(1)
  • ArrayList更省内存
  • 如果需要随机访问,ArrayList优势明显
场景5:需要subList操作

特征: 需要频繁使用subList方法

选择理由:

  • ArrayList的subList是O(1)视图
  • LinkedList的subList需要遍历,性能差

3.2.2 选择LinkedList的场景详解

场景1:频繁在头部插入删除

特征: 需要频繁使用addFirstremoveFirst等操作

选择理由:

  • LinkedList头部操作是O(1)
  • ArrayList头部操作是O(n)
  • 性能差异:100-1000倍
场景2:需要实现队列或双端队列

特征: 需要FIFO或LIFO功能

选择理由:

  • LinkedList实现了Deque接口
  • 头尾操作都是O(1)
  • 功能丰富,使用方便
场景3:频繁在中间位置插入(如果有节点引用)

特征: 使用ListIterator在遍历过程中插入

选择理由:

  • ListIterator的add/remove是O(1)
  • 在遍历过程中插入,性能优异
  • ArrayList的ListIterator插入需要移动元素
场景4:不确定元素数量且需要频繁插入删除

特征: 元素数量不确定,需要频繁在任意位置插入删除

选择理由:

  • 不需要预分配容量
  • 插入删除只需修改引用,不需要移动元素
  • 适合动态数据
场景5:需要栈功能

特征: 需要LIFO(后进先出)功能

选择理由:

  • LinkedList实现了Deque接口
  • push/pop都是O(1)
  • 比Stack类性能更好

3.3 选择决策树

选择ArrayList的关键因素:

  1. ✅ 频繁随机访问
  2. ✅ 频繁遍历
  3. ✅ 内存紧张
  4. ✅ 数据规模大
  5. ✅ 需要subList操作
  6. ✅ 关注GC和缓存性能

选择LinkedList的关键因素:

  1. ✅ 频繁在头尾插入删除
  2. ✅ 需要队列/栈功能
  3. ✅ 频繁在中间插入(有迭代器引用)
  4. ✅ 内存充足
  5. ✅ 数据规模小到中等

默认选择: ArrayList(大多数场景)


3.4 性能优化最佳实践

3.4.1 ArrayList优化技巧详解

技巧1:预分配初始容量(最重要)
// ❌ 错误:频繁扩容,性能差
List<Integer> badList = new ArrayList<>();  // 默认容量10
for (int i = 0; i < 1000000; i++) {
    badList.add(i);  // 多次扩容:10→15→22→33→...
    // 扩容次数:约20次
    // 总复制元素数:约2000万个
    // 耗时:约200ms
}

// ✅ 正确:预分配,性能好
List<Integer> goodList = new ArrayList<>(1000000);
for (int i = 0; i < 1000000; i++) {
    goodList.add(i);  // 无扩容开销
    // 扩容次数:0次
    // 总复制元素数:0个
    // 耗时:约50ms
}

// 性能提升:约4倍

优化建议:

// 方法1:根据预估大小预分配
int expectedSize = 10000;
List<String> list = new ArrayList<>(expectedSize);

// 方法2:考虑负载因子,预留33%空间
int optimalCapacity = (int)(expectedSize / 0.75f) + 1;
List<String> list = new ArrayList<>(optimalCapacity);

// 方法3:使用ensureCapacity预扩容
List<String> list = new ArrayList<>();
list.ensureCapacity(1000000);
技巧2:批量操作使用addAll
// ❌ 错误:逐个添加
List<String> target = new ArrayList<>();
for (String item : source) {
    target.add(item);  // 可能多次触发扩容
    // 如果source有1000个元素,可能扩容多次
}

// ✅ 正确:批量添加
List<String> target = new ArrayList<>(source.size());
target.addAll(source);  // 一次扩容,一次复制
// 性能提升:约2-5倍

优化建议:

// 场景1:合并多个集合
List<String> result = new ArrayList<>(list1.size() + list2.size());
result.addAll(list1);
result.addAll(list2);

// 场景2:从数组创建
String[] array = {"A", "B", "C"};
List<String> list = new ArrayList<>(Arrays.asList(array));

// 场景3:从Stream创建
List<String> list = stream.collect(Collectors.toCollection(
    () -> new ArrayList<>(estimatedSize)));
技巧3:适时使用trimToSize
// 场景:添加大量数据后删除大部分
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
    list.add("data" + i);
}
// 此时容量可能是1500000

// 删除大部分数据
list.subList(1000, list.size()).clear();
// 此时size=1000,但容量仍是1500000

// 释放多余空间
list.trimToSize();  // 容量调整为1000,释放约12MB内存

使用场景:

  • 列表大小固定不再变化
  • 内存紧张,需要释放空间
  • 批量删除后,容量远大于size

3.4.2 LinkedList优化技巧详解

技巧1:绝对避免随机访问
// ❌ 绝对错误:O(n²)时间复杂度
List<String> list = new LinkedList<>();
for (int i = 0; i < list.size(); i++) {
    String item = list.get(i);  // 每次get都是O(n)
    // 对于10000个元素,需要约5000万次操作
}

// ✅ 正确:O(n)时间复杂度
for (String item : list) {
    // 使用迭代器,每次next()是O(1)
    // 对于10000个元素,只需要10000次操作
}

// 性能提升:约5000倍!
技巧2:利用ListIterator进行高效操作
// 场景1:在遍历过程中插入元素
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
    String item = it.next();
    if (needInsertBefore(item)) {
        it.add("new item");  // O(1),在当前位置插入
    }
}

// 场景2:双向遍历
ListIterator<String> it = list.listIterator(list.size());
while (it.hasPrevious()) {
    String item = it.previous();  // 从后往前遍历
}

// 场景3:在指定位置操作
ListIterator<String> it = list.listIterator(index);
it.add("new item");  // 在指定位置插入,O(1)
技巧3:选择合适的头部/尾部操作
// 正确使用头部操作
public void useAsStack() {
    // 栈:后进先出
    list.push("item1");  // addFirst
    list.push("item2");
    String top = list.pop();  // removeFirst
}

// 正确使用尾部操作
public void useAsQueue() {
    // 队列:先进先出
    list.offer("item1");  // addLast
    list.offer("item2");
    String first = list.poll();  // removeFirst
}

// 双端队列
public void useAsDeque() {
    // 可以在两端操作
    list.addFirst("urgent");
    list.addLast("normal");
    
    String first = list.removeFirst();
    String last = list.removeLast();
}

3.4.3 通用优化技巧

技巧1:避免不必要的装箱拆箱
// ❌ 错误:使用包装类型
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
    list.add(i);  // 自动装箱,性能开销
}

// ✅ 正确:使用原始数组(如果可能)
int[] array = new int[1000000];
for (int i = 0; i < 1000000; i++) {
    array[i] = i;  // 无装箱开销
}

// 或者使用第三方库(如Trove、FastUtil)
TIntArrayList list = new TIntArrayList();
技巧2:使用Stream API进行复杂操作
// 传统方式
List<String> result = new ArrayList<>();
for (String s : list) {
    if (s != null && s.length() > 3) {
        result.add(s.toUpperCase());
    }
}

// Stream API方式(更简洁,可能更高效)
List<String> result = list.stream()
    .filter(s -> s != null && s.length() > 3)
    .map(String::toUpperCase)
    .collect(Collectors.toList());
技巧3:并行处理(大数据量)
// 串行处理
list.stream()
    .forEach(item -> process(item));

// 并行处理(CPU密集型,大数据量)
list.parallelStream()
    .forEach(item -> process(item));

// 注意:并行有开销,小数据量可能更慢
// 通常数据量>10000才考虑并行

第4章:线程安全List实现

4.1 Vector的线程安全实现(已过时,了解即可)

Vector的基本特性

Vector是JDK 1.0就存在的集合类,已不推荐在新代码中使用

核心特点:

  • 基于动态数组实现,类似ArrayList
  • 所有公共方法都使用synchronized关键字实现线程安全
  • 由于性能问题和设计过时,已被更好的替代方案取代
同步机制详解
// Vector的所有公共方法都使用synchronized
public synchronized boolean add(E e) { ... }
public synchronized E get(int index) { ... }
public synchronized int size() { ... }
public synchronized boolean isEmpty() { ... }

锁竞争问题:

// 每次操作都需要获取锁,读操作也要锁
Vector<Integer> vector = new Vector<>();
vector.add(1);      // 获取锁
int value = vector.get(0);  // 获取锁(读操作也要锁!)
// 问题:读操作之间无法并发,性能差

性能问题分析:

  • 所有方法都加锁,锁粒度太粗
  • 读操作也要加锁,无法并发读取
  • 锁竞争严重,高并发场景性能急剧下降
  • 复合操作仍需外部同步

4.2 CopyOnWriteArrayList原理深度解析

核心设计思想

Copy-On-Write(写时复制)模式:

  • 读取操作: 无锁,直接访问当前数组,性能极高
  • 写入操作: 加锁,复制新数组,修改新数组,替换引用

适用场景: 读多写少,读频率远大于写频率(通常≥10:1)

核心数据结构
public class CopyOnWriteArrayList<E> {
    // 核心:volatile保证可见性
    private transient volatile Object[] array;
    
    final Object[] getArray() {
        return array;  // volatile读,保证看到最新值
    }
    
    final void setArray(Object[] a) {
        array = a;  // volatile写,保证对其他线程可见
    }
}
写操作实现原理详解
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();  // 1. 加锁(写操作互斥)
    try {
        Object[] elements = getArray();     // 获取当前数组
        int len = elements.length;
        // 2. 创建新数组(长度+1)
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 3. 在新数组上修改
        newElements[len] = e;
        // 4. 替换引用(volatile写,保证可见性)
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();  // 5. 解锁
    }
}

写操作特点:

  • ✅ 加锁:所有写操作都使用ReentrantLock加锁,保证互斥
  • ✅ 复制数组:每次写操作都复制整个数组(写时复制)
  • ✅ 原子性:写操作是原子的,读者要么看到旧数组,要么看到新数组
  • ⚠️ 开销大:复制整个数组,时间复杂度O(n),空间复杂度O(n)
读操作实现原理详解
// 所有读操作都不需要加锁
public E get(int index) {
    return get(getArray(), index);  // 直接访问数组
}

public int size() {
    return getArray().length;  // 直接返回数组长度
}

public boolean contains(Object o) {
    Object[] elements = getArray();
    return indexOf(o, elements, 0, elements.length) >= 0;
}

读操作特点:

  • ✅ 完全无锁:不需要任何同步机制,性能极高
  • ✅ 直接内存访问:通过数组索引直接访问,O(1)时间复杂度
  • ✅ volatile保证可见性:通过volatile读获取最新的array引用
  • ⚠️ 弱一致性:读操作可能看到过期的数据(但不会看到不一致的数据)
迭代器特性详解
public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

static final class COWIterator<E> implements ListIterator<E> {
    // 迭代器创建时复制当前数组的快照
    private final Object[] snapshot;
    private int cursor;
    
    private COWIterator(Object[] elements, int initialCursor) {
        snapshot = elements;  // 保存快照,不会反映后续修改
        cursor = initialCursor;
    }
    
    public boolean hasNext() {
        return cursor < snapshot.length;
    }
    
    public E next() {
        if (!hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
    
    // 不支持修改操作
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

迭代器特点:

  • ✅ 基于快照:迭代器创建时复制数组快照
  • ✅ 弱一致性:不反映创建后的修改
  • ✅ 不会抛出ConcurrentModificationException
  • ❌ 不支持修改操作:remove、set、add都抛出UnsupportedOperationException
  • ⚠️ 内存开销:每个迭代器持有快照引用,可能阻止旧数组被GC
适用场景深度分析

✅ 适合使用CopyOnWriteArrayList的场景:

  1. 读多写少的配置信息
private CopyOnWriteArrayList<ConfigListener> listeners = 
    new CopyOnWriteArrayList<>();

public void notifyConfigChange(ConfigEvent event) {
    // 无锁遍历,高性能
    for (ConfigListener listener : listeners) {
        listener.onConfigChanged(event);
    }
}

public void addListener(ConfigListener listener) {
    listeners.add(listener);  // 写操作少,开销可接受
}
  1. 事件监听器列表
private CopyOnWriteArrayList<EventListener> listeners = 
    new CopyOnWriteArrayList<>();

public void fireEvent(Event event) {
    // 事件触发时,监听器列表通常不变
    for (EventListener listener : listeners) {
        listener.onEvent(event);  // 无锁读取,性能优异
    }
}

❌ 不适合使用的场景:

  • ❌ 写多读少:频繁写入会导致大量数组复制,性能急剧下降
  • ❌ 大数据量:存储大量数据,复制开销不可接受
  • ❌ 需要强一致性:需要立即看到最新数据的场景
  • ❌ 内存敏感:内存有限,不能接受数据重复

4.3 Collections.synchronizedList详解

实现原理
public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

static class SynchronizedList<E> implements List<E> {
    final List<E> list;
    final Object mutex;  // 同步对象
    
    public boolean add(E e) {
        synchronized (mutex) { return list.add(e); }
    }
    
    public E get(int index) {
        synchronized (mutex) { return list.get(index); }
    }
}

特点:

  • 所有方法都使用synchronized同步
  • 使用mutex对象作为锁
  • 复合操作仍需外部同步

使用示例:

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

// 单个操作是线程安全的
syncList.add("item");

// 复合操作需要外部同步
synchronized (syncList) {
    for (String item : syncList) {
        // 安全遍历
    }
}

4.4 线程安全List性能对比

性能对比总结
实现类读性能写性能适用场景内存占用一致性
CopyOnWriteArrayList⭐⭐⭐⭐⭐⭐⭐读多写少较大弱一致性
Vector⭐⭐⭐⭐⭐已过时,不推荐正常强一致性
Collections.synchronizedList⭐⭐⭐⭐⭐低并发场景正常强一致性
ReadWriteLockList⭐⭐⭐⭐⭐⭐⭐读写均衡正常强一致性

关键结论:

  • 读性能: CopyOnWriteArrayList >> ReadWriteLockList > synchronizedList ≈ Vector
  • 写性能: synchronizedList ≈ Vector > ReadWriteLockList > CopyOnWriteArrayList
  • 内存占用: CopyOnWriteArrayList > 其他
  • 一致性: Vector ≈ synchronizedList > ReadWriteLockList > CopyOnWriteArrayList
选择建议

读多写少(读/写比例≥10:1):

  • 推荐:CopyOnWriteArrayList
  • 原因:读操作无锁,性能极高

读写均衡:

  • 推荐:Collections.synchronizedList或ReadWriteLockList
  • 原因:性能平衡,强一致性

低并发场景:

  • 推荐:Collections.synchronizedList
  • 原因:简单易用,性能足够

高并发写入:

  • 推荐:分段锁实现
  • 原因:减少锁竞争,提升并发性能

第5章:高频面试题精选

面试题1:ArrayList和LinkedList的区别是什么?如何选择?

核心区别:

维度ArrayListLinkedList
数据结构动态数组双向链表
随机访问O(1)O(n)
头部插入删除O(n)O(1)
尾部插入删除O(1)摊销O(1)
内存占用较小较大(2-5倍)
缓存友好性

选择策略:

选择ArrayList:

  • 频繁随机访问(get/set)
  • 频繁遍历操作
  • 内存敏感的应用
  • 数据主要在尾部添加

选择LinkedList:

  • 频繁在头部插入删除
  • 需要实现队列或双端队列
  • 频繁在中间位置插入(有迭代器引用)
  • 内存充足,对随机访问要求不高

决策树:

需要频繁随机访问吗?
    ├── 是 → ArrayList
    └── 否 → 需要频繁在头尾操作吗?
            ├── 是 → LinkedList
            └── 否 → 默认选择ArrayList

面试题2:ArrayList的底层实现原理是什么?请详细说明。

ArrayList基于动态数组实现,内部使用Object[] elementData存储元素。

  1. 数据结构: 动态数组,内存连续分配
  2. 核心字段:
transient Object[] elementData;     
private int size;                   
protected transient int modCount = 0;    
  1. 默认容量: 10(但无参构造器延迟初始化,第一次add时才分配)

  2. 扩容机制: 1.5倍增长(newCapacity = oldCapacity + (oldCapacity >> 1)

  3. 扩容触发: 当size + 1 > elementData.length时触发

  4. 扩容过程: 创建新数组 → 复制元素 → 替换引用

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);      
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    elementData = Arrays.copyOf(elementData, newCapacity);
}

深入理解:

  • 为什么用transient修饰elementData?自定义序列化,只序列化实际元素

面试题3:ArrayList和LinkedList在什么场景下性能差异最大?

性能差异最大的场景:

  1. 频繁随机访问(ArrayList绝对优势)

    // ArrayList:O(1)
    for (int i = 0; i < 10000; i++) {
        list.get(i);  // 直接数组索引,极快
    }
    
    // LinkedList:O(n²)
    for (int i = 0; i < 10000; i++) {
        list.get(i);  // 每次都要遍历,极慢
    }
    // 性能差异:约1000倍
    
  2. 频繁头部插入删除(LinkedList绝对优势)

    // ArrayList:O(n)
    for (int i = 0; i < 10000; i++) {
        list.add(0, i);  // 需要移动所有元素,极慢
    }
    
    // LinkedList:O(1)
    for (int i = 0; i < 10000; i++) {
        list.addFirst(i);  // 直接修改头节点,极快
    }
    // 性能差异:约100倍
    
  3. 遍历操作(ArrayList稍快)

    // ArrayList:缓存友好,稍快
    for (String item : list) { }
    
    // LinkedList:缓存不友好,稍慢
    for (String item : list) { }
    // 性能差异:约30-50%
    

面试题4:ArrayList的扩容机制是怎样的?为什么选择1.5倍?

  1. 触发条件: size + 1 > elementData.length
  2. 计算新容量: newCapacity = oldCapacity + (oldCapacity >> 1)(1.5倍)
  3. 特殊情况处理: 如果1.5倍仍不足,使用minCapacity
  4. 创建新数组并复制元素: Arrays.copyOf(elementData, newCapacity)
  5. 替换引用: elementData = newElements

为什么选择1.5倍?

1.5倍在扩容频率和空间利用率之间取得最佳平衡。 深入理解:

  • 扩容的时间复杂度:单次O(n),但分摊到每次add是O(1)

面试题5:如何优化ArrayList和LinkedList的性能?

标准答案:

ArrayList优化:

  1. 预分配初始容量(最重要)

    List<Integer> list = new ArrayList<>(expectedSize);
    
  2. 批量操作使用addAll

    list.addAll(anotherList);  // 一次扩容
    
  3. 适时使用trimToSize

    list.trimToSize();  // 释放多余空间
    

LinkedList优化:

  1. 绝对避免随机访问

    // 错误:O(n²)
    for (int i = 0; i < list.size(); i++) {
        list.get(i);
    }
    
    // 正确:O(n)
    for (String item : list) { }
    
  2. 利用ListIterator

    ListIterator<String> it = list.listIterator();
    it.add("new item");  // O(1)插入
    

面试题6:ArrayList的fail-fast机制是什么?如何实现的?

**fail-fast(快速失败)**是Java集合框架的重要特性,当检测到并发修改时立即抛出ConcurrentModificationException

  1. modCount字段: 记录集合被结构性修改的次数
  2. expectedModCount: 迭代器创建时记录当前的modCount
  3. 检查机制: 每次迭代操作前检查modCount != expectedModCount

源码实现:

private class Itr implements Iterator<E> {
    int expectedModCount = modCount;
    public E next() {
        checkForComodification();
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

正确做法:

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String item = it.next();
    if ("B".equals(item)) {
        it.remove();      }
}

局限性: 非原子性检查,不保证100%检测并发修改。

深入理解:

  • fail-fast vs fail-safe的区别

扩展问题:

  • 为什么迭代器的remove方法不会抛出异常?

面试题7:ArrayList和数组有什么区别?

代码对比:

// 数组:固定大小
String[] array = new String[10];
array[0] = "item";

// ArrayList:动态扩容
List<String> list = new ArrayList<>();
list.add("item");
list.add("another");

深入理解:

  • ArrayList本质上就是数组的封装

面试题8:LinkedList为什么使用双向链表而不是单向链表?

原因:

  1. 支持双向遍历: 可以从头到尾,也可以从尾到头
  2. 高效删除: 删除节点时,可以直接通过prev找到前驱节点,O(1)时间
  3. ListIterator支持: 支持前向和后向遍历,实现ListIterator接口
  4. Deque接口实现: 双端队列需要双向链表支持

对比单向链表:

  • 单向链表删除需要O(n)找到前驱
  • 双向链表删除只需要O(1)

面试题9:如何优化ArrayList的性能?

  1. 预分配初始容量(最重要)
List<Integer> list = new ArrayList<>(1000000);
for (int i = 0; i < 1000000; i++) {
    list.add(i);
}
  1. 批量操作使用addAll
target.addAll(source);
  1. 适时使用trimToSize()
list.trimToSize();
  1. 选择合适的遍历方式
for (String item : list) { }  // 推荐

面试题10:LinkedList的底层实现是什么?为什么使用双向链表?

LinkedList基于双向链表实现,每个节点包含:

  • E item:存储的元素
  • Node<E> next:指向下一个节点
  • Node<E> prev:指向上一个节点
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;
    }
}

为什么使用双向链表?

  1. 支持双向遍历: 可以从头到尾,也可以从尾到头
  2. 高效删除: 删除节点时,可以直接通过prev找到前驱节点,O(1)时间
  3. ListIterator支持: 支持前向和后向遍历,实现ListIterator接口
  4. Deque接口实现: 双端队列需要双向链表支持

对比单向链表:

// 双向链表删除:O(1)
void unlink(Node<E> x) {
    x.prev.next = x.next;
    x.next.prev = x.prev;
}

// 单向链表删除:需要O(n)找到前驱
void remove(Node<E> x) {
    Node<E> prev = first;
    while (prev.next != x) {
        prev = prev.next;
    }
    prev.next = x.next;
}

深入理解:

  • 双向链表的节点开销:每个节点约28字节(64位JVM压缩指针)

面试题11:LinkedList实现了哪些接口?为什么可以当作队列和栈使用?

LinkedList实现了以下接口:

  • List:列表接口,提供有序可重复的集合
  • Deque:双端队列接口(继承自Queue)
  • Cloneable:支持克隆
  • Serializable:支持序列化

为什么可以当作队列和栈使用?

因为LinkedList实现了Deque接口,而Deque同时定义了队列和栈的操作:

使用示例:

// 作为队列
Queue<String> queue = new LinkedList<>();
queue.offer("A");
queue.poll();

// 作为栈
Deque<String> stack = new LinkedList<>();
stack.push("A");
stack.pop();

// 作为双端队列
Deque<String> deque = new LinkedList<>();
deque.addFirst("A");
deque.addLast("B");

⚠️ 重要提示: Java官方推荐使用Deque的实现类(如LinkedList、ArrayDeque)代替旧的Stack类,因为Stack基于Vector实现,性能差且设计不佳。

深入理解:

  • Stack类的问题:基于Vector,所有方法都同步,性能差

面试题12:LinkedList的get方法为什么性能差?如何优化?

LinkedList的get方法需要遍历链表找到指定索引的节点:

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

时间复杂度:O(n),最坏情况需要遍历一半的节点

  1. 使用迭代器遍历(最重要)
for (int i = 0; i < list.size(); i++) {
       list.get(i);     }
   
      for (String item : list) {
```          }
  1. 使用ListIterator在指定位置操作
ListIterator<String> it = list.listIterator(index);
it.add("new item");
it.add("new item");     

深入理解:

  • 为什么node方法要优化?将最坏情况O(n)优化为平均O(n/2)

面试题13:如何实现一个线程安全的List?

方案1:使用同步包装器

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

syncList.add("item");

synchronized (syncList) {
    for (String item : syncList) {
            }
}

方案2:使用CopyOnWriteArrayList

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

cowList.add("item");
String item = cowList.get(0); 

方案3:使用显式锁

public class ThreadSafeList<E> {
    private final List<E> list = new ArrayList<>();
    private final ReentrantLock lock = new ReentrantLock();
    
    public void add(E element) {
        lock.lock();
        try {
            list.add(element);
        } finally {
            lock.unlock();
        }
    }
}

方案4:使用读写锁

public class ReadWriteList<E> {
    private final List<E> list = new ArrayList<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public void add(E element) {
        lock.writeLock().lock();
        try {
            list.add(element);
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public E get(int index) {
    }
}
  • 读多写少:CopyOnWriteArrayList
  • 读写均衡:Collections.synchronizedList或读写锁
  • 需要精细控制:显式锁
  • 低并发:Collections.synchronizedList

面试题14:Vector为什么被淘汰?有哪些替代方案?

Vector被淘汰的原因:

  1. 性能问题: 所有方法都加synchronized,锁粒度太粗
  2. 设计过时: JDK 1.0的设计,API设计不够现代化
  3. 读操作也要锁: 无法并发读取,性能差
  4. 有更好的替代方案: Collections.synchronizedList、CopyOnWriteArrayList等

替代方案:

List<String> list1 = new ArrayList<>();

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

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

深入理解:

  • Vector的历史:JDK 1.0就存在,是Java最早的集合类

扩展问题:

  • Vector和ArrayList的区别是什么?

面试题15:CopyOnWriteArrayList的实现原理是什么?适用什么场景?

CopyOnWriteArrayList采用**写时复制(Copy-On-Write)**模式:

  1. 读操作: 无锁,直接访问当前数组,性能极高
  2. 写操作: 加锁,复制新数组,修改新数组,替换引用

核心代码:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        Object[] newElements = Arrays.copyOf(elements, elements.length + 1);
        newElements[elements.length] = e;
        setArray(newElements);          return true;
    } finally {
    }
}

适用场景:

适合:

  • 读多写少(读/写比例≥10:1)
  • 配置信息存储
  • 事件监听器列表
  • 白名单/黑名单

不适合:

  • 写多读少
  • 大数据量
  • 需要强一致性
  • 内存敏感

深入理解:

  • 迭代器基于快照,不会抛出ConcurrentModificationException

面试题16:请分析ArrayList的add方法实现,包括扩容过程。

add方法完整流程:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);      
    elementData[size++] = e;              
    return true;
}

详细分析:

  1. ensureCapacityInternal方法:
private void ensureCapacityInternal(int minCapacity) {
       if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                      minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
       }
       ensureExplicitCapacity(minCapacity);
}
  1. ensureExplicitCapacity方法:
private void ensureExplicitCapacity(int minCapacity) {
       modCount++;         
       if (minCapacity - elementData.length > 0)
        grow(minCapacity);     
}
  1. grow方法(核心扩容算法):
private void grow(int minCapacity) {
       int oldCapacity = elementData.length;
       
       int newCapacity = oldCapacity + (oldCapacity >> 1);
       
       if (newCapacity - minCapacity < 0)
       newCapacity = minCapacity;
}

扩容过程详解:

  1. 计算新容量(1.5倍)
  2. 检查特殊情况(不足或超大)
  3. 创建新数组
  4. 复制所有元素到新数组(O(n))
  5. 替换引用

时间复杂度分析:

  • 单次扩容:O(n)
  • 分摊到每次add:O(1)
  • 预分配容量:O(1)

深入理解:

  • 为什么选择1.5倍?平衡扩容频率和空间利用率

面试题17:请分析LinkedList的node方法实现,说明其优化原理。

node方法实现:

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

优化原理:

  1. 未优化版本: 总是从头开始遍历,最坏情况O(n)
  2. 优化版本: 根据索引位置选择遍历方向,平均O(n/2)
  • 索引在前半部分:从头开始遍历

  • 索引在后半部分:从尾开始遍历

  • 最多只需要遍历一半的节点

  • 最坏情况:O(n) → O(n/2)

  • 平均情况:O(n/2) → O(n/4)

  • 性能提升:约50%


面试题18:如何优化List的性能?有哪些最佳实践?

ArrayList优化:

  1. 预分配初始容量(最重要)
List<Integer> list = new ArrayList<>(expectedSize);
  1. 批量操作使用addAll
list.addAll(anotherList);        
  1. 适时使用trimToSize()
list.trimToSize();
  1. 选择合适的遍历方式
for (String item : list) { }  // 推荐

LinkedList优化:

  1. 绝对避免随机访问
for (int i = 0; i < list.size(); i++) {
       list.get(i);
   }
   
```      for (String item : list) { }
  1. 利用ListIterator
ListIterator<String> it = list.listIterator();
it.add("new item");     

通用优化:

  1. 根据场景选择合适的数据结构
  2. 避免不必要的装箱拆箱
  3. 使用Stream API进行批量操作
  4. 适时使用并行流(大数据量)

性能测试方法:

@Benchmark
public void testArrayList() {
    List<Integer> list = new ArrayList<>(1000000);
    for (int i = 0; i < 1000000; i++) {
        list.add(i);
    }
}

面试题19:ArrayList的扩容会影响性能吗?如何优化?

扩容对性能的影响:

  1. 时间成本: 单次扩容O(n),需要复制所有元素

  2. 空间成本: 新旧数组同时存在,内存占用翻倍

  3. GC压力: 旧数组成为垃圾,增加GC压力

  4. 预分配容量(最重要)

int expectedSize = 10000;
List<String> list = new ArrayList<>(expectedSize);
  1. 使用ensureCapacity预扩容
List<String> list = new ArrayList<>();
list.ensureCapacity(1000000);     
  1. 批量操作使用addAll
list.addAll(anotherList);     
  1. 适时使用trimToSize
list.trimToSize();    

面试题20:ArrayList的subList方法返回的是什么?有什么注意事项?

subList返回的是视图(View),不是独立的副本

注意事项:

  1. 视图特性: subList是原列表的视图,修改会影响原列表
  2. 失效问题: 原列表的结构性修改会使子列表失效
  3. 需要独立副本: 如果需要独立副本,使用new ArrayList<>(list.subList(from, to))

正确使用:

List<Integer> independentCopy = new ArrayList<>(list.subList(1, 4));
list.add(7);  
independentCopy.get(0);  

面试题21:ArrayList的contains方法如何实现?时间复杂度是多少?

1. 源码实现

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

public int indexOf(Object o) {
    if (o == 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;
    }
    return -1;
}

2. 实现原理

  • contains 调用 indexOf,判断返回值是否 >= 0
  • indexOf 顺序遍历数组查找匹配元素
  • null 处理:使用 == 比较
  • 非 null 处理:使用 equals() 比较
  • 返回第一个匹配元素的索引,未找到返回 -1

3. 时间复杂度

  • 最好情况:O(1) — 第一个元素即匹配
  • 平均情况:O(n) — 平均遍历 n/2 个元素
  • 最坏情况:O(n) — 遍历所有元素或未找到

4. 为什么是O(n)?

ArrayList 基于数组,不支持按值直接定位,只能顺序查找。

5. 性能优化建议

如果频繁调用 contains,可考虑:

方案1:使用HashSet(不保持顺序)

Set<String> set = new HashSet<>(list);

boolean contains = set.contains("item");  // O(1)

方案2:使用LinkedHashSet(保持顺序)

Set<String> linkedSet = new LinkedHashSet<>(list);
boolean contains = linkedSet.contains("item");  // O(1)

面试题22:LinkedList的addAll方法如何实现?性能如何?

1. 源码实现

LinkedList的addAll方法有两个重载版本:

在尾部添加

public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

在指定位置插入

public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);
    // 1. 将集合转换为数组
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;
    // 2. 找到插入位置的前驱和后继节点
    Node<E> pred, succ;
    if (index == size) {
        // 在尾部插入
        succ = null;
        pred = last;
    } else {
        // 在中间插入,需要找到index位置的节点
        succ = node(index);
        pred = succ.prev;
    }
    // 3. 批量创建节点并链接
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        pred = newNode;
    }
    // 4. 链接到原链表
    if (succ == null) {
        last = pred;
    } else {
        pred.next = succ;
        succ.prev = pred;
    }
    size += numNew;
    modCount++;
    return true;
}

2. 实现原理

  1. 集合转数组:使用c.toArray()将集合转为数组,便于遍历
  2. 定位插入点:根据index找到前驱节点pred和后继节点succ
  3. 批量创建节点:遍历数组,为每个元素创建新节点并链接
  4. 链接到链表:将新节点链连接到原链表

3. 性能分析

时间复杂度:

  • 最好情况:O(m) — 在尾部插入(index == size),无需查找位置
  • 平均情况:O(n/2 + m) — 需要找到插入位置(平均n/2),然后插入m个元素
  • 最坏情况:O(n + m) — 在头部插入,需要遍历整个链表找到位置 空间复杂度:O(m) — 需要创建m个新节点

4. 性能优化

相比逐个调用add():

  • 只需定位一次插入位置(O(n)),而不是m次
  • 批量创建节点,减少链表遍历次数
  • 一次性更新size和modCount

性能对比示例:

方式1:逐个add(低效)

for (String item : source) {
    list.add(item);  // 每次都要遍历找到尾部,O(n) × m
}

方式2:使用addAll(高效)

list.addAll(source);  // 只找一次位置,O(n) + O(m)

5. 使用建议

  • 批量添加时优先使用addAll,而不是循环调用add()
  • 在尾部添加时性能最好(O(m))
  • 在头部或中间插入时,性能取决于链表大小和插入位置 总结:addAll通过批量操作和单次定位,显著提升了批量添加的效率,是LinkedList批量操作的首选方法。

面试题23:ArrayList的clear方法如何实现?为什么要遍历置null?

public void clear() {
    modCount++;
    for (int i = 0; i < size; i++)
    elementData[i] = null;
    size = 0;
}

为什么要遍历置null?

原因:帮助垃圾回收,防止内存泄漏

内存泄漏示例:

List<BigObject> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    list.add(new BigObject());  
}
list.clear();  
  • 时间复杂度: O(n) - 需要遍历所有元素
  • 空间复杂度: O(1) - 不创建新对象

优化建议:

list = new ArrayList<>(); 

面试题24:LinkedList的size()方法时间复杂度是多少?为什么?

时间复杂度:O(1)

public int size() {
   return size;  
}

为什么是O(1)?

LinkedList内部维护了size字段,记录链表的元素数量:

transient int size = 0;  
void linkLast(E e) {
        size++; 
}

E unlink(Node<E> x) {
      size--;  
}

对比:如果不维护size字段

public int size() {
    int count = 0;
    Node<E> x = first;
    while (x != null) {
        count++;
        x = x.next;
    }
return count;  
}

优势:

  • O(1)时间复杂度,性能优异
  • 快速判断是否为空:size == 0
  • 快速获取大小,无需遍历

面试题25:ArrayList的toArray方法有几种重载?有什么区别?

两种重载方法

1. toArray() - 返回Object[]

public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}

特点:

  • 返回类型:Object[]
  • 总是创建新数组
  • 需要强制类型转换才能使用

2. toArray(T[] a) - 返回指定类型数组

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // 数组太小,创建新数组
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    // 数组足够大,直接复制到传入的数组
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;  // 标记结束位置
    return a;
}

特点:

  • 返回类型:T[](泛型数组)
  • 根据传入数组大小决定是否创建新数组
  • 不需要强制类型转换

详细对比

维度toArray()toArray(T[] a)
返回类型Object[]T[](泛型数组)
类型安全需要强制转换类型安全
数组创建总是创建新数组可能复用传入数组
性能一般可优化(传入精确大小)
使用场景简单场景需要类型安全或性能优化

使用示例

List<String> list = new ArrayList<>(Arrays.asList("A""B""C"));

// 方式1:使用toArray()

Object[] array1 = list.toArray();

// String[] strArray1 = (String[]) array1;  // 需要强制转换,但会抛出ClassCastException

// 方式2:使用toArray(T[] a) - 传入空数组

String[] array2 = list.toArray(new String[0]);  // 自动创建新数组

// 方式3:使用toArray(T[] a) - 传入精确大小的数组(推荐)

String[] array3 = list.toArray(new String[list.size()]);  // 复用数组,性能最好

// 方式4:使用Stream API

String[] array4 = list.stream().toArray(String[]::new);

实现原理

toArray(T[] a)的检查逻辑:

  1. 如果数组太小(a.length < size):
  • 创建新数组,避免数组越界
  • 使用Arrays.copyOf创建指定类型的新数组
  1. 如果数组足够大(a.length >= size):
  • 直接使用传入的数组,避免创建新数组
  • 使用System.arraycopy复制元素
  1. 如果数组太大(a.length > size):
  • 在size位置置null,标记结束位置
  • 帮助调用者识别实际元素数量

注意事项

  1. 类型转换:toArray()返回Object[],不能直接转换为具体类型数组
  1. 数组大小:toArray(T[] a)如果传入数组太小,会创建新数组
  1. 性能考虑:传入精确大小的数组可以避免数组复制,提升性能
  1. null标记:如果传入数组太大,会在size位置置null作为结束标记

最佳实践

// ✅ 推荐:传入精确大小的数组
String[] array = list.toArray(new String[list.size()]);

// ✅ 也可以:传入空数组(Java 6+优化后性能接近)
String[] array = list.toArray(new String[0]);

// ❌ 不推荐:使用toArray()需要强制转换
Object[] array = list.toArray();

总结:优先使用toArray(T[] a),传入精确大小的数组以获得最佳性能和类型安全。


面试题26:LinkedList的remove方法有几种重载?有什么区别?

两种重载方法

1. remove(int index) - 按索引删除

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));

}

特点:

  • 参数:int index(索引位置)
  • 返回类型:E(被删除的元素)
  • 删除指定索引位置的元素
  • 如果索引越界,抛出IndexOutOfBoundsException

2. remove(Object o) - 按对象删除

public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

特点:

  • 参数:Object o(要删除的对象)
  • 返回类型:boolean(是否删除成功)
  • 删除第一个匹配的元素
  • 使用equals()方法比较(null使用==)
  • 如果元素不存在,返回false,不抛异常

详细对比

维度remove(int index)remove(Object o)
参数类型int(索引)Object(对象)
返回值E(被删除的元素)boolean(是否成功)
删除策略按位置删除按值删除(第一个匹配)
异常处理索引越界抛异常元素不存在返回false
null处理不涉及null使用==比较
时间复杂度O(n)O(n)
查找方式通过node(index)查找遍历查找匹配元素

使用示例

List<String> list = new LinkedList<>(Arrays.asList("A""B""C""B""D"));

// 方式1:按索引删除
String removed1 = list.remove(1);  // 删除索引1的元素"B"
// 结果:removed1 = "B", list = ["A""C""B""D"]

// 方式2:按对象删除
boolean removed2 = list.remove("B");  // 删除第一个"B"
// 结果:removed2 = true, list = ["A""C""D"]

// 删除不存在的元素
boolean removed3 = list.remove("X");  // 元素不存在
// 结果:removed3 = false, list不变

// 索引越界
// String removed4 = list.remove(10);  // 抛出IndexOutOfBoundsException

性能分析

时间复杂度

两者都是O(n):

  • remove(int index):需要先通过node(index)找到节点(O(n)),然后unlink删除(O(1))
  • remove(Object o):需要遍历链表查找匹配元素(O(n)),然后unlink删除(O(1))

性能对比

场景remove(int index)remove(Object o)
头部删除O(1)O(1)
中间删除O(n)O(n)
尾部删除O(1)O(n)
元素不存在抛异常返回false

实现原理

remove(int index)流程

  1. 检查索引:checkElementIndex(index)
  2. 查找节点:node(index) - 根据索引位置选择从头部或尾部遍历
  3. 删除节点:unlink(node) - 断开节点连接

remove(Object o)流程

  1. null处理:如果对象为null,使用==比较
  2. 非null处理:使用equals()方法比较
  3. 遍历查找:从first开始遍历,找到第一个匹配的节点
  4. 删除节点:unlink(node) - 断开节点连接
  5. 返回结果:找到并删除返回true,否则返回false

注意事项

1. 只删除第一个匹配的元素

List<String> list = new LinkedList<>(Arrays.asList("A""B""C""B""A"));
list.remove("B");  // 只删除第一个"B"
// 结果:["A""C""B""A"]

2. null值的处理

List<String> list = new LinkedList<>(Arrays.asList("A", null, "B", null));
list.remove(null);  // 删除第一个null
// 结果:["A", "B", null]

3. 索引越界

List<String> list = new LinkedList<>(Arrays.asList("A""B""C"));

// list.remove(10);  // 抛出IndexOutOfBoundsException

4. 如何删除所有匹配的元素

// 方式1:使用removeIf(推荐)
list.removeIf(item -> item.equals("B"));

// 方式2:循环删除

while (list.remove("B")) {
    // 继续删除
}

// 方式3:使用迭代器

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if ("B".equals(it.next())) {
        it.remove();
    }
}

最佳实践

推荐做法

// ✅ 如果知道索引,使用remove(int index)
String removed = list.remove(0);

// ✅ 如果知道对象,使用remove(Object o)
boolean success = list.remove("target");

// ✅ 删除所有匹配元素,使用removeIf
list.removeIf(item -> item.equals("target"));

### 不推荐做法
// ❌ 不要循环调用remove(Object o)删除所有匹配元素
while (list.remove("target")) {}  // 效率低

// ❌ 不要使用索引删除时不做边界检查
if (index < list.size()) {
    list.remove(index);
}

总结

  • remove(int index):按位置删除,返回被删除的元素,索引越界抛异常
  • remove(Object o):按值删除第一个匹配,返回是否成功,元素不存在返回false
  • 两者时间复杂度都是O(n),但适用场景不同
  • 删除所有匹配元素时,优先使用removeIf方法

面试题27:ArrayList的clone方法如何实现?是深拷贝还是浅拷贝?

clone方法实现

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotS

实现步骤

  1. 调用super.clone():创建ArrayList对象的浅拷贝
  2. 复制数组:使用Arrays.copyOf(elementData, size)创建新数组,只复制实际元素(根据size)
  3. 重置modCount:将modCount设为0
  4. 返回新对象

拷贝类型:浅拷贝(Shallow Copy)

ArrayList的clone()是浅拷贝,原因如下:

浅拷贝的特点

  • 复制ArrayList对象本身
  • 复制数组引用指向的新数组
  • 不复制数组中的元素对象
  • 原列表和克隆列表的元素引用指向同一对象

验证示例

示例1:基本类型(String等不可变对象)

List<String> original = new ArrayList<>(Arrays.asList("A""B""C"));
List<String> cloned = (ArrayList<String>) ((ArrayList<String>) original).clone();

// 修改原列表
original.set(0"X");
// 克隆列表不受影响(因为String是不可变的)
System.out.println(cloned.get(0));  // 输出:"A"

注意:String不可变,所以看起来像深拷贝,实际仍是浅拷贝。

示例2:可变对象(证明是浅拷贝)

List<List<String>> original = new ArrayList<>();
original.add(new ArrayList<>(Arrays.asList("A")));
List<List<String>> cloned = (ArrayList<List<String>>) ((ArrayList<List<String>>) original).clone();

// 修改原列表中的内部列表
original.get(0).add("B");

// 克隆列表也被影响(共享同一个内部列表对象)
System.out.println(cloned.get(0));  // 输出:[A, B]

浅拷贝 vs 深拷贝对比

维度浅拷贝(Shallow Copy)深拷贝(Deep Copy)
对象本身创建新对象创建新对象
数组创建新数组创建新数组
元素对象共享引用(同一对象)创建新对象
修改影响修改元素会影响两个列表修改元素互不影响
内存占用较少较多
性能较快较慢

为什么是浅拷贝?

  1. 性能考虑:深拷贝需要递归复制所有元素,开销大
  2. 通用性:元素类型未知,无法确定如何深拷贝
  3. 设计原则:默认浅拷贝,需要深拷贝时由开发者实现

如何实现深拷贝?

方案1:手动深拷贝(推荐)

public static <T> List<T> deepCopy(List<T> original) {
    List<T> copy = new ArrayList<>();
    for (T item : original) {
        copy.add(deepCopyItem(item));
    }
    return copy;

}

private static <T> T deepCopyItem(T item) {
    // 根据实际类型实现深拷贝
    if (item instanceof Cloneable) {
        // 如果元素实现了Cloneable,可以调用clone
        return (T) ((Cloneable) item).clone();
    }
    // 其他情况需要根据具体类型处理
    return item;  // 如果无法深拷贝,返回原对象

}

方案2:序列化深拷贝(通用但性能较低)

public static <T extends Serializable> List<T> deepCopyBySerialization(List<T> original) {
    try {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(original);
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        return (List<T>) ois.readObject();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

使用建议

浅拷贝适用场景

  • 元素是不可变对象(如String、Integer)
  • 不需要修改元素对象
  • 只需要独立的列表结构

深拷贝适用场景

  • 元素是可变对象(如自定义对象、集合等)
  • 需要完全独立的副本
  • 修改元素不应影响原列表

注意事项

  1. 类型转换:clone()返回Object,需要强制类型转换
  2. 元素共享:浅拷贝时,修改可变元素会影响两个列表
  3. 性能考虑:深拷贝开销大,仅在必要时使用
  4. 元素类型:确保元素类型支持深拷贝(实现Cloneable或Serializable)

总结

  • ArrayList的clone()是浅拷贝
  • 复制了ArrayList对象和数组,但元素对象是共享的
  • 对于不可变对象(如String),行为类似深拷贝
  • 对于可变对象,修改会影响两个列表
  • 需要深拷贝时,需要手动实现或使用序列化等方式

面试题28:ArrayList的isEmpty和size() == 0有什么区别?

源码实现对比

isEmpty()方法

public boolean isEmpty() {
    return size == 0;
}

size()方法

public int size() {
    return size;
}

详细对比

维度isEmpty()size() == 0
方法调用1次方法调用1次方法调用 + 1次比较
返回值类型booleanboolean(通过比较得到)
语义清晰度语义明确,表达"是否为空"语义间接,表达"大小是否为0"
代码可读性更好,意图明确较差,需要理解size的含义
性能O(1)O(1)
代码规范符合Java编码规范不符合最佳实践
IDE提示通常推荐使用可能提示使用isEmpty()

性能分析

时间复杂度

两者都是O(1):

  • isEmpty():直接返回size == 0的结果
  • size() == 0:先调用size()返回size值,再与0比较

性能差异

实际性能几乎相同:

  • 都是直接访问size字段
  • JVM优化后,两者性能差异可忽略
  • 主要区别在于代码可读性和规范

使用示例

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

// 方式1:使用isEmpty()(推荐)
if (list.isEmpty()) {
    System.out.println("列表为空");
}

// 方式2:使用size() == 0(不推荐)
if (list.size() == 0) {
    System.out.println("列表为空");
}

为什么推荐isEmpty()?

1. 可读性更好

// ✅ 语义清晰,表达意图明确
if (list.isEmpty()) {
    // 处理空列表
}

// ❌ 需要理解size的含义
if (list.size() == 0) {
    // 处理空列表
}

2. 符合Java编码规范

  • Java官方文档推荐使用isEmpty()
  • 大多数Java代码规范(如Google Java Style Guide)推荐使用isEmpty()
  • IDE(如IntelliJ IDEA、Eclipse)通常会提示使用isEmpty()

3. 通用性更好

  • isEmpty()适用于所有Collection接口的实现
  • size() == 0 只适用于有size()方法的集合

总结

结论:虽然两者功能相同,但isEmpty()更符合Java编码规范和最佳实践,推荐使用isEmpty()。


面试题29:ArrayList的默认容量为什么是10?可以修改吗?

为什么是10?

  • 经验值:经过大量测试,10是一个平衡点
  • 太小:频繁扩容,性能差
  • 太大:浪费内存,初始化慢
  • 10是一个折中的选择

可以修改吗?

  • 不能直接修改DEFAULT_CAPACITY常量(final)
  • 但可以通过构造器指定初始容量
  • 可以通过ensureCapacity预扩容

实际使用建议:

// 如果知道大概大小,预分配容量
List<String> list = new ArrayList<>(expectedSize);

// 如果不知道大小,使用默认构造器
List<String> list = new ArrayList<>();

面试题30:ArrayList和LinkedList的内存占用差异有多大?

内存占用对比(存储1000个Integer对象):

ArrayList:

  • ArrayList对象:24字节
  • 数组对象:16 + 1000 × 8 = 8016字节
  • Integer对象:1000 × 16 = 16000字节
  • 总计:约24040字节(约23.5KB)

LinkedList:

  • LinkedList对象:24字节
  • Node对象:1000 × 28 = 28000字节
  • Integer对象:1000 × 16 = 16000字节
  • 总计:约44024字节(约43KB)

差异:LinkedList是ArrayList的1.83倍

影响因素:

  • 元素数量:越多差异越大
  • 元素大小:元素越大,相对差异越小
  • JVM配置:指针压缩可以减小差距

面试题31:ArrayList的equals方法如何实现?

equals方法源码

public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof List))
        return false;
    ListIterator<E> e1 = listIterator();
    ListIterator<?> e2 = ((List<?>) o).listIterator();
    while (e1.hasNext() && e2.hasNext()) {
        E o1 = e1.next();
        Object o2 = e2.next();
        if (!(o1==null ? o2==null : o1.equals(o2)))
            return false;
    }
    return !(e1.hasNext() || e2.hasNext());
}

实现步骤

1. 引用相等检查

if (o == this)
    return true;
  • 如果引用相同,直接返回true
  • 避免不必要的遍历

2. 类型检查

if (!(o instanceof List))
    return false;
  • 检查是否为List类型
  • 不是List则返回false

3. 获取迭代器

ListIterator<E> e1 = listIterator();
ListIterator<?> e2 = ((List<?>) o).listIterator();
  • 获取两个列表的ListIterator
  • 使用迭代器逐个比较元素

4. 逐个比较元素

while (e1.hasNext() && e2.hasNext()) {
    E o1 = e1.next();
    Object o2 = e2.next();
    if (!(o1==null ? o2==null : o1.equals(o2)))
        return false;
}
  • 同时遍历两个列表
  • 对每个元素:
  • 如果都为null,认为相等
  • 如果都不为null,使用equals()比较
  • 如果不等,返回false

5. 长度检查

return !(e1.hasNext() || e2.hasNext());
  • 检查是否还有剩余元素
  • 如果任一列表还有元素,说明长度不同,返回false
  • 否则返回true

关键点

  1. 先检查引用相等:o == this
  2. 检查类型:必须是List类型
  3. 逐个比较元素:使用equals()方法(不是==)
  4. null处理:两个null视为相等
  5. 长度检查:确保两个列表长度相同

时间复杂度

  • 时间复杂度:O(n)
  • 需要遍历所有元素进行比较
  • 最好情况:O(1)(引用相等或类型不匹配)
  • 最坏情况:O(n)(需要比较所有元素)

equals方法的特性

1. 自反性(Reflexive)

list.equals(list);  // 总是返回true

2. 对称性(Symmetric)

list1.equals(list2) == list2.equals(list1);  // 总是相等

3. 传递性(Transitive)

// 如果list1.equals(list2) && list2.equals(list3)
// 那么list1.equals(list3)也为true

4. 一致性(Consistent)

// 多次调用equals(),结果应该一致
list1.equals(list2);  // true
list1.equals(list2);  // true(结果相同)

5. 非空性(Non-nullity)

list.equals(null);  // 总是返回false

与hashCode的关系

根据Java规范,如果两个对象equals()返回true,它们的hashCode()也必须相同:

List<String> list1 = new ArrayList<>(Arrays.asList("A""B"));
List<String> list2 = new ArrayList<>(Arrays.asList("A""B"));
if (list1.equals(list2)) {
    // hashCode必须相同
    assert list1.hashCode() == list2.hashCode();
}

总结

  • ArrayList的equals()方法实现了完整的相等性比较
  • 先检查引用相等,再检查类型,然后逐个比较元素
  • 使用equals()比较元素(不是==),正确处理null值
  • 时间复杂度为O(n),需要遍历所有元素
  • 符合Java的equals()方法规范(自反性、对称性、传递性等)

面试题32:ArrayList的hashCode方法如何实现?

hashCode方法实现:

public int hashCode() {
    int hashCode = 1;
    for (E e : this)
        hashCode = 31 * hashCode + (e == null ? 0 : e.hashCode());
    return hashCode;
}

关键点:

  1. 使用31作为乘数(经验值,31是质数,31×i可以用位移优化)
  2. 对每个元素调用hashCode方法
  3. null元素的hashCode为0
  4. 累加所有元素的hashCode

时间复杂度:O(n)

  • 需要遍历所有元素

为什么用31?

  • 31是质数,减少哈希冲突
  • 31×i可以优化为(i << 5) - i,性能好
  • 经验值,经过大量测试验证

面试题33:LinkedList的spliterator方法是什么?如何使用?

spliterator:可分割迭代器,用于并行处理

Spliterator<String> spliterator = list.spliterator();

// 顺序处理
spliterator.forEachRemaining(item -> {
    System.out.println(item);
});

// 并行处理
Spliterator<String> spliterator2 = list.spliterator();
Spliterator<String> spliterator3 = spliterator2.trySplit();
// 可以在不同线程中处理spliterator2和spliterator3

特点:

  • 支持并行处理
  • 可以分割成多个部分
  • 用于Stream API的并行流

使用场景:

  • 大数据量处理
  • 需要并行计算的场景
  • Stream API的底层实现

面试题34:ArrayList的sort方法如何实现?时间复杂度是多少?

sort方法实现:

public void sort(Comparator<? super E> c) {
    final int expectedModCount = modCount;
    Arrays.sort((E[]) elementData, 0, size, c);
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

时间复杂度:O(n log n)

  • 使用Arrays.sort,底层是TimSort或Dual-Pivot QuickSort
  • 平均情况:O(n log n)
  • 最好情况:O(n)(已排序)
  • 最坏情况:O(n log n)

使用示例:

List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5));
list.sort(Comparator.naturalOrder());
// 结果:[1, 1, 3, 4, 5]

注意事项:

  • 会检查并发修改
  • 使用指定的Comparator排序
  • 原地排序,不创建新列表

面试题35:ArrayList的parallelStream和stream有什么区别?

stream:串行流

list.stream()
    .filter(s -> s.length() > 3)
    .map(String::toUpperCase)
    .forEach(System.out::println);

parallelStream:并行流

list.parallelStream()
    .filter(s -> s.length() > 3)
    .map(String::toUpperCase)
    .forEach(System.out::println);

区别:

维度streamparallelStream
执行方式串行并行
线程安全不需要需要保证线程安全
性能小数据量快大数据量快
适用场景一般场景CPU密集型,大数据量

注意事项:

  • 并行流有开销,小数据量可能更慢
  • 需要保证操作是线程安全的
  • 通常数据量>10000才考虑并行