图书馆动态书架:从源码角度解析 Java ArrayList 的奇妙世界

66 阅读6分钟

一、图书馆的智能书架:ArrayList 的基本概念

想象你走进一家高科技图书馆,这里的书架会根据藏书量自动调整大小,这就是 Java 中的ArrayList—— 一个会 "成长" 的动态数组书架。与传统固定大小的书架不同,它能在你放书时自动扩展,在你捐书时收缩空间,是程序员最常用的数据结构之一。

1.1 书架的基本结构

每个ArrayList就像一个智能书架系统,包含三个核心部分:

  • 实际放书的书架格子elementData(Object [] 数组)

  • 当前书架上的书数量size

  • 书架的默认初始容量DEFAULT_CAPACITY=10(就像新书架默认有 10 个格子)

java

// ArrayList的核心属性
private transient Object[] elementData; // 存储书籍的书架
private int size; // 当前书籍数量
private static final int DEFAULT_CAPACITY = 10; // 默认书架容量
private static final Object[] EMPTY_ELEMENTDATA = {}; // 空书架

1.2 书架的三种创建方式

图书馆提供三种书架初始化方式:

  1. 空书架(默认 10 格)

java

List<String> bookshelf = new ArrayList<>(); // 空书架,首次放书时自动扩展到10格
  1. 指定大小的书架

java

List<String> bookshelf = new ArrayList<>(20); // 初始化20格的书架
  1. 复制其他书架的书籍

java

List<String> sourceShelf = Arrays.asList("Java", "Python", "C++");
List<String> bookshelf = new ArrayList<>(sourceShelf); // 复制sourceShelf的书籍

二、书架操作:ArrayList 的核心方法

2.1 放书到书架:add 方法

当你向书架放书时,ArrayList会智能处理:

  • 普通放书(尾部添加)

  • 插队放书(指定位置插入)

java

// 场景:向书架放书
List<String> bookshelf = new ArrayList<>();

// 1. 尾部放书:最常用的放书方式
bookshelf.add("《数据结构》");
bookshelf.add("《算法导论》");
System.out.println("当前书架:" + bookshelf); // [《数据结构》, 《算法导论》]

// 2. 插队放书:在指定位置插入
bookshelf.add(1, "《Java编程思想》");
System.out.println("插入后:" + bookshelf); // [《数据结构》, 《Java编程思想》, 《算法导论》]

放书的幕后故事:源码解析

当调用add(e)时,书架会:

  1. 检查容量是否足够,不足则扩容

  2. 在尾部放入新书

  3. 更新书籍数量

java

public boolean add(E e) {
    ensureCapacityInternal(size + 1); // 检查容量
    elementData[size++] = e; // 放书并更新数量
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    // 计算需要的最小容量,首次放书时默认10格
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity); // 确认容量
}

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

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容1.5倍
    if (newCapacity < minCapacity) newCapacity = minCapacity; // 确保足够大
    elementData = Arrays.copyOf(elementData, newCapacity); // 复制到新书架
}

2.2 从书架取书:get 方法

你可以直接按位置取书,就像知道书在第 3 格,直接伸手去拿:

java

List<String> bookshelf = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
String book = bookshelf.get(2); // 取第3格的书
System.out.println("取出的书:" + book); // 输出: C

取书的秘密:数组随机访问

java

public E get(int index) {
    rangeCheck(index); // 检查位置是否合法
    return elementData(index); // 直接按索引取书
}

E elementData(int index) {
    return (E) elementData[index]; // 数组随机访问,时间O(1)
}

private void rangeCheck(int index) {
    if (index >= size) throw new IndexOutOfBoundsException(); // 防止越界
}

2.3 从书架捐书:remove 方法

当你需要移除一本书时,后面的书会自动往前挪:

java

List<String> bookshelf = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
String removedBook = bookshelf.remove(1); // 移除第2格的书
System.out.println("移除的书:" + removedBook); // 输出: B
System.out.println("剩余书籍:" + bookshelf); // [A, C, D]

捐书的麻烦:数组元素移动

java

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; // 最后一格置空,帮助垃圾回收
    return oldValue;
}

三、书架的智能管理:容量优化

3.1 自动扩容:书架不够用时的自我扩展

当书架快满时,会自动扩展容量,就像图书馆管理员发现书架快满了,立即增加 50% 的格子:

java

