Java ArrayList API 实战指南:从原理到深度定制实现

在 Java 的集合框架中,ArrayList 无疑是我们最常用、最熟悉的工具之一。它就像一个动态升级版的数组,不仅保留了数组访问速度快的优势,还完美解决了数组长度固定的痛点。在这篇文章中,我们将不仅仅停留在“如何使用”的层面,而是会深入探讨 ArrayList 的内部机制、API 细节,甚至我们还会手写代码来实现一个自定义版本的 ArrayList,以此来彻底掌握它的核心原理。准备好跟我一起深入挖掘了吗?

为什么 ArrayList 如此重要?

在开发过程中,你肯定遇到过这样的情况:你不知道运行时究竟需要存储多少个对象。如果使用普通的数组,你必须一开始就定好长度,这就像订桌子一样,如果客人多了,桌子却坐不下了。ArrayList 就像是那种可以随时拼接加长的桌子,它为我们提供了一系列灵活的方法,让我们能够随心所欲地操纵数据的大小和内容。

核心特性概览:

  • 动态扩容:它可以根据需要自动增长。每一个 ArrayList 实例都有一定的容量,这个容量通常大于或等于实际存储元素的大小。
  • 性能考量:这可能是你最关心的部分。像 INLINECODE906e7ca8、INLINECODEbb703a8f、INLINECODE8e82dc52、INLINECODE32fdfd0b 这些操作,都是在常数时间内运行的,速度非常快。但是,add 操作(尤其是扩容时)的运行时间并不一定是常数。
  • 内存管理:如果你不手动指定容量,默认的容量是 10。作为一种可增长的列表,当我们存入第 11 个元素时,ArrayList 会根据需求自动增加其容量(通常是扩容为原来的 1.5 倍),这个过程涉及到数组的拷贝,会有一定的性能开销。

ArrayList 的构造方法与基础

在开始写代码之前,让我们先理清它的入口。ArrayList 主要为我们提供了两种构造方式:

  • ArrayList():这是最常用的方式,创建一个初始容量为 10 的空列表。当然,在较新的 JDK 版本中,这个实现变得更加懒惰,直到第一次添加元素时才真正分配内存。
  • ArrayList(Collection c):这个构造方法非常有用,它允许你将任何其他的集合转换成 ArrayList。比如你有一个 LinkedList 但需要随机访问,你可以直接用这个构造函数转换。

深入代码:ArrayList API 全解

为了让你更直观地理解,我们将通过一个完整的实战示例,覆盖 ArrayList 90% 的日常使用场景。请注意,下面的代码不仅包含了简单的 CRUD 操作,还包含了一些容易出错的高级用法。

// Java Program to Implement ArrayList API
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Spliterator;

