ArrayList源码分析JDK18

27次阅读

共计 9890 个字符,预计需要花费 25 分钟才能阅读完成。

1. 概述

ArrayList本质上是一个数组, 它内部通过对数组的操作实现了 List 功能, 所以 ArrayList 又被叫做动态数组. 每个 ArrayList 实例都有容量, 会自动扩容. 它可添加 null, 有序可重复, 线程不安全.VectorArrayList内部实现基本是一致的, 除了 Vector 添加了 synchronized 保证其线程安全.

1.1 继承体系

2. 源码解析

2.1 属性

/**
 * 默认初始容量
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 初始容量为 0 时,elementData 指向此对象(空元素对象)
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 调用 ArrayList()构造方法时,elementData 指向此对象(默认容量空元素对象)
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 用于存储集合元素, 非 private 类型能够简化内部类的访问(在编译阶段)
 */
transient Object[] elementData;

/**
 * 包含的元素个数
 */
private int size;

为什么 DEFAULT_CAPACITY 这种变量为什么要声明为 private static final 类型

private是为了把变量的作用范围控制在类中.

static修饰的变量是静态变量.JVM只会为静态变量分配一次内存. 这样无论对象被创建多少次, 此变量始终指向的都是同一内存地址. 达到节省内存, 提升性能的目的.

final修饰的变量在被初始化后, 不可再被指向别的内存地址, 以防变量的地址被篡改.

2.2 构造方法

无参构造方法

public ArrayList() {
        // 无参构造器方法, 将 elementData 指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

指定容量构造方法

public ArrayList(int initialCapacity) {if (initialCapacity > 0) {
        //initialCapacity 大于 0 时, 将 elementData 指向新建的 initialCapacity 大小的数组.
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //initialCapacity 为空时, 将 elementData 指向 EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity:" +
                initialCapacity);
    }
}

指定集合构造方法

