1. ArrayList 核心知识点解析
ArrayList 作为 Java 集合框架中最常用的动态数组实现,其重要性不言而喻。我们先从底层实现原理开始,逐步剖析这个看似简单却暗藏玄机的数据结构。
1.1 底层实现机制
ArrayList 的底层实际上是一个 Object[] 数组,这个设计决定了它的核心特性。当我们在代码中声明 ArrayList<String> list = new ArrayList<>() 时,JVM 会在堆内存中分配一个默认容量为 10 的 Object 数组。
关键点:虽然我们指定了泛型类型 String,但运行时仍然使用 Object[] 存储,这是 Java 泛型类型擦除特性的体现。
扩容机制是 ArrayList 最精妙的设计之一。当元素数量超过当前容量时,会触发 grow() 方法:
java复制private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
这个扩容过程会创建新数组并复制元素,时间复杂度为 O(n),这也是为什么在已知数据量时建议使用带初始容量的构造方法。
1.2 核心操作时间复杂度
理解各个操作的时间成本对写出高效代码至关重要:
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| get(int) | O(1) | 直接通过索引访问数组元素 |
| add(E) | 均摊 O(1) | 尾部插入,偶尔触发扩容 |
| add(int, E) | O(n) | 需要移动插入点后的所有元素 |
| remove(int) | O(n) | 需要移动被删除元素后的所有元素 |
| contains(E) | O(n) | 需要遍历整个数组 |
| iterator() | O(1) | 创建迭代器对象本身开销很小,但遍历过程是 O(n) |
1.3 线程安全问题实战分析
ArrayList 的非线程安全特性常常被初学者忽视。看下面这个典型问题场景:
java复制List<String> list = new ArrayList<>();
// 线程1
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
list.add("A" + i);
}
}).start();
// 线程2
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
list.add("B" + i);
}
}).start();
这段代码运行时可能出现三种异常情况:
- ArrayIndexOutOfBoundsException:两个线程同时触发扩容导致数组越界
- NullPointerException:元素覆盖导致空指针
- 数据不一致:最终元素数量少于预期
解决方案根据场景不同有多种选择:
- Collections.synchronizedList():适合读多写少场景
- CopyOnWriteArrayList:适合读远多于写场景
- Vector:历史遗留类,不推荐使用
2. ArrayList 综合案例实战
2.1 外卖系统菜品管理优化版
原始案例已经实现了基本功能,我们可以从以下几个方面进行深度优化:
2.1.1 输入验证增强
原始代码直接使用 sc.next() 接收输入存在风险,改进后的版本:
java复制public String getValidatedInput(String prompt, Predicate<String> validator) {
while (true) {
System.out.print(prompt);
String input = sc.nextLine().trim();
if (validator.test(input)) {
return input;
}
System.out.println("输入无效,请重新输入!");
}
}
// 使用示例
String name = getValidatedInput("请输入菜品名称:",
s -> !s.isEmpty() && s.length() <= 20);
2.1.2 价格计算策略模式
引入策略模式处理不同的计价方式:
java复制interface PricingStrategy {
double calculatePrice(double originalPrice);
}
class DiscountStrategy implements PricingStrategy {
private double discountRate;
public DiscountStrategy(double rate) {
this.discountRate = rate;
}
@Override
public double calculatePrice(double originalPrice) {
return originalPrice * discountRate;
}
}
// 在Food类中使用
public void applyPricingStrategy(PricingStrategy strategy) {
this.vipPrice = strategy.calculatePrice(this.oldPrice);
}
2.1.3 分页查询实现
当菜品数量较多时,需要分页显示:
java复制public void showFoodsByPage(int pageSize) {
int totalPages = (int) Math.ceil((double)list.size() / pageSize);
int currentPage = 0;
while (currentPage < totalPages) {
System.out.println("=== 第 " + (currentPage+1) + " 页 ===");
int start = currentPage * pageSize;
int end = Math.min(start + pageSize, list.size());
for (int i = start; i < end; i++) {
System.out.println(list.get(i));
}
System.out.println("1-下一页 2-上一页 3-退出");
String choice = sc.next();
// 处理翻页逻辑...
}
}
2.2 购物车批量删除进阶版
原始案例解决了基本删除问题,我们可以进一步扩展:
2.2.1 多条件复合删除
支持更复杂的删除条件:
java复制public void removeItems(Predicate<String> condition) {
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
if (condition.test(item)) {
it.remove();
}
}
}
// 使用示例:删除所有包含"枸杞"且价格高于50的商品
removeItems(item -> item.contains("枸杞") && getPrice(item) > 50);
2.2.2 删除日志记录
记录删除操作便于审计:
java复制class RemovalLog {
private String itemName;
private LocalDateTime removalTime;
// getters/setters...
}
List<RemovalLog> removalLogs = new ArrayList<>();
public void safeRemove(String item) {
if (list.remove(item)) {
RemovalLog log = new RemovalLog();
log.setItemName(item);
log.setRemovalTime(LocalDateTime.now());
removalLogs.add(log);
}
}
2.2.3 批量删除性能优化
对于超大型列表,可以考虑分批处理:
java复制public void batchRemove(List<String> itemsToRemove) {
List<String> tempList = new ArrayList<>(list.size());
Set<String> removalSet = new HashSet<>(itemsToRemove);
for (String item : list) {
if (!removalSet.contains(item)) {
tempList.add(item);
}
}
list = tempList;
}
3. ArrayList 高级应用技巧
3.1 自定义排序的多种实现
3.1.1 Comparator 链式排序
java复制// 多条件排序:先按价格降序,再按名称升序
foods.sort(Comparator.comparingDouble(Food::getVipPrice)
.reversed()
.thenComparing(Food::getName));
3.1.2 自定义排序算法
实现冒泡排序算法:
java复制public static <T> void bubbleSort(List<T> list, Comparator<? super T> c) {
for (int i = 0; i < list.size() - 1; i++) {
for (int j = 0; j < list.size() - 1 - i; j++) {
if (c.compare(list.get(j), list.get(j + 1)) > 0) {
Collections.swap(list, j, j + 1);
}
}
}
}
3.2 内存优化技巧
3.2.1 容量调整
对于不再增长的 ArrayList,可以调用 trimToSize() 释放多余空间:
java复制ArrayList<String> largeList = new ArrayList<>(10000);
// 添加大量元素后...
largeList.trimToSize(); // 将底层数组调整为刚好容纳当前元素
3.2.2 元素清空优化
清空列表的几种方式对比:
java复制// 方法1:保留底层数组
list.clear();
// 方法2:完全新建数组(GC友好)
list = new ArrayList<>();
// 方法3:特殊场景下的优化
while (!list.isEmpty()) {
list.remove(list.size() - 1);
}
3.3 并行处理技巧
3.3.1 并行流处理
java复制List<String> processed = list.parallelStream()
.filter(item -> item.contains("枸杞"))
.map(String::toUpperCase)
.collect(Collectors.toList());
3.3.2 读写分离方案
java复制// 写时复制模式
List<String> snapshot = Collections.unmodifiableList(new ArrayList<>(list));
// 读者线程可以安全地使用snapshot
for (String item : snapshot) {
System.out.println(item);
}
4. 性能调优与问题排查
4.1 性能基准测试
使用 JMH 进行性能测试:
java复制@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class ArrayListBenchmark {
@State(Scope.Thread)
public static class MyState {
List<Integer> arrayList = new ArrayList<>();
@Setup(Level.Trial)
public void doSetup() {
for (int i = 0; i < 10000; i++) {
arrayList.add(i);
}
}
}
@Benchmark
public void testAdd(MyState state) {
state.arrayList.add(1000);
}
@Benchmark
public void testRemove(MyState state) {
state.arrayList.remove(0);
}
}
4.2 常见问题排查指南
4.2.1 ConcurrentModificationException 深度分析
这个异常的根本原因是 modCount 机制:
java复制final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
解决方案对比表:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 使用迭代器删除 | 单线程环境 | 安全可靠 | 代码稍复杂 |
| CopyOnWriteArrayList | 多线程读多写少 | 线程安全 | 写性能差,内存占用大 |
| synchronizedList | 多线程环境 | 简单易用 | 性能较差 |
| 倒序删除 | 单线程简单场景 | 代码简单 | 不适用于复杂条件删除 |
4.2.2 内存泄漏排查
ArrayList 可能导致内存泄漏的典型场景:
java复制// 场景1:静态集合持有对象引用
private static List<Object> staticList = new ArrayList<>();
public void addToStaticList(Object obj) {
staticList.add(obj); // 这些对象永远不会被GC回收
}
// 场景2:未及时清理的监听器列表
public class EventSource {
private List<Listener> listeners = new ArrayList<>();
public void addListener(Listener l) {
listeners.add(l);
}
// 忘记实现removeListener方法...
}
排查工具建议:
- Eclipse Memory Analyzer (MAT)
- VisualVM
- JProfiler
4.3 最佳实践总结
-
初始化容量:在已知数据量时,始终指定初始容量
java复制// 不好 List<String> list1 = new ArrayList<>(); // 好 List<String> list2 = new ArrayList<>(expectedSize); -
遍历选择:
- 只需要元素 → 增强for循环
- 需要索引 → 传统for循环
- 需要删除 → 迭代器
-
线程安全:
- 读多写少 → CopyOnWriteArrayList
- 写操作频繁 → Collections.synchronizedList() + 同步块
-
性能敏感场景:
- 考虑使用 Arrays.asList() 替代小型不可变列表
- 考虑使用 LinkedList 当频繁在中间位置插入/删除
-
与Stream API结合:
java复制// 传统方式 List<String> filtered = new ArrayList<>(); for (String item : list) { if (item.length() > 3) { filtered.add(item); } } // Stream方式 List<String> filtered = list.stream() .filter(item -> item.length() > 3) .collect(Collectors.toList());
在实际项目中,ArrayList 90% 的使用场景都可以遵循这些原则。对于特别复杂的场景,可以考虑使用更专业的集合实现如 Guava 的 ImmutableList 或者 Apache Commons 的 FastArrayList。