public class ArrayListIntegerExample {
    public static void main(String[] args) {
        // 1. 基础构造:创建一个默认容量的 ArrayList
        // 这里的 array 对象初始容量虽然说是10,但在 JDK 8+ 中,实际上是空数组,第一次 add 时才扩容
        ArrayList array = new ArrayList();

        // 2. 集合构造:基于现有集合创建新的 ArrayList
        // 这种复制是浅拷贝,两个列表里的对象指向同一个内存地址(对于 Integer 这种不可变对象没影响,但要注意自定义对象)
        ArrayList arrayWithColl = new ArrayList(array);

        // 3. 基础添加元素
        array.add(100);
        array.add(120);
        array.add(500);
        array.add(220);
        array.add(150);

        // 4. 指定位置插入
        // 这是一个昂贵的操作,因为需要移动 index 之后的所有元素
        // array.add(4, 50); // 注意:在这个位置之前 list 只有5个元素,index 最大为 4,这会抛出异常,我们先往下看

        // 5. 批量添加:addAll
        // 我们先准备一个 List 接口的实现
        List list = new ArrayList(Arrays.asList(90, 20, 40, 10, 15));
        array.addAll(list);

        // 再准备一个 List 插入到指定位置(例如索引 2)
        List listToInsert = new ArrayList(Arrays.asList(60, 25, 12, 16, 45));
        array.addAll(2, listToInsert);

        // 此时 array 的内容是复杂的混合体
        System.out.println("当前列表内容: " + array);

        // 6. 创建引用(陷阱预警)
        // 这里只是把引用赋值给了 array1,它们指向同一个内存对象
        ArrayList array1 = array;

        // 7. 清空列表
        // 这会清空 array 的所有元素,同时 array1 也会变空,因为它们是同一个东西!
        array.clear();
        System.out.println("Array 清空后的大小: " + array.size()); // 输出 0

        // 8. 包含检查
        // 我们重新往 array1 里添加元素来测试 contains
        // 记住,array1 和 array 指向同一个堆内存,所以这里操作的是同一个列表
        array1.add(100);
        array1.add(120);
        array1.add(500);
        array1.add(220);
        array1.add(150);

        // contains 方法内部其实是通过 equals() 方法比较的,时间复杂度是 O(n)
        System.out.println("Contains 120? : " + array1.contains(120)); // true
        System.out.println("Contains 200? : " + array1.contains(200)); // false

        // 9. 确保容量
        // 如果你知道大概要存 10000 个元素,手动设置容量可以避免多次扩容带来的性能损耗
        ArrayList array2 = new ArrayList(10);
        array2.ensureCapacity(100); // 这是一个优化手段

        // 10. 遍历的艺术
        // 方式一:增强 for 循环 (最常用)
        System.out.println("
Elements using enhanced for-loop : ");
        for (Integer i : array1) {
            System.out.print(i + " ");
        }
        System.out.println();

        // 方式二:Iterator (适合在遍历时删除元素)
        System.out.println("Elements using iterator : ");
        Iterator itr = array1.iterator();
        while (itr.hasNext()) {
            System.out.print(itr.next() + " ");
        }
        System.out.println();

        // 方式三:ListIterator (可以双向遍历)
        System.out.println("Elements using list-iterator : ");
        ListIterator listItr = array1.listIterator();
        while (listItr.hasNext()) {
            System.out.print(listItr.next() + " ");
        }
        System.out.println();

        // 11. 访问与查找
        // get(index) 是 ArrayList 的杀手锏,O(1) 时间复杂度
        System.out.println("Element at index 2 is : " + array1.get(2));

        // indexOf() 返回第一次出现的索引,找不到返回 -1
        System.out.println("Element 500 is at index position : " + array1.indexOf(500));

        // 12. 列表状态检查
        System.out.println("Array is empty? " + array1.isEmpty()); // 现在是 false

        // 13. 最后一次出现的位置
        // 如果列表中有重复元素,可以用这个找最后一个
        System.out.println("Last index of the element 100 is : " + array1.lastIndexOf(100));

        // 14. 删除元素
        // remove(int index) 按索引删,remove(Object o) 按对象删
        // 注意:如果你有一个 List,调用 remove(10) 是删索引还是删数字10?
        // Java 编译器会优先匹配 remove(int index)
        System.out.println("Remove element at index 3 : " + array1.remove(3));
        
        // 若想删除数字 10,必须装箱成 Integer
        // array1.remove(Integer.valueOf(10)); 
    }
}

实战中的常见陷阱与最佳实践

通过上面的代码,我们已经了解了基本的 API。但作为专业的开发者,我们需要避开那些常见的坑。

陷阱一:remove 方法的二义性

如果你看上面的代码注释,你会发现 INLINECODEdfbec9a9 有一个著名的问题。当你调用 INLINECODE5c4c3978 时,编译器是把它当成索引(第11个元素)还是数字10?

  • 解决方案:如果你想删除数字对象,请务必使用 array.remove(Integer.valueOf(10)) 来强制装箱,告诉编译器这是一个对象,不是索引。

陷阱二:并发修改异常 (ConcurrentModificationException)

你肯定遇到过这种情况:你在用 INLINECODE321bafc9 循环遍历列表,然后在循环里调用了 INLINECODE1c99bdb0,结果程序瞬间崩了。

  • 原理:ArrayList 的迭代器有一个 modCount 机制。当你直接修改列表结构(添加或删除)而不通过迭代器本身,迭代器就会发现数据被别人动过了,于是抛出异常。
  • 解决方案:在遍历且需要删除时,务必使用 INLINECODE6697cc8a 的 INLINECODE8ea843fc 方法,或者使用 Java 8 引入的 removeIf() 方法(这点我们在下面会详述)。

陷阱三:浅拷贝的陷阱

代码中我们写了 INLINECODEcd18528d。这根本不是拷贝,这只是给同一个对象起了个绰号。INLINECODE53fad23c 清空了,INLINECODEd53a44e1 也就空了。如果你真的需要复制一份完全独立的列表,请使用 INLINECODE4960b71a 或者 oldList.clone()

性能优化:如何让 ArrayList 飞起来?

ArrayList 的性能很大程度上取决于你如何使用它。下面是几条经过实战检验的建议:

