Java ArrayList案例如何实操

wen java案例 25

Java ArrayList案例实操:从入门到项目实战的完整指南

📚 文章目录导读

  • 第一部分:ArrayList核心机制深度解析
    • ArrayList与数组的本质区别
    • 动态扩容原理与性能陷阱
  • 第二部分:5大高频实操案例(附代码)
    • 案例1:学生成绩管理系统(增删改查)
    • 案例2:购物车商品列表管理
    • 案例3:去重与数据清洗实战
    • 案例4:排序与自定义对象比较
    • 案例5:多线程环境下的安全操作
  • 第三部分:常见问题与性能优化FAQ
  • 第四部分:面试高频问题与避坑指南

第一部分:ArrayList核心机制深度解析

在Java集合框架中,ArrayList是开发中使用频率最高的类之一,许多开发者问:“为什么不用数组而用ArrayList?” 这正是我们首先要明确的。

Java 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。

第四部分:面试高频问题与避坑指南

高频面试题

  1. ArrayList的扩容机制中,为什么扩容1.5倍而不是2倍?
    答:1.5倍是权衡时间与空间的较优解,扩容倍数过大(如2倍)浪费内存;倍数过小(如1.25倍)需频繁扩容,复制成本高,1.5倍计算使用右移oldCapacity >> 1实现,高效且避免浮点运算。

  2. 如何实现ArrayList的线程安全,为什么Vector逐渐被淘汰?
    答:使用Collections.synchronizedList()或CopyOnWriteArrayList,Vector虽线程安全,但所有方法都用synchronized修饰,并发性能极差,且不支持复合操作的原子性(如“检查再添加”仍需外部同步)。

  3. 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个案例,你应该已经掌握了从基础操作到多线程安全、从去重排序到性能优化的完整技能。

下一步实战建议

  1. 尝试用ArrayList实现一个简易的“最近浏览记录”(LRU缓存),需考虑去重和容量限制。
  2. 在实际项目中,遇到需要频繁查找的场景(如根据ID查找用户),用ArrayList还是HashMap?答案显然是HashMap,因为ArrayList的indexOf是O(n)。

掌握ArrayList只是第一步,结合Map、Set和Stream API,才能写出高效、简洁的Java代码,如果你在实际项目中遇到过ArrayList的坑,欢迎在评论区分享你的案例。


本文由原创技术作者“Hedwig”撰写,未经许可禁止转载。

抱歉,评论功能暂时关闭!