public ArrayList(Collection<? extends E> c) {
    // 将 elementData 指向 c 转换后的数组
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {//c.toArray 可能不会返回 Object[], 所以需要手动检查下. 关于这点, 会单独讲解下, 看 3.3
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 如果 elementData.length 为 0, 将 elementData 指向 EMPTY_ELEMENTDATA.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

ArrayList的无参构造方法使用频率是非常高的, 在第一次添加元素时, 会将 capacity 初始化为 10. 在我们知道 ArrayList 中需要存储多少元素时, 使用指定容量构造方法, 可避免扩容带来的运行开销, 提高程序运行效率. 当我们需要复用 Collection 对象时, 使用指定集合构造方法.

2.3 插入

2.3.1 添加元素到列表尾部

add(E e)源码

// 将指定的元素添加到列表的末尾
public boolean add(E e) {
    // 确保内部容量, 如果容量不够则计算出所需的容量值.
    ensureCapacityInternal(size + 1);
    // 将元素插入到数组尾部,size 加一.
    elementData[size++] = e;
    return true;
}

add(E e)方法的平均时间复杂度是O(1). 它的流程大体上分为两步:

  1. 保证内部容量可用;
  2. 将元素添加到数组尾部;

第一步就是自动扩容机制, 具体分析参看2.3.3.

第二步则是在确保有可用容量的基础上, 在尾部添加元素, 如下图:

2.3.2 将元素插入到指定位置

add(int index, E element)源码

// 在列表的指定位置上添加指定元素, 在添加之前将在此位置上的元素及其后面的元素向右移一位.
public void add(int index, E element) {
    // 检查索引是否越界
    rangeCheckForAdd(index);
    // 确保内部容量, 如果容量不够则计算出所需的容量值.
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将 index 及 index 之后的元素向右移一位.
    System.arraycopy(elementData, index, elementData, index + 1,
            size - index);
    // 将新元素插入到 index 处.
    elementData[index] = element;
    // 元素个数加一.
    size++;
}

// 检查索引是否越界
private void rangeCheckForAdd(int index) {if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

add(int index, E element)的平均时间复杂度是O(N). 所以在大容量的集合中不要频繁使用此方法, 否则可能会产生效率问题. 在指定位置添加元素的流程如下图所示:

2.3.3 自动扩容

ensureCapacityInternal(int minCapacity)源码

    // 是否需要扩容
    private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    // 计算容量
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 如果 elementData 指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA, 返回 DEFAULT_CAPACITY 和 minCapacity 中的较大值.
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 否则直接返回 minCapacity
        return minCapacity;
    }

    // 确保明确的容量
    private void ensureExplicitCapacity(int minCapacity) {
        // 修改次数加一
        modCount++;

        // 如果 minCapacity 大于 elementData 数组长度, 那么进行扩容.
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

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

    /**
     * 增加容量确保数组至少能够容纳最小容量参数指定的元素个数.
     *
     * @param minCapacity 所需的最小容量
     */
    private void grow(int minCapacity) {
        // 声明 oldCapacity 为 elementData 长度
        int oldCapacity = elementData.length;
        // 将 newCapacity 声明为 oldCapacity 的 1.5 倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 如果 newCapacity 小于 minCapacity, 将 newCapacity 指向 minCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 如果 newCapacity 超出了最大数组长度, 调用 hugeCapacity()方法计算 newCapacity.
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 根据 newCapacity 生成一个新的数组, 并将 elementData 老数据放入 elementData 中.
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    /**
     *
     * @param minCapacity 最小容量
     * @return 计算后的容量
     */
    private static int hugeCapacity(int minCapacity) {
        // 如果 minCapacity 小于 0, 抛出内存溢出异常.
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        // 比较得出所需容量
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }

自动扩容的流程参看上面代码, 总结几个要点:

  • 如果使用的是 new ArrayList() 构造方法, 在添加第一个元素时, 容量会被设置为 DEFAULT_CAPACITY(10) 大小.
  • 容量会被扩容为之前的 1.5 倍
  • 如果扩容后的容量大于 MAX_ARRAY_SIZE, 那么将Integer.MAX_VALUE 作为容量.

2.4 删除

remove(int index)

    // 移除指定索引位置元素
    public E remove(int index) {
        //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);
        //size 减一, 并将 elementData 数组最后一个元素指向 null, 让 GC 进行操作
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

    @SuppressWarnings("unchecked")
    E elementData(int index) {return (E) elementData[index];
    }

remove(Object o)

    // 删除指定元素
    public boolean remove(Object o) {
        // 如果对象为 null, 删除数组中的第一个为 null 的元素. 没有 null 元素的话则不会变化
        if (o == null) {for (int index = 0; index < size; index++)
                if (elementData[index] == null) {fastRemove(index);
                    return true;
                }
        } else {
            // 删除数组中的第一个为 o 的元素, 没有则不操作.
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    // 省略了越界检查, 而且不会返回被删除的值, 反映了 JDK 将性能优化到极致.
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

删除操作的平均时间复杂度是O(N), 其主要步骤是:

  1. 将索引后面的元素向左移一位;
  2. 数组的最后一个元素赋值为null;
  3. size减一;

具体步骤如下图所示:

2.5 遍历

ArrayList实现了 RandomAccess 接口.RandomAccess是标识接口, 标识实现类能够快速随机访问存储元素. 因为 ArrayList 的底层是数组, 通过下标 index 访问. 所以在 ArrayList 元素量较大时, 应当使用普通 for 循环, 也就是通过下标进行访问. 而 LinkedList 此层是链表, 不具备快速随机访问的能力, 因此没有实现 RandomAccess 接口, 推荐使用 forEach 遍历 (也就是Iterator 遍历).

测试代码:

@Test
public void test6() {
    // 实现了 RandomAccess 接口,底层是数组的 ArrayList 测试
    List<Long> arrayList = new ArrayList<>(150000000);
    Random random = new Random();
    // 为了更好的展示测试结果, 避免程序运行时间过长,arrayList 和 linkedList 添加元素的个数不同.
    for (int i = 0; i < 100000000; i++) {arrayList.add(random.nextLong());
    }
    System.out.println("======ArrayList======");
    traverseByLoop(arrayList);
    traverseByIterator(arrayList);

    System.out.println("======LinkedList======");
    // 没有实现 RandomAccess 接口, 底层是链表的 LinkedList 测试
    LinkedList<Long> linkedList = new LinkedList<>();
    for (int i = 0; i < 100000; i++) {linkedList.add(random.nextLong());
    }
    traverseByLoop(linkedList);
    traverseByIterator(linkedList);
}

// 普通 for 循环进行遍历
public void traverseByLoop(List<Long> list) {long startTime = System.currentTimeMillis();
    for (int i = 0; i < list.size(); i++) {list.get(i);
    }
    long endTime = System.currentTimeMillis();
    System.out.println("RandomAccess 遍历用时:" + (endTime - startTime) + "ms");
}

//Iterator 遍历
public void traverseByIterator(List<Long> list) {long startTime = System.currentTimeMillis();
    Iterator<Long> iterator = list.iterator();
    while (iterator.hasNext()) {iterator.next();
    }
    long endTime = System.currentTimeMillis();
    System.out.println("Iterator 遍历用时:" + (endTime - startTime) + "ms");
}

运行 test6() 方法, 控制台输出:

======ArrayList======
RandomAccess 遍历用时:4ms
Iterator 遍历用时:10ms
======LinkedList======
RandomAccess 遍历用时:5572ms
Iterator 遍历用时:3ms

3. 其它细节

3.1fail-fast机制

Iterator 被创建后, 如果 List 对象不是调用 iteratorremove()add(Object obj) 方法更改内部结构.iterator就会抛出 ConcurrentModificationException. 以此避免在迭代过程中List 对象不可知的变化. 这个机制只能用来侦测异常的操作, 并不能作为并发操作的保障. 在 JDK1.5 新增的 forEach 循环, 其本质就是用迭代器遍历. 下面用 forEach 语法来对 fail-fast 机制进行测试.

@Test
public void test1() {List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
    for (Integer num : list) {if (1 == num) {list.remove(num);
        }
    }
    System.out.println(list);
}

由于在遍历过程中调用了 Listremove()方法, 导致程序检测到非法修改, 抛出异常.

3.2fail-fast失效

forEach 中删除 List 的倒数第二个元素,fail-fast不会生效.

@Test
public void test2() {List<Integer> list = new ArrayList<>(Arrays.asList(1, 2));

    for (Integer num : list) {if (1 == num) {list.remove(num);
        }
    }
    System.out.println(list);
}

forEachjava 的语法糖, 为了搞清楚为啥出现上面的问题, 我们将上面的代码转换为 Iterator 遍历.

@Test
public void test3() {List<Integer> list = new ArrayList<>(Arrays.asList(1, 2));

    Iterator<Integer> iterator = list.iterator();

    while (iterator.hasNext()) {Integer next = iterator.next();
        if (1 == next) {list.remove(next);
        }
    }

    System.out.println(list);
}

首先看看 list.iterator() 的源码, 看看这个 Iterator 是啥?

public Iterator<E> iterator() {return new Itr();
}

Itr类是 ArrayList 的内部类, 咱来看看源码.

private class Itr implements Iterator<E> {
    // 下一个要返回元素的索引, 默认为 0.
    int cursor;
    // 之前返回元素的索引, 默认为 -1.
    int lastRet = -1;
    // 保存创建时 modCount 的值
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {return cursor != size;}

    @SuppressWarnings("unchecked")
    public E next() {
        //fail-fast 检测
        checkForComodification();
        // 将 i 值声明为 cursor
        int i = cursor;
        //index 越界检查
        if (i >= size)
            throw new NoSuchElementException();

        Object[] elementData = ArrayList.this.elementData;
        // 如果 i 值大于等于当前数组的长度,fail-fast
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        // 光标加一
        cursor = i + 1;
        // 对 lastRet 赋值并返回值.
        return (E) elementData[lastRet = i];
    }

    // 每次操作时比较 expectedModCount 和 modCount 的值, 若不一致, 抛出 ConcurrentModificationException
    final void checkForComodification() {if (modCount != expectedModCount)
            throw new ConcurrentModificationException();}
}

while (iterator.hasNext()) 开始分析, 第一次进入 hasNext() 方法.cursor为 0,size为 2, 返回 true, 进入循环体. 然后进入next() 方法, 正常执行,cursor变为 1 ,lastRet变为 0. 然后开始执行 ArrayListremove()方法,modCount变为 1,size变为 1 . 这时候再进入第二次循环, 执行 hasNext() 方法,cursor是 1 ,size也是 1 , 返回 false, 退出循环体. 因此fail-fast 失效. 所以不要在 forEach 循环中使用非 Iterator 的方法进行增删操作,fail-fast也不能完全避免数据被更改的风险, 从源头规避风险是首选.

3.3c.toArray()返回非Object[]

ArrayList 的集合构造器源码中有 c.toArray might (incorrectly) not return Object[] 这句注释. 就上网查了下这个问题. 我们先来看下代码:

@Test
public void test5() {
    // 获取并输出 Object 数组类型
    Object[] objArr = new Object[0];
    //class [Ljava.lang.Object;
    System.out.println(objArr.getClass());

    // 通过 Arrays.asList()方法构建 List 对象. 该对象的 class 不是 Object[]类型.
    List<Integer> list1 = Arrays.asList(1, 2, 3);
    //class [Ljava.lang.Integer;
    System.out.println(list1.toArray().getClass());

    // 通过 new ArrayList 构造器创建 List 对象
    ArrayList<Integer> list2 = new ArrayList<>(Arrays.asList(4, 5, 6));
    //class [Ljava.lang.Object;
    System.out.println(list2.toArray().getClass());
}

控制台输出:

class [Ljava.lang.Object;
class [Ljava.lang.Integer;
class [Ljava.lang.Object;

可以看到通过 Arrays.asList(1, 2, 3) 创建的对象 class 不是 Object[] 类型. 至于具体原因不再分析, 感兴趣的朋友研究下哈.

3.4elementData为啥定义为transient

ArrayList自己根据 size 序列化真实的元素, 而不是根据数组的长度序列化元素, 减少了空间占用.

4. 参考

田小波 -ArrayList 源码分析

彤哥读源码 - 死磕 java 集合之 ArrayList 源码分析

IT 从业者说 -RandomAccess 这个空架子有何用?

正文完
 0