1. ArrayList基础操作全解析
ArrayList作为Java集合框架中最常用的动态数组实现,几乎每个Java开发者都绕不开它。记得我刚入行时,第一次用ArrayList存储用户数据就踩了不少坑——比如在遍历时删除元素导致ConcurrentModificationException异常,或者自定义对象没重写equals()方法导致contains()判断失效。今天我们就来彻底搞懂ArrayList对字符串和自定义对象的存储、修改、删除及遍历操作。
ArrayList底层基于Object[]数组实现,当元素数量超过数组容量时会自动扩容1.5倍。与LinkedList相比,它的随机访问效率更高(O(1)复杂度),但在中间位置插入/删除元素时需要移动后续所有元素(O(n)复杂度)。理解这些特性对正确使用API至关重要。
2. 存储字符串元素的标准操作
2.1 基础存储与初始化
创建存储字符串的ArrayList有三种常见方式:
java复制// 方式1:默认初始容量(10)
ArrayList<String> strList = new ArrayList<>();
// 方式2:指定初始容量
ArrayList<String> strList = new ArrayList<>(100);
// 方式3:通过已有集合初始化
List<String> temp = Arrays.asList("A", "B", "C");
ArrayList<String> strList = new ArrayList<>(temp);
提示:预估数据量并设置合理初始容量能减少扩容开销。比如已知要存储1000个元素,直接new ArrayList<>(1000)比默认扩容7次效率更高。
2.2 元素添加与扩容验证
添加元素的方法区别:
java复制strList.add("Java"); // 尾部追加
strList.add(0, "Python"); // 指定索引插入
strList.addAll(Arrays.asList("C++", "Go")); // 批量添加
验证扩容机制的测试代码:
java复制ArrayList<String> demo = new ArrayList<>(2);
System.out.println(demo.size()); // 0
demo.add("A");
System.out.println(demo.size()); // 1
demo.add("B");
System.out.println(demo.size()); // 2
demo.add("C"); // 触发扩容
System.out.println(demo.size()); // 3
3. 存储自定义对象的实践要点
3.1 对象定义与存储
定义Student类:
java复制public class Student {
private int id;
private String name;
// 必须重写equals和hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return id == student.id && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
存储自定义对象:
java复制ArrayList<Student> students = new ArrayList<>();
students.add(new Student(1, "Alice"));
students.add(new Student(2, "Bob"));
3.2 对象比较的陷阱
未重写equals()时会出现的问题:
java复制Student s1 = new Student(1, "Alice");
Student s2 = new Student(1, "Alice");
ArrayList<Student> list = new ArrayList<>();
list.add(s1);
System.out.println(list.contains(s2)); // false(未重写equals时)
System.out.println(s1.equals(s2)); // false
警告:自定义对象作为元素时,必须重写equals()和hashCode()方法,否则remove()、contains()等依赖对象相等性的方法将无法正常工作。
4. 元素修改与删除的API详解
4.1 修改元素的两种方式
直接通过索引修改:
java复制strList.set(1, "JavaScript"); // 将索引1的元素替换
批量修改示例:
java复制// 将所有长度>3的字符串改为大写
for (int i = 0; i < strList.size(); i++) {
String s = strList.get(i);
if (s.length() > 3) {
strList.set(i, s.toUpperCase());
}
}
4.2 删除元素的三种模式
- 按索引删除:
java复制String removed = strList.remove(0); // 删除并返回被删元素
- 按对象值删除:
java复制boolean isRemoved = strList.remove("Java"); // 依赖equals()方法
- 批量删除:
java复制strList.removeAll(Arrays.asList("A", "B"));
strList.removeIf(s -> s.length() < 3); // Java8+ 谓词删除
4.3 删除操作的性能对比
测试不同删除方式的耗时差异:
java复制ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
// 测试从头部删除
long start = System.currentTimeMillis();
while (!list.isEmpty()) {
list.remove(0); // 每次删除都需移动后续所有元素
}
System.out.println("头部删除耗时:" + (System.currentTimeMillis() - start));
// 测试从尾部删除
list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
start = System.currentTimeMillis();
while (!list.isEmpty()) {
list.remove(list.size() - 1); // 无需移动元素
}
System.out.println("尾部删除耗时:" + (System.currentTimeMillis() - start));
输出结果示例:
code复制头部删除耗时:1289ms
尾部删除耗时:8ms
5. 遍历操作的四种方式与陷阱
5.1 基础for循环遍历
java复制for (int i = 0; i < strList.size(); i++) {
System.out.println(strList.get(i));
}
优点:可在遍历时通过索引修改元素
缺点:无法在遍历时安全删除元素(会改变后续索引)
5.2 增强for循环遍历
java复制for (String s : strList) {
System.out.println(s);
}
陷阱:尝试在增强for循环中调用remove()会抛出ConcurrentModificationException
5.3 迭代器遍历(推荐)
安全删除的正确姿势:
java复制Iterator<String> it = strList.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("Java")) {
it.remove(); // 唯一安全的遍历时删除方式
}
}
5.4 Java8+的forEach遍历
java复制strList.forEach(System.out::println);
// 带条件的遍历
strList.forEach(s -> {
if (s.length() > 3) {
System.out.println(s);
}
});
6. 实战中的常见问题与解决方案
6.1 ConcurrentModificationException异常
错误示例:
java复制for (String s : strList) {
if (s.equals("Java")) {
strList.remove(s); // 抛出异常
}
}
解决方案:
- 使用迭代器的remove()方法
- 使用Java8的removeIf()
- 记录要删除的元素,遍历后统一删除
6.2 自定义对象比较失效
问题现象:
java复制ArrayList<Student> list = new ArrayList<>();
list.add(new Student(1, "Alice"));
// 即使id和name相同,contains返回false
System.out.println(list.contains(new Student(1, "Alice")));
根本原因:未重写equals()和hashCode(),默认使用Object的引用比较
6.3 性能优化建议
- 预分配容量:new ArrayList<>(预估大小)
- 批量操作:使用addAll()替代循环add
- 避免中间删除:如需频繁插入/删除,考虑LinkedList
- 遍历选择:
- 只读遍历用增强for循环
- 需要删除时用迭代器
- 需要索引时用普通for循环
7. 完整示例:学生管理系统核心实现
java复制public class StudentManager {
private ArrayList<Student> students = new ArrayList<>();
// 添加学生
public void addStudent(Student s) {
if (!students.contains(s)) { // 依赖equals()
students.add(s);
}
}
// 删除学生
public boolean removeStudent(int id) {
return students.removeIf(s -> s.getId() == id);
}
// 查询学生
public Student findStudent(int id) {
for (Student s : students) {
if (s.getId() == id) {
return s;
}
}
return null;
}
// 更新学生信息
public void updateStudent(Student newData) {
for (int i = 0; i < students.size(); i++) {
Student s = students.get(i);
if (s.equals(newData)) {
students.set(i, newData);
break;
}
}
}
// 打印所有学生
public void printAll() {
students.forEach(System.out::println);
}
}
在实际项目中,处理自定义对象集合时我总结出几个经验法则:1) 所有作为集合元素的类必须实现equals/hashCode;2) 批量操作优先考虑Java8的Stream API;3) 多线程环境考虑使用CopyOnWriteArrayList替代ArrayList。