Java ArrayList案例实操:从入门到项目实战的完整指南
📚 文章目录导读
- 第一部分:ArrayList核心机制深度解析
- ArrayList与数组的本质区别
- 动态扩容原理与性能陷阱
- 第二部分:5大高频实操案例(附代码)
- 案例1:学生成绩管理系统(增删改查)
- 案例2:购物车商品列表管理
- 案例3:去重与数据清洗实战
- 案例4:排序与自定义对象比较
- 案例5:多线程环境下的安全操作
- 第三部分:常见问题与性能优化FAQ
- 第四部分:面试高频问题与避坑指南
第一部分:ArrayList核心机制深度解析
在Java集合框架中,ArrayList是开发中使用频率最高的类之一,许多开发者问:“为什么不用数组而用ArrayList?” 这正是我们首先要明确的。

1 ArrayList与数组的本质区别
- 数组:长度固定,声明后无法改变,例如
int[] arr = new int[5];,只能存储5个元素,一旦存满必须新建数组并手动复制数据。 - ArrayList:底层基于数组实现(Object[] elementData),但自动管理动态扩容,当元素数量超过容量时,会创建新数组(原容量的1.5倍),并将旧数据复制过去。
实操对比代码:
// 数组方式
String[] oldArr = new String[3];
oldArr[0] = "A"; oldArr[1] = "B"; oldArr[2] = "C";
// 要添加第4个?必须手动扩容
String[] newArr = new String[oldArr.length * 2]; // 手动扩容
System.arraycopy(oldArr, 0, newArr, 0, oldArr.length);
newArr[3] = "D";
// ArrayList方式
ArrayList<String> list = new ArrayList<>(3);
list.add("A"); list.add("B"); list.add("C");
list.add("D"); // 自动扩容,无需手动处理
💡 问答环节
Q:ArrayList默认初始容量是多少?扩容时具体扩多少倍?
A:默认初始容量为10(JDK 1.7+),当添加第11个元素时触发扩容,新容量 = 旧容量 × 1.5(即int newCapacity = oldCapacity + (oldCapacity >> 1)),然后执行Arrays.copyOf()复制数据,注意:若1.5倍后仍小于minCapacity(实际需要的最小容量),则直接取minCapacity。
第二部分:5大高频实操案例(附代码)
案例1:学生成绩管理系统(增删改查)
需求:存储学生姓名和成绩,支持添加、根据姓名删除、修改成绩、查询最高分。
class Student {
String name;
int score;
// 构造方法、getter/setter省略
}
ArrayList<Student> students = new ArrayList<>();
// 添加
students.add(new Student("张三", 85));
// 删除(按姓名)
students.removeIf(s -> s.getName().equals("李四")); // JDK 8+
// 修改
for (Student s : students) {
if ("王五".equals(s.getName())) {
s.setScore(95);
break;
}
}
// 查询最高分
int max = students.stream()
.mapToInt(Student::getScore)
.max()
.orElse(0);
注意:removeIf内部使用了迭代器,遍历过程中直接删除元素是安全的,若用普通for循环删除,需注意索引变化。
案例2:购物车商品列表管理
需求:实现购物车中商品的增加、修改数量、计算总价、清空。
class CartItem {
String productId;
String name;
int quantity;
double price;
// 构造、getter/setter
}
ArrayList<CartItem> cart = new ArrayList<>();
// 添加商品(若已存在则增加数量)
public void addProduct(CartItem item) {
for (CartItem c : cart) {
if (c.getProductId().equals(item.getProductId())) {
c.setQuantity(c.getQuantity() + item.getQuantity());
return;
}
}
cart.add(item);
}
// 计算总价
double total = cart.stream()
.mapToDouble(i -> i.getPrice() * i.getQuantity())
.sum();
// 清空
cart.clear(); // 注意:clear只是清空列表,对象本身等待GC
💡 问答环节
Q:如何避免购物车中同一个商品被重复添加(即去重逻辑)?
A:核心是重写CartItem的equals()和hashCode()方法,基于productId判断,但更推荐方案:使用Map<String, CartItem>(key为productId)替代ArrayList,查询和去重效率更高(O(1) vs O(n))。
案例3:去重与数据清洗实战
需求:从数据库中查询到一批用户ID列表(List
// 方案1:利用HashSet去重(推荐)
List<String> originalIds = Arrays.asList("A001","A002","A001","A003","A002");
List<String> distinctIds = new ArrayList<>(new HashSet<>(originalIds));
// 方案2:保持原顺序去重(LinkedHashSet)
List<String> distinctOrdered = new ArrayList<>(new LinkedHashSet<>(originalIds));
// 方案3:JDK 8 stream去重
List<String> distinctStream = originalIds.stream()
.distinct()
.collect(Collectors.toList());
性能对比:HashSet去重时间复杂度O(n),适合大量数据;stream去重底层也是用LinkedHashSet,但会创建额外流对象。
案例4:排序与自定义对象比较
需求:对商品按价格降序、名称升序排序。
class Product implements Comparable<Product> {
String name;
double price;
@Override
public int compareTo(Product o) {
// 先按价格降序,再按名称升序
int priceCompare = Double.compare(o.price, this.price); // 降序
if (priceCompare != 0) return priceCompare;
return this.name.compareTo(o.name); // 升序
}
}
// 使用Collections.sort
Collections.sort(productList);
// 或使用Comparator(推荐,更灵活)
productList.sort(Comparator.comparingDouble(Product::getPrice).reversed()
.thenComparing(Product::getName));
注意:自定义对象必须实现Comparable接口并重写compareTo(),或使用Comparator,compareTo返回负数表示当前对象小于参数对象,0相等,正数大于。
案例5:多线程环境下的安全操作
常见问题:多个线程同时读写ArrayList会抛出ConcurrentModificationException。
解决方案:
// 方案1:使用Collections.synchronizedList包装
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
// 但迭代时仍需手动同步:
synchronized (safeList) {
Iterator<String> it = safeList.iterator();
while(it.hasNext()) { ... }
}
// 方案2:使用CopyOnWriteArrayList(读多写少场景)
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
// 写操作会复制整个数组,适合读频繁、写极少的场景
💡 问答环节
Q:为什么CopyOnWriteArrayList读操作不需要加锁?
A:因为它的读操作(get、iterator)直接作用于底层数组的引用,即使写操作时复制新数组,读操作仍指向旧数组,互不干扰,这是“写时复制”策略的核心优势。
第三部分:常见问题与性能优化FAQ
Q1:ArrayList和LinkedList如何选择?
- ArrayList:随机访问快(O(1)),尾部插入/删除快(O(1)),但中间插入/删除慢(O(n)),因为要移动元素。
- LinkedList:双向链表,头部/尾部操作快(O(1)),中间插入/删除也快(O(1)),但随机访问极慢(O(n))。
- 绝大多数场景选ArrayList,除非你需要频繁在列表中间增删元素。
Q2:如何避免ArrayList扩容带来的性能损耗?
- 预分配容量:如果已知要存储1000个元素,使用
new ArrayList<>(1000),避免多次扩容导致的数组复制。 - 使用ensureCapacity():若后期需要大量新增,可先调用
list.ensureCapacity(minCapacity)。
Q3:subList()返回的是什么?有何风险?
List<String> subList = originalList.subList(1, 3);
- subList返回的是原列表的视图,对subList的修改会直接反映到原列表。
- 警告:若在subList操作期间修改原列表(如add/remove),子列表将失效并抛出ConcurrentModificationException。
第四部分:面试高频问题与避坑指南
高频面试题
-
ArrayList的扩容机制中,为什么扩容1.5倍而不是2倍?
答:1.5倍是权衡时间与空间的较优解,扩容倍数过大(如2倍)浪费内存;倍数过小(如1.25倍)需频繁扩容,复制成本高,1.5倍计算使用右移oldCapacity >> 1实现,高效且避免浮点运算。 -
如何实现ArrayList的线程安全,为什么Vector逐渐被淘汰?
答:使用Collections.synchronizedList()或CopyOnWriteArrayList,Vector虽线程安全,但所有方法都用synchronized修饰,并发性能极差,且不支持复合操作的原子性(如“检查再添加”仍需外部同步)。 -
ArrayList的toArray()和toArray(T[] a)有什么区别?
答:无参版本返回Object[],不能强转为具体类型;有参版本若传入数组容量足够,直接填充该数组,否则创建新数组,推荐写法:String[] arr = list.toArray(new String[0])。
避坑指南
- 不要使用for循环删除元素:遍历时删除中间元素会导致索引错乱,应使用迭代器或removeIf。
- 注意remove(int index)和remove(Object o)的区别:对于List
,remove(1)会删除索引1的元素,而非数值1,若想删除数值1,需拆箱:remove(Integer.valueOf(1))。 - 使用List.of()创建的不可变列表:Java 9+的
List.of()返回不可修改的列表,不能执行add/remove操作,否则抛出UnsupportedOperationException。
总结与实践建议
ArrayList作为Java开发中最常用的集合类,其核心价值在于“动态数组+自动扩容”的便捷性,通过上面的5个案例,你应该已经掌握了从基础操作到多线程安全、从去重排序到性能优化的完整技能。
下一步实战建议:
- 尝试用ArrayList实现一个简易的“最近浏览记录”(LRU缓存),需考虑去重和容量限制。
- 在实际项目中,遇到需要频繁查找的场景(如根据ID查找用户),用ArrayList还是HashMap?答案显然是HashMap,因为ArrayList的indexOf是O(n)。
掌握ArrayList只是第一步,结合Map、Set和Stream API,才能写出高效、简洁的Java代码,如果你在实际项目中遇到过ArrayList的坑,欢迎在评论区分享你的案例。
本文由原创技术作者“Hedwig”撰写,未经许可禁止转载。