JDK源码分析——ArrayList

本文详细剖析ArrayList源码,涉及数据结构、默认与最大容量设定、查询快增删慢的原因、初始化容量、扩容机制、线程不安全问题及解决方案,以及Fail-Fast机制的探讨和实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ArrayList源码分析

ArrayList简介

在这里插入图片描述

  • ArrayList 是 Collection 和 List 接口的实现类。底层的数据结构是数组,数组结构特点:增删慢、查询快。线程不安全集合!
  • ArrayList 的特点:
    • 单列集合:对应于 Map 集合来说【双列集合】
    • 有序性:存入元素和取出元素时顺序是一样的
    • 元素可以重复:可以存放两个相同的元素
    • 含带索引的方法:数组与生俱来就含有索引【下标】

ArrayList原理分析

ArrayList 的数据结构源码分析

// 空的对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量空对象数组,通过空的构造参数生成 ArrayList 对象实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// ArrayList 实际存储数据的对象数组
transient Object[] elementData;
// 1. 为什么是 Object 类型?利用面向对象多态特性,可以存储任意引用数据类型
// 2. ArrayList 不能存储基本数据类型

ArrayList 默认容量&最大容量

// 默认初始化容量是 10
private static final int DEFAULT_CAPACITY = 10;
// 最大容量:2^31 - 1 - 8 = 21 4748 3639 【21亿】
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
  • 为什么最大容量要减8?
    • 目的是为了存储 ArrayList 集合的基本信息,比如 List 集合的最大容量
  • 参考资料:https://stackoverflow.com/questions/35756277/why-the-maximum-array-size-of-arraylist-is-integer-max-value-8

在这里插入图片描述

为什么ArrayList查询快、增删慢?

ArrayList 的底层数据结构就是一个 Object 的数组,是一个可变的数组,对于其所有操作都是通过数组来实现的。

  • 数组的特点是查询快、增删慢
  • 查询数据是通过索引定位,查询任意数据耗时均相同——查询效率高
  • 删除数据时,需要将原始数据删除,同时迁移后面的所有数据——删除效率比较低
  • 在指定索引位置插入数据时,需要添加位置后的每个元素后移,再在该索引位置添加元素——插入效率极低
  • 当新增数据发生扩容时,要先将原数组复制到另一个内存空间更大的数组中,然后将新元素添加到扩容后的数组中——添加效率低

ArrayList 初始化容量

  • ArrayList 底层是数组、动态数组:底层是 、Object 对象数组,数组存储的数据类型是 Object,数组名字为 elementData
transient Object[] elementData;
无参创建

创建 ArrayList 之后,ArrayList 容量是多少?
答案:JDK 1.7及以前,初始容量 10;JDK 1.8 及之后,初始化容量是 0

// 初始化的 ArrayList 容量,是0
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 空数组,容量是 0
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

如何初始化动态数组的容量?10个

  • 在执行 add() 方法时进行初始化【懒加载】
  • 判断当前数组的容量是否有存储空间,若没有则初始化容量大小为 10
// 向数组中添加一个元素
public boolean add(E e) {
    // 确保有容量,如果第一次添加,会初始化一个容量为 10 的list
    // size 是当前集合元素的个数,随着添加的元素递增
    ensureCapacityInternal(size + 1);  // Increments modCount!!
	// 添加元素
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    // 两个方法
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 将当前 ArrayList 对象与默认数组进行比较
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 从默认容量 10 与 传入的容量1 中取一个最大值,返回初始化容量 10
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
// 确保不会超过数组的真实容量
private void ensureExplicitCapacity(int minCapacity) {
    // minCapacity是当前计算后的容量10
    modCount++; // 对当前数组操作的计数器

    // overflow-conscious code
    // 最小容量(10) - 当前数组({})的长度 0
    if (minCapacity - elementData.length > 0)
        grow(minCapacity); // 扩容
}
带初始化容量创建
// 创建 ArrayList 集合,并且设置固定容量,initialCapacity:手动设置的初始化容量
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 如果大于0,则创建一个容量为 initialCapacity 的对象数据,并交给 elementData
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) { // 如果设置容量为 0,则设置为默认数组
        this.elementData = EMPTY_ELEMENTDATA;
    } else { // 以上都不是,则抛出非法参数异常
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}
  • 注意:使用 ArrayList 集合,建议如果知道集合大小,最好提前设置,提升集合的使用效率。

ArrayList扩容原理

add 方法先要确保数组的容量足够,防止数组已经填满还往里面添加数据而造成数组越界:

  • 如果数组空间足够,直接将数据添加到数组中;
  • 如果数组空间不够了,则进行扩容,扩容 1.5 倍;
  • 扩容:原始数组 copy 到新数组中,同时向新数组后面加入数据。

使用 new 创建的 ArrayList 的对象是没有容量的,在第一次 add 时,会进行第一次扩容,扩容到初始化值10。0 => 10。