  • 预设容量:如果你大概知道要存多少数据(比如从数据库查 1000 条记录),在构造时直接 new ArrayList(1000)。这能避免中间多次扩容带来的数组拷贝开销,提升大约 20%-30% 的性能。
  • 遍历选对方式:随机访问用 INLINECODE7b1edfbb 是最快的。但如果你只是想遍历处理,增强 for 循环代码更简洁,性能也足够好(编译器会优化它)。只有当你需要在遍历中删除元素时,才使用 INLINECODE44831fca。
  • INLINECODEa69d44a4 的妙用:如果你有一个 ArrayList,经过大量操作后不再增长,且很占内存,你可以调用 INLINECODE01af33df。它会将内部数组的容量缩减到刚好等于当前元素数量,释放多余的内存空间。这在内存受限的移动端开发中很有用。

进阶实战:自定义 ArrayList 实现

为了真正理解底层原理,让我们来手写一个简易版的 INLINECODEf510e47b。我们不依赖现有的 API,而是直接操作底层的 INLINECODEd3930446 数组。这将彻底让你明白 INLINECODE6223624b 和 INLINECODE92a8ce20 背后的秘密。

// 自定义 ArrayList 实现
public class MyArrayList {
    private Object[] elementData; // 存储数据的底层数组
    private int size;             // 当前元素个数,不是容量
    private static final int DEFAULT_CAPACITY = 10; // 默认容量

    // 构造方法
    public MyArrayList() {
        this.elementData = new Object[DEFAULT_CAPACITY];
        this.size = 0;
    }

    // 添加元素:核心逻辑
    public boolean add(E e) {
        // 1. 检查是否需要扩容
        ensureCapacityInternal(size + 1);
        // 2. 赋值
        elementData[size++] = e;
        return true;
    }

    // 获取元素
    public E get(int index) {
        // 越界检查
        if (index >= size || index  0) {
            grow(minCapacity); // 真正的扩容逻辑
        }
    }

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容 1.5 倍
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 数组拷贝是耗时操作
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    // 获取当前大小
    public int size() {
        return size;
    }
    
    public static void main(String[] args) {
        MyArrayList myList = new MyArrayList();
        myList.add("Hello");
        myList.add("World");
        System.out.println("Element at index 1: " + myList.get(1));
        System.out.println("Size: " + myList.size());
    }
}

看,这就是魔法发生的地方。当你调用 add 时,系统首先检查“房间”是否住满了。如果满了,它就申请一个更大的房间(通常是原大小的 1.5 倍),然后把所有人“搬家”过去。这就是为什么在确定数据量时预设容量如此重要的原因。

关键要点与后续步骤

今天,我们从基础的 CRUD 操作,一路聊到了并发陷阱和底层的源码实现。让我们快速回顾一下重点:

  • ArrayList 是 List 接口的可调整大小的数组实现,它允许我们在需要时灵活操纵数组的大小。
  • 性能取决于你的用法:随机访问快,中间插入删除慢。
  • 警惕坑点:遍历时删除元素要用 INLINECODEeb7c5f8d 或 INLINECODEcdaa9cf5,注意 remove(1) 是删索引还是删对象。
  • 优化手段:预估大小使用构造函数 new ArrayList(int capacity) 是最简单的性能优化手段。

后续步骤建议:

我建议你接下来去看看 INLINECODE261e05c1 的实现。你会发现,相比于 ArrayList 的“连续内存存储”策略,LinkedList 使用了“链表”策略。对比学习这两者,你会对 Java 集合框架有更深的理解。此外,你也可以尝试去研究 Java 8 中 INLINECODE0031e26e 的 INLINECODE5eb843ad 和 INLINECODEd046c23b 方法,看看并行流是如何在 ArrayList 上工作的。

希望这篇文章能让你对 ArrayList 有一个全新的认识!在你的下一个项目中,试着运用这些优化技巧,你会看到代码性能的显著提升。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/30319.html
点赞
0.00 平均评分 (0% 分数) - 0