深入Java集合框架:ArrayList源码解析(JDK 8)

前言

最近在系统地复习 Java 基础,发现自己对集合框架的掌握一直停留在面试八股阶段,背得出ArrayList基于数组、查询快增删慢,但从来没真正点开过它的源码看一看。

于是诞生了这一篇帖子,跟着JDK 8的源码一步步搞清楚它底层到底是怎么玩的。这篇帖子就是我的学习记录,既是给自己留个备忘,也希望能为同样在啃源码的小伙伴提供一些参考。如果有理解不到位的地方,欢迎大家在评论区指正!

一、ArrayList 继承体系与核心属性

1. 类继承关系

ArrayList位于java.util包下,它的核心继承与实现关系如下:

  • 继承AbstractList:提供了 List 接口的骨架实现。

  • 实现List接口:定义列表的操作规范。

  • 实现RandomAccess接口标记接口 ,表明支持快速随机访问(底层用 for 循环遍历比用迭代器快)。

  • 实现Cloneable接口:支持克隆(浅拷贝)。

  • 实现Serializable接口:支持序列化。

2. 核心成员变量

打开 ArrayList 的源码,我们会看到几个非常关键的属性:

java 复制代码
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;

// 用于空实例的共享空数组(带初始容量0时)
private static final Object[] EMPTY_ELEMENTDATA = {};

// 用于默认大小空实例的共享空数组(无参构造时)
// 和无参构造区分开,以便在第一次添加元素时知道要扩容多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 真正存放元素的数组缓冲区(ArrayList 的底层核心)
transient Object[] elementData;

// 当前列表中实际存放的元素个数(注意:不是数组长度)
private int size;

// 数组能分配的最大大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

二、构造方法详解

ArrayList 提供了三种构造方法:

1. 无参构造(最常用)

java 复制代码
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

JDK 8 中,无参构造并没有立刻初始化一个长度为 10 的数组,而是先指向一个共享的空数组。真正的扩容发生在第一次添加元素时。

2. 指定初始容量

java 复制代码
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. 包含指定集合

java 复制代码
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray() 可能返回的不是 Object[] 类型,需要做防御性拷贝
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

三、动态扩容机制

扩容是 ArrayList 最核心的机制,发生在添加元素时。我们以 add(E e) 方法为入口:

1. add 方法入口

java 复制代码
public boolean add(E e) {
    // 确保内部容量足够,这是扩容的核心判断
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

2. 扩容流程追踪

java 复制代码
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果是无参构造后的第一次添加,取默认容量 10 和 所需最小容量 的较大值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++; // 修改次数+1,为 Fail-Fast 机制服务
    // 如果所需最小容量 > 当前数组长度,执行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

3. grow() 方法(真正的扩容逻辑)

java 复制代码
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 新容量 = 旧容量 + 旧容量的一半(位运算,相当于 1.5 倍扩容)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 如果新容量仍然小于所需最小容量,直接等于最小容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
        
    // 如果新容量超过了最大数组大小限制
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
        
    // 拷贝原数组到新数组(这是一个耗时的 O(n) 操作!)
    elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容总结

  • 扩容倍数 :旧容量的 1.5 倍oldCapacity >> 1)。

  • 触发时机 :当 size + 1 > elementData.length时。

  • 性能损耗 :扩容会触发 Arrays.copyOf 进行全量数据拷贝,因此在能预估数据量时,务必指定初始容量

四、核心方法源码剖析

1. 指定位置插入 add(int index, E element)

java 复制代码
public void add(int index, E element) {
    rangeCheckForAdd(index); // 检查下标越界
    ensureCapacityInternal(size + 1); // 检查扩容
    // 将 index 及其之后的所有元素向右移动一位(O(n) 操作)
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element;
    size++;
}

结论 :在 ArrayList 中间插入元素,需要移动后续所有元素,效率较低。

2. 删除元素 remove(int index)

java 复制代码
public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 将 index 之后的元素向左移动一位
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    // 将最后一个位置置为 null,方便 GC 回收
    elementData[--size] = null; 
    return oldValue;
}

结论 :删除元素同样需要移动数组,且最后一个元素会被显式设为 null,帮助垃圾回收器回收。

3. 获取与修改 get / set

java 复制代码
public E get(int index) {
    rangeCheck(index);
    return elementData(index); // 直接通过数组下标访问,O(1)
}

public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element; // 直接替换,O(1)
    return oldValue;
}

结论 :基于数组索引访问,这也是 ArrayList 查询快的根本原因。

五、线程安全问题

ArrayList线程不安全的。在多线程环境下,可能会出现:

  1. 数据覆盖:多个线程同时写入,导致元素丢失。

  2. 并发修改异常:一个线程遍历,另一个线程修改。

  3. JDK 1.7 及之前 HashMap 的死循环问题(虽然 ArrayList 不会死循环,但数据一致性无法保证)。

解决方案

  • 使用 Collections.synchronizedList(new ArrayList<>())(方法级加锁,性能较差)。

  • 使用 CopyOnWriteArrayList(写时复制,读操作完全无锁,适合读多写少的高并发场景)。

写在最后

这是我个人源码阅读系列的第一篇。虽然只是分析了ArrayList,但感觉自己对Java集合的理解比之前扎实了很多。

源码并不可怕,只要带着问题一步步Debug进去,总能发现很多精妙的设计。

如果这篇笔记对你有帮助,点个赞鼓励一下吧~ 有任何疑问也欢迎在评论区一起讨论交流!

相关推荐
小小啊python1 小时前
IDEA中SpringBoot项目配置热部署
java·ide·intellij-idea·springboot·热部署
Java成神之路-1 小时前
Spring AI Alibaba 实现多轮对话记忆:ChatMemory 与 Redis 持久化实战
java·springaialibaba
JAVA面经实录9172 小时前
网络编程基础(Java Web/分布式前置·完整版)(十一)
java·前端·网络
代码方舟2 小时前
零信任架构实战:基于天远人企关联构建自动化企业尽职调查网关
java·人工智能·架构·自动化
Escalating_xu2 小时前
【Linux】基础 I/O 深度解析:FILE、文件描述符、open/read/write、重定向与缓冲区
java·linux·服务器
城管不管2 小时前
MySQL 慢查询完整排查
java·服务器·jvm·数据库·mysql·spring·面试
ZC跨境爬虫2 小时前
LeetCode 88. 合并两个有序数组(双指针详解 + Java Python 实现)
java·python·算法·leetcode
北风toto2 小时前
研发效能与后端核心技术全景指南:从CI/CD到共性组件实战
java·开发语言·ci/cd
01二进制代码漫游日记2 小时前
C++基础入门速通
java·开发语言·c++