List<Integer> shelf = new ArrayList<>();
for (int i = 0; i < 15; i++) {
    shelf.add(i);
    // 打印每次放书后的书架容量
    System.out.println("放入" + i + "后,容量:" + java.lang.reflect.Array.getLength(shelf.toArray()));
}
/* 输出:
放入0-9后容量都是10(默认)
放入10时容量变为15(10+10/2)
放入11-14时容量还是15(未超过15)
*/

3.2 手动调整容量:预先设置书架大小

如果你知道要放 20 本书,可以提前设置书架大小,避免多次扩容:

java

List<String> bookshelf = new ArrayList<>();
bookshelf.ensureCapacity(20); // 手动设置最小容量20
for (int i = 0; i < 18; i++) {
    bookshelf.add("书" + i);
}
// 全程容量都是20,不会触发自动扩容

3.3 缩容:释放多余的书架空间

当你捐出很多书后,可以收缩书架,节省空间:

java

List<String> bookshelf = new ArrayList<>(Arrays.asList("A", "B", "C", "D", "E"));
bookshelf.remove(2); // 移除一本书
bookshelf.remove(2); // 再移除一本
System.out.println("缩容前容量:" + java.lang.reflect.Array.getLength(bookshelf.toArray())); // 5
bookshelf.trimToSize(); // 缩容到实际数量
System.out.println("缩容后容量:" + java.lang.reflect.Array.getLength(bookshelf.toArray())); // 3

四、多读者问题:ArrayList 的线程安全隐患

4.1 混乱的图书馆:多线程操作的问题

当多个读者同时放书和取书时,可能会出现混乱,比如书被放错位置或丢失:

java

// 危险!多线程同时操作ArrayList
List<Integer> shelf = new ArrayList<>();
Thread t1 = new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        shelf.add(i);
    }
});
Thread t2 = new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        if (shelf.size() > 0) {
            shelf.remove(0); // 同时删除,可能出错
        }
    }
});
t1.start();
t2.start();
// 可能抛出ConcurrentModificationException或数据错乱

4.2 解决方案:给书架加锁或复制

  1. 加锁方案(同步书架):

java

List<Integer> safeShelf = Collections.synchronizedList(new ArrayList<>());
Thread t1 = new Thread(() -> {
    synchronized (safeShelf) { // 每次操作都加锁
        safeShelf.add(1);
    }
});
  1. 复制书架方案(适合读多写少场景):

java

import java.util.concurrent.CopyOnWriteArrayList;

CopyOnWriteArrayList<Integer> cowShelf = new CopyOnWriteArrayList<>();
Thread reader = new Thread(() -> {
    for (Integer book : cowShelf) {
        System.out.println(book); // 读操作不阻塞
    }
});
Thread writer = new Thread(() -> {
    cowShelf.add(100); // 写时复制新数组,不影响读者
});

五、书架使用指南:ArrayList 的最佳实践

5.1 适用场景

  • 推荐使用

    • 需要频繁按位置取书(随机访问)
    • 数据量可预估,或尾部添加为主
    • 单线程环境下使用
  • 不推荐使用

    • 频繁在中间位置插入 / 删除(推荐用 LinkedList)
    • 多线程并发操作(需额外处理)
    • 数据量极小且固定(浪费空间)

5.2 性能优化建议

  1. 预设置初始容量:

java

// 知道要存1000本书,提前设置容量
List<String> bookshelf = new ArrayList<>(1000);
  1. 及时缩容:

java

// 大量删除后,释放多余空间
bookshelf.trimToSize();
  1. 避免自动装箱拆箱:

java

// 推荐使用Integer数组,避免int自动装箱
List<Integer> intShelf = new ArrayList<>();

六、ArrayList vs LinkedList:书架类型选择

场景ArrayList(动态数组书架)LinkedList(链表书架)
取书速度快(O (1),直接按位置拿)慢(O (n),从头找到尾)
中间插书 / 捐书慢(需移动后面的书)快(只需改前后书的指针)
空间占用紧凑(数组连续)浪费(每个书有前后指针)
多线程安全性不安全(需额外处理)不安全(同上)
典型场景学生成绩表、固定格式数据聊天消息、频繁增删的列表

七、总结:ArrayList 的核心秘密

通过图书馆书架的比喻,我们理解了ArrayList的核心原理:

  • 基于数组实现,支持动态扩容

  • 添加 / 删除中间元素需要移动数据,性能 O (n)

  • 随机访问性能优秀,O (1) 时间

  • 非线程安全,多线程需额外处理

  • 合理设置初始容量和缩容,避免性能损耗

记住这个动态书架的故事,下次使用ArrayList时,你就能像资深图书馆管理员一样,高效管理你的数据了!