【Java】ArrayList详解:从使用到源码的全面剖析

作为 Java 集合框架中最常用的类之一,ArrayList 凭借其动态扩容的特性、便捷的增删改查操作,成为日常开发中的 “常客”。但很多开发者可能只停留在 “会用” 的层面,对其底层实现、扩容机制及性能优化却一知半解。今天这篇文章,我将从实战使用、源码解析、性能分析三个维度,带大家彻底吃透 ArrayList!

一、ArrayList 核心特性

ArrayList 是 List 接口的动态数组实现,核心特点如下:

  • 基于数组存储元素,支持随机访问(通过索引直接获取元素)
  • 元素有序且可重复,允许存储 null 值
  • 容量动态扩容,默认初始容量为 10
  • 线程不安全,适合单线程环境(多线程可使用Collections.synchronizedList()CopyOnWriteArrayList

二、ArrayList 实战使用

1. 创建 ArrayList

ArrayList 提供三种常见创建方式,根据场景选择:

// 1. 基本创建方式(指定元素类型)
ArrayList<String> alist1 = new ArrayList();

// 2. 基于List接口(类型推断,JDK7+支持)
List<String> alist2 = new ArrayList<>();

// 3. 指定初始容量(避免频繁扩容,推荐已知元素数量时使用)
List<String> alist3 = new ArrayList<>(15);

2. 增删改查操作

(1)添加元素

  • 末尾添加:add(E e)
  • 指定位置添加:add(int index, E element)
List<String> userList = new ArrayList<>();
// 末尾添加元素
userList.add("张三");
userList.add("李四");
userList.add(null); // 允许添加null

// 指定索引添加(索引0-2,超出范围抛IndexOutOfBoundsException)
userList.add(1, "王五");
System.out.println(userList); // 输出:[张三, 王五, 李四, null]

(2)修改元素

使用set(int index, E element)通过索引修改元素:

// 将索引0的元素改为"张三丰"
userList.set(0, "张三丰");
System.out.println(userList); // 输出:[张三丰, 王五, 李四, null]

(3)删除元素

  • 按索引删除:remove(int index)
  • 按元素删除:remove(Object o)
// 按索引删除(返回被删除元素)
String removed = userList.remove(2);
System.out.println("删除的元素:" + removed); // 输出:李四

// 按元素删除(删除第一个匹配的元素,返回boolean)
boolean isRemoved = userList.remove("王五");
System.out.println("是否删除成功:" + isRemoved); // 输出:true

// 删除null元素
userList.remove(null);

(4)查找元素

  • 索引查找:get(int index)
  • 元素位置查找:indexOf(Object o)lastIndexOf(Object o)
  • 包含判断:contains(Object o)
// 通过索引获取元素
String first = userList.get(0);
System.out.println("第一个元素:" + first); // 输出:张三丰

// 查找元素位置(正序)
int index = userList.indexOf("张三丰");
System.out.println("元素位置:" + index); // 输出:0

// 查找元素位置(倒序)
userList.add("张三丰");
int lastIndex = userList.lastIndexOf("张三丰");
System.out.println("最后一次出现的位置:" + lastIndex); // 输出:1

// 判断是否包含元素
boolean hasElement = userList.contains("张三丰");
System.out.println("是否包含该元素:" + hasElement); // 输出:true

3. 排序与二分查找

若元素已排序,可使用二分查找提升查询效率:

List<Integer> numList = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9));
// 排序(默认升序)
Collections.sort(numList);
System.out.println("排序后:" + numList); // 输出:[1, 1, 3, 4, 5, 9]
// 二分查找(返回元素索引,未找到返回负数)
int searchIndex = Collections.binarySearch(numList, 4);
System.out.println("元素4的位置:" + searchIndex); // 输出:3

三、底层源码解析

1. 核心成员变量

ArrayList 的核心状态由三个变量维护:

// 存储元素的数组(transient避免序列化时存储空元素)
transient Object[] elementData;
// 实际元素个数
private int size;
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;

2. 构造方法

// 无参构造:初始化空数组(首次添加元素时扩容为10)
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 指定初始容量构造
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}

3. 扩容机制(关键!)

当添加元素时,若数组已满(size == elementData.length),会触发扩容。核心逻辑在grow()方法中:

private void grow(int minCapacity) {
    // 原容量
    int oldCapacity = elementData.length;
    // 新容量 = 原容量 + 原容量/2(1.5倍扩容)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 处理初始为空数组的情况(首次扩容为10)
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
    // 处理超大容量(超过Integer.MAX_VALUE - 8时)
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    
    // 复制原数组元素到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容关键结论

  • 默认扩容为原容量的 1.5 倍(通过右移操作oldCapacity >> 1实现,效率更高)
  • 首次添加元素时,空数组会扩容为默认容量 10
  • 扩容时会创建新数组并复制元素,频繁扩容会影响性能(建议提前指定初始容量)

4. 关键方法源码

(1)add(int index, E element)

指定位置添加元素需移动后续元素:

public void add(int index, E element) {
    // 检查索引合法性
    rangeCheckForAdd(index);
    // 确保容量足够
    ensureCapacityInternal(size + 1);
    // 复制数组:index及后续元素向后移动1位(本地方法,效率较高)
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    // 插入新元素
    elementData[index] = element;
    size++;
}

(2)remove(int index)

删除元素需移动后续元素填补空位:

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    // 计算需要移动的元素个数
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    // 置空最后一个元素,帮助GC
    elementData[--size] = null;
    return oldValue;
}

四、时间复杂度分析

ArrayList 的性能关键在于 “索引操作” 与 “非索引操作” 的区别:

操作时间复杂度说明
查询(get)O(1)直接通过索引访问,随机访问效率高
修改(set)O(1)索引定位后直接修改
末尾添加(add)O(1)无元素移动,仅需赋值
中间 / 开头添加O(n)需移动后续 n 个元素
中间 / 开头删除O(n)需移动后续 n 个元素

性能优化建议

  1. 已知元素数量时,创建时指定初始容量,避免扩容
  2. 频繁在中间插入 / 删除元素时,考虑使用 LinkedList(O (1) 插入删除,但查询 O (n))
  3. 遍历 ArrayList 时,使用增强 for 循环或迭代器(效率高于普通 for 循环)

五、常见面试题

  1. ArrayList 与 LinkedList 的区别?
    • 底层实现:ArrayList 基于数组,LinkedList 基于双向链表
    • 访问效率:ArrayList 随机访问 O (1),LinkedListO (n)
    • 插入删除:ArrayList 中间操作 O (n),LinkedListO (1)
    • 内存占用:ArrayList 连续存储,LinkedList 需存储节点指针,内存开销更大
  2. ArrayList 的扩容机制?
    • 默认初始容量 10,扩容为原容量的 1.5 倍
    • 扩容时通过Arrays.copyOf()复制元素到新数组
    • 首次添加元素时,空数组扩容为 10
  3. ArrayList 如何保证线程安全?
    • 原生线程不安全
    • 解决方案:Collections.synchronizedList(list)CopyOnWriteArrayListVector(不推荐,效率低)

总结

ArrayList 是 Java 开发中最基础也最常用的集合类,掌握其使用方法、底层实现及性能特点,能帮助我们在实际开发中做出更合理的选择。希望这篇文章能让你对 ArrayList 有更深入的理解,后续我还会分享更多 Java 集合框架的源码解析,敬请关注!

Tags:

发表回复

Your email address will not be published. Required fields are marked *.

*
*