// grow 扩容数组
// minCapacity 当前数组的最小容量,是指存储了多少元素
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length; // 获取当前存储数据数组的长度
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量 = 旧容量 + 扩容容量【旧容量/2】
    // 极端情况过滤:新容量 - 旧容量 < 0【int 值溢出了】,则不扩容
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity; 
    // 新容量比 ArrayList 的最大值还要大,则设置新的容量为 ArrayList 的最大值,以ArrayList最大值为当前容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

总结

  • 扩容的规则并不是翻倍,而是原来容量的 1.5 倍。
  • ArrayList 的数组最大值 Integer.MAX_VALUE - 8,不允许超过这个最大值。
  • 新增元素时,没有严格的数据值的检查,所以可用设置为null。

ArrayList线程安全问题及解决方案

错误复现

  • 我们知道 ArrayList 底层是以数据方式实现,数组大小可变,允许所有元素,包括null。下面举个例子:开启多线程操作 List 集合,向 ArrayList 中添加和删除元素。
/**
 * ArrayList线程安全问题复现
 * @author yangwei
 */
public class ArrayListTest {
    /**
     * 全局线程共享的 ArrayList
     */
    protected static ArrayList<String> arrayList = new ArrayList<>();

    /**
     * 定义线程,线程执行:向集合中添加自己的线程名称
     */
    private static class MyThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                ArrayListTest.arrayList.add(Thread.currentThread().getName());
            } catch (Exception e) {}
        }
    }
    public static void main(String[] args) throws Exception {
        // 创建500个线程数组
        MyThread[] threads = new MyThread[500];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new MyThread();
            threads[i].start();
        }
        // 遍历线程,等待线程执行完毕
        for (MyThread t : threads) t.join();
        // 遍历arrayList集合,打印所有线程名称
        for (String threadName : arrayList) System.out.println("threadName = " + threadName);
    }
}

在这里插入图片描述

  • 运行代码,可能出现如下几种情况:① 打印 null,② 某些线程并未打印,③ 数组下标越界。

导致ArrayList线程不安全的源码分析

  • ArrayList 成员变量:ArrayList的Object的数组存所有元素,size变量保存当前数组中元素个数。
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	transient Object[] elementData; // non-private to simplify nested class access
    private int size;
    // ...
}
  • 出现线程不安全的源码之一:add() 方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
  • add 添加元素,实际做了两大步骤:
    • 判断 elementData 数组容量是否满足需求;
    • 在 elementData 数组对应的位置设置值。
  • 线程不安全的隐患【1】,导致 ③ 数组下标越界

在这里插入图片描述

  • 线程不安全的隐患【1】,导致 ① 打印 null,② 某些线程并未打印

在这里插入图片描述

  • 由此我们可以得出,在多线程情况下操作ArrayList 并不是线性安全的。

解决方案

  • 第一种方案:使用 Vector 集合,Vector 集合是线程安全的【不推荐】
protected static Vector<String> vector = new Vector<>();
  • 第二种方案:使用 Collections.synchronizedList,它会自动将我们的 list 方法进行改变,最后返回给我一个加锁的 List
protected static List<String> list = Collections.synchronizedList(arrayList);
  • 第三种方案:使用 JUC 中的 copyOnWriteArrayList 类进行替换【最佳选择】
protected static CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();

ArrayList的Fail-Fast机制深入理解

什么是 Fail-Fast 机制?

“快速失败”即 Fail-Fast 机制,是Java中的一种错误检测机制。

  • 当多个线程对集合进行结构上的改变,或者在迭代元素时直接调用自身方法,改变集合结构而没有通知迭代器时,有可能会发生 Fail-Fast 机制并抛出异常【ConcurrentModificationException】。注意是有可能,并不是一定会发生。
  • 触发时机:在迭代的过程中,集合的结构发生改变,而此时迭代器并不知情,或者还没来得及反应,便会产生 Fail-Fast 事件。
  • 再次强调,迭代器的快速失败行为无法得到保证!一般来说,不可能对是否出现不同步并发修改,或者自身修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。
  • Java.util包中的所有集合类都是快速失败的,而java.util.concurrent包中的集合类都是安全失败的;快速失败的迭代器抛出ConcurrentModificationException,而安全失败的迭代器从不抛出这个异常。

ArrayList的Fail-Fast问题复现

  • ArrayList的Fast-Fail事件复现及解决方案:
/**
 * 复现Fail-Fast机制
 * 1.产生条件:
 *    多个线程操作同一个集合
 *    同时遍历这个集合,该集合被修改
 * 2. 解决办法:使用JUC并发包中的集合 CopyOnWriteArrayList
 */
public class ArrayListTest {
//    protected static List<String> list = new ArrayList<>();
    protected static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws Exception {
        // 创建线程1,并且向集合添加元素,打印集合中的内容
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 6; i++) {
                list.add(Thread.currentThread().getName() + " " + i);
                printAll();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 6; i++) {
                list.add(Thread.currentThread().getName() + " " + i);
                printAll();
            }
        });
        t1.start();
        t2.start();

    }

    private static void printAll() {
        // 获取当前集合的迭代器
        Iterator<String> it = list.iterator();
        // 通过迭代器遍历集合
        while (it.hasNext()) {
            System.out.println(it.next() + ",");
        }
    }
}

在这里插入图片描述

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值