一、图书馆的智能书架: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 书架的三种创建方式
图书馆提供三种书架初始化方式:
-
空书架(默认 10 格)
java
List<String> bookshelf = new ArrayList<>(); // 空书架,首次放书时自动扩展到10格
-
指定大小的书架
java
List<String> bookshelf = new ArrayList<>(20); // 初始化20格的书架
-
复制其他书架的书籍
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)时,书架会:
-
检查容量是否足够,不足则扩容
-
在尾部放入新书
-
更新书籍数量
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 解决方案:给书架加锁或复制
-
加锁方案(同步书架):
java
List<Integer> safeShelf = Collections.synchronizedList(new ArrayList<>());
Thread t1 = new Thread(() -> {
synchronized (safeShelf) { // 每次操作都加锁
safeShelf.add(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 性能优化建议
-
预设置初始容量:
java
// 知道要存1000本书,提前设置容量
List<String> bookshelf = new ArrayList<>(1000);
-
及时缩容:
java
// 大量删除后,释放多余空间
bookshelf.trimToSize();
-
避免自动装箱拆箱:
java
// 推荐使用Integer数组,避免int自动装箱
List<Integer> intShelf = new ArrayList<>();
六、ArrayList vs LinkedList:书架类型选择
| 场景 | ArrayList(动态数组书架) | LinkedList(链表书架) |
|---|---|---|
| 取书速度 | 快(O (1),直接按位置拿) | 慢(O (n),从头找到尾) |
| 中间插书 / 捐书 | 慢(需移动后面的书) | 快(只需改前后书的指针) |
| 空间占用 | 紧凑(数组连续) | 浪费(每个书有前后指针) |
| 多线程安全性 | 不安全(需额外处理) | 不安全(同上) |
| 典型场景 | 学生成绩表、固定格式数据 | 聊天消息、频繁增删的列表 |
七、总结:ArrayList 的核心秘密
通过图书馆书架的比喻,我们理解了ArrayList的核心原理:
-
基于数组实现,支持动态扩容
-
添加 / 删除中间元素需要移动数据,性能 O (n)
-
随机访问性能优秀,O (1) 时间
-
非线程安全,多线程需额外处理
-
合理设置初始容量和缩容,避免性能损耗
记住这个动态书架的故事,下次使用ArrayList时,你就能像资深图书馆管理员一样,高效管理你的数据了!