1. ArrayList基础操作全解析:从字符串到自定义对象
ArrayList作为Java集合框架中最常用的动态数组实现,几乎出现在所有Java开发者的日常编码中。今天我们就来深入探讨ArrayList对字符串和自定义对象的存储、修改、删除及遍历操作。这些基础操作看似简单,但其中隐藏着不少性能陷阱和最佳实践。
先看一个最基础的字符串存储示例:
ArrayList<String> strList = new ArrayList<>(); strList.add("Java"); strList.add("Python"); strList.add("C++");1.1 核心API操作原理
ArrayList的底层实现是一个Object[]数组,当添加元素时:
- 检查当前数组容量是否足够
- 不足时进行扩容(通常扩容为原来的1.5倍)
- 将元素添加到数组末尾
这种实现方式决定了它的几个重要特性:
- 随机访问速度快(O(1)复杂度)
- 尾部插入效率高
- 中间插入/删除效率较低(需要移动后续元素)
重要提示:初始化时如果知道大概的元素数量,应该使用带初始容量的构造函数,避免频繁扩容带来的性能损耗。
2. 存储自定义对象的完整实践
存储自定义对象是ArrayList更常见的用法。假设我们有一个Student类:
public class Student { private String name; private int age; // 构造方法、getter/setter省略... }2.1 对象存储的正确姿势
存储自定义对象时要注意:
- 重写equals()和hashCode()方法
- 考虑实现Comparable接口以便排序
- 对于要频繁修改的对象,注意线程安全问题
添加对象示例:
ArrayList<Student> studentList = new ArrayList<>(); studentList.add(new Student("张三", 20)); studentList.add(new Student("李四", 22));2.2 对象修改的两种方式
修改ArrayList中的对象有两种典型场景:
- 直接通过索引获取并修改:
Student s = studentList.get(0); s.setName("王五");- 使用set()方法替换整个对象:
studentList.set(0, new Student("王五", 20));第一种方式更高效,但要注意如果对象被多个地方引用,可能会产生副作用。第二种方式更安全,但会产生更多垃圾对象。
3. 删除操作的陷阱与解决方案
ArrayList的删除操作可能是最容易出问题的部分,常见的有以下几种方式:
3.1 按索引删除
studentList.remove(0); // 删除第一个元素3.2 按对象删除
Student toRemove = new Student("李四", 22); studentList.remove(toRemove); // 依赖equals()方法3.3 遍历时删除的坑
最常见的错误是在for循环中直接删除:
for(int i=0; i<studentList.size(); i++) { if(/*某些条件*/) { studentList.remove(i); // 这会导致后续元素索引变化! i--; // 需要手动调整索引 } }正确做法是使用迭代器:
Iterator<Student> it = studentList.iterator(); while(it.hasNext()) { Student s = it.next(); if(/*某些条件*/) { it.remove(); // 安全的删除方式 } }或者使用Java8的removeIf:
studentList.removeIf(s -> /*谓词条件*/);4. 遍历的四种方式与性能对比
ArrayList提供了多种遍历方式,各有优缺点:
4.1 传统for循环
for(int i=0; i<studentList.size(); i++) { Student s = studentList.get(i); // 处理s }- 优点:可以通过索引随机访问
- 缺点:对于LinkedList等实现效率低
4.2 增强for循环
for(Student s : studentList) { // 处理s }- 优点:语法简洁
- 缺点:不能修改集合结构
4.3 迭代器方式
Iterator<Student> it = studentList.iterator(); while(it.hasNext()) { Student s = it.next(); // 处理s }- 优点:可以安全删除元素
- 缺点:语法稍显复杂
4.4 Java8 forEach
studentList.forEach(s -> { // 处理s });- 优点:函数式风格,简洁
- 缺点:调试不太方便
性能测试表明,对于ArrayList,传统for循环通常是最快的,但在大多数应用场景中差异不大。选择哪种方式更多取决于具体需求和代码风格。
5. 实战经验与性能优化
在实际项目中使用ArrayList时,我总结了一些经验教训:
初始化容量:如果能预估元素数量,初始化时指定容量可以避免多次扩容。例如:
ArrayList<String> list = new ArrayList<>(1000);批量操作:使用addAll()比多次add()更高效:
list.addAll(Arrays.asList("a", "b", "c"));避免频繁修改:如果需要频繁在中间位置插入/删除,考虑使用LinkedList
线程安全:ArrayList不是线程安全的,多线程环境下应该使用:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());或者考虑CopyOnWriteArrayList
空间回收:对于长期存在且可能变小的ArrayList,适时trimToSize()可以节省内存:
list.trimToSize();元素查找:contains()和indexOf()方法的时间复杂度是O(n),对于大型集合要考虑性能影响
子列表陷阱:subList()返回的视图会反映原列表的修改,使用时要注意:
List<String> sub = list.subList(0, 5); list.add("new"); // 会导致sub列表操作抛出ConcurrentModificationException
6. 常见问题排查
在实际开发中,我遇到过不少ArrayList相关的问题,这里分享几个典型案例:
问题1:ConcurrentModificationException异常
- 场景:在遍历集合时同时修改它
- 解决方案:使用迭代器的remove()方法,或者先收集要删除的元素最后统一删除
问题2:性能突然下降
- 场景:大型ArrayList频繁在头部插入
- 解决方案:改为尾部插入,或使用LinkedList
问题3:内存泄漏
- 场景:长期持有不再使用的大型ArrayList
- 解决方案:及时清空或缩小容量
问题4:元素顺序混乱
- 场景:多线程环境下未经同步直接修改
- 解决方案:使用线程安全集合或手动同步
问题5:contains()返回错误结果
- 场景:自定义对象未正确实现equals()
- 解决方案:确保重写了equals()和hashCode()
7. 与其它集合的对比选择
虽然本文聚焦ArrayList,但合理选择集合类型很重要:
| 集合类型 | 适用场景 | 不适用场景 |
|---|---|---|
| ArrayList | 随机访问多,修改少 | 频繁在中间插入/删除 |
| LinkedList | 频繁插入/删除 | 随机访问多 |
| HashSet | 快速查找,去重 | 需要保持插入顺序 |
| TreeSet | 需要排序 | 内存敏感场景 |
| HashMap | 键值对存储 | 需要有序遍历 |
对于字符串存储,如果不需要重复元素,考虑使用HashSet;如果需要保持插入顺序,LinkedHashSet可能更合适。