Java集合框架 ——— ArrayList 源码分析

本文以 JDK1.8为例,分析 ArrayList 的源码。

主要属性

  1. 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
  1. 用于空实例的空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
  1. 用于默认大小的空实例的共享空数组实例。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  1. 存储数组元素的列表缓冲区,所以ArrayList的底层实现是 Object[] 数组,并且可以扩容
transient Object[] elementData; // non-private to simplify nested class access
  1. 包含的元素数
private int size;
  1. 要分配的数组的最大大小。某些 VM 会在数组中保留一些标头字。尝试分配更大的阵列可能会导致内存不足错误:请求的阵列大小超过 VM 限制
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

构造方法

带参的构造方法,用户自定义集合初始容量

    /**
     * 带参构造方法.
     *
     * @param  initialCapacity  列表初始大小
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    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);
        }
    }

无参构造方法

    /**
     * 无参构造方法
     * 初始为空数组,当添加第一个元素时,数组容量变为10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

带参构造方法。用户传入一个集合,通过这个构造方法改造成 ArrayList

    /**
     * 按照集合的迭代器返回的顺序构造包含指定集合的元素的列表
     *
     * @param c 要将其元素放入此列表中的集合
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        // 判断数组长度
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 数组为空,转为空集合.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

集合常用方法

1. size()

获取列表中的元素数量

    /**
     * 返回此列表中的元素数。
     *
     * @return the number of elements in this list
     */
    public int size() {
        return size;
    }

2. isEmpty()

如果此列表不包含任何元素,则返回 true 。阿里巴巴开发手册建议使用这个方法进行集合判空。

    /**
     * 如果此列表不包含任何元素,则返回 true 。
     *
     * @return <tt>true</tt> if this list contains no elements
     */
    public boolean isEmpty() {
        return size == 0;
    }

3. toArray()

按顺序返回指定集合中的所有元素,并转换为 Object[]

    /**
     * 返回一个数组,该数组按正确的顺序(从第一个元素到最后一个元素)包含此列表中的所有元素。
返回的数组将是“安全的”,因为此列表不会维护对它的引用。(换句话说,此方法必须分配一个新数组)。因此,调用方可以自由修改返回的数组。
     *
     * 此方法充当基于数组和基于集合的 API 之间的桥梁。
     *
     * @return 以正确顺序包含此列表中所有元素的数组
     */
    public Object[] toArray() {
        // 调用工具类拷贝方法
        return Arrays.copyOf(elementData, size);
    }

    /**
     * 返回一个数组,其中包含此列表中所有元素的正确顺序(从第一个到最后一个元素);返回数组的运行时类型是指定数组的运行时类型。如果列表适合指定的数组,则在其中返回该列表。否则,将分配一个具有指定数组的运行时类型和此列表大小的新数组。
     *
     * 如果列表适合指定的数组,并留出空间(即,数组的元素比列表多),则紧跟在集合末尾之后的数组中的元素设置为 null。(仅当调用方知道列表不包含任何 null 元素时, 这在 确定列表的长度时才很有用
     *
     * @param a – 要存储列表元素的数组,如果它足够大;否则,将为此目的分配相同运行时类型的新数组
     * @return 包含列表元素的数组
     * @throws ArrayStoreException if the runtime type of the specified array
     *         is not a supertype of the runtime type of every element in
     *         this list
     * @throws NullPointerException if the specified array is null
     */
    @SuppressWarnings("unchecked")  // 忽略警告
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // 创建一个指定泛型类的数组
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        // 调用System提供的arraycopy()方法实现数组之间的复制
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

4. get()

返回此列表中指定位置的元素

    /**
     * 返回此列表中指定位置的元素
     *
     * @param  查找的元素索引下标
     * @return 此列表中指定位置的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        // 判断索引是否合法,否则抛出 IndexOutOfBoundsException 越界异常
        rangeCheck(index);
		// 返回数据数组中的指定位置元素
        return elementData(index);
    }

    /**
     * 检查给定的索引是否在范围内。
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    E elementData(int index) {
        return (E) elementData[index];
    }

5. set()

将此列表中指定位置的元素替换为指定的元素。

    /**
     * 将此列表中指定位置的元素替换为指定的元素。
     *
     * @param index – 要替换的元素的索引
     * @param element – 要存储在指定位置的元素
     * @return 原来位于这个位置的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        // 越界检查
        rangeCheck(index);
		// 获取原值
        E oldValue = elementData(index);
        // 重新赋值
        elementData[index] = element;
        // 返回原值
        return oldValue;
    }

6. add()

添加元素

    /**
     * 将指定的元素追加到此列表的末尾。
     *
     * @param e – 要附加到此列表的元素
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        // 扩容机制
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 新增元素
        elementData[size++] = e;
        return true;
    }

    /**
     * 在此列表中的指定位置插入指定的元素。将当前位于该位置的元素(如果有)和任何后续元素向右移动(将一个元素添加到其索引中)
     *
     * @param index – 要插入指定元素的索引
     * @param element – 要插入的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        // 添加元素时使用的索引越界检查
        rangeCheckForAdd(index);
        // 扩容机制
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 将当前索引及后续元素,向后移动,空出下标为 index 的位置
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        // 赋值
        elementData[index] = element;
        size++;
    }

	/**
	* 添加和添加全部使用的 rangeCheck 版本。
	*/
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

7. remove()

删除列表元素

	/**
     * 删除此列表中指定位置的元素。将任何后续元素向左移动(从其索引中减去一个)。
     *
     * @param index – 要删除的元素的索引
     * @return 从列表中删除的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        // 索引越界检查
        rangeCheck(index);
        
		// AbstractList 中定制的属性,这些线程不安全的集合中,实现List的 fail-fast 机制
        modCount++;
        // 获取旧值
        E oldValue = elementData(index);
		// 向左移动被删除元素后面的元素,达到删除效果
        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

        return oldValue;
    }

    /**
     * 从此列表中删除指定元素的第一个匹配项(如果存在)。如果列表不包含该元素,则它保持不变。
     *
     * @param o – 要从此列表中删除的元素(如果存在)
     * @return 如果此列表包含指定的元素,则为 true
     */
    public boolean remove(Object o) {
        // 从左到右遍历找到第一个符合的元素并删除
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

	/** 
	* 跳过边界检查的私有删除方法
	*/ 
    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
    }

8. addAll()

将指定集合中的所有元素插入到此列表中

    /**
     * 将指定集合中的所有元素追加到此列表的末尾,顺序与指定集合的迭代器返回这些元素的顺序相同.
     *
     * @param c –包含要添加到此列表中的元素的集合
     * @return 如果此列表因调用而更改,则为 true
     * @throws NullPointerException if the specified collection is null
     */
    public boolean addAll(Collection<? extends E> c) {
        // 将指定集合转为 Object 数组
        Object[] a = c.toArray();
        // 获取指定集合的长度
        int numNew = a.length;
        // 扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount
        // 拷贝数据
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

    /**
     * 从指定位置开始,将指定集合中的所有元素插入到此列表中。将当前位于该位置的元素(如果有)和任何后续元素向右移动(增加其索引)。新元素将按照指定集合的迭代器返回的顺序显示在列表中
     *
     * @param index – 插入指定集合中的第一个元素的索引
     * @param c –包含要添加到此列表中的元素的集合
     * @return 如果此列表因调用而更改,则为 true
     * @throws IndexOutOfBoundsException {@inheritDoc}
     * @throws NullPointerException if the specified collection is null
     */
    public boolean addAll(int index, Collection<? extends E> c) {
        // 所有下标检查
        rangeCheckForAdd(index);
        // 将指定集合转为 Object 数组
        Object[] a = c.toArray();
        // 获取指定集合的长度
        int numNew = a.length;
        // 扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount

        // 移动指定下标右边的元素
        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
		// 拷贝数据
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

扩容机制分析

观察 ArrayList 的无参构造方法可以看出,初始化的时候是一个长度为0的空数组。

	// 空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
	// 无参构造方法
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

那么到底是什么时候给数组分配容量呢?

我们观察到,在 add() 方法的第一行,调用了一个 ensureCapacityInternal()方法,所以我们开始分析一下 ensureCapacityInternal()方法,发现它调用了另外两个方法 calculateCapacity()ensureExplicitCapacity() ,下面针对这三个方法先进行分析。

	// 扩容机制入口方法
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    // 根据所需最小容量和当前数组计算所需容量
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 如果当前是初始情况下的空数组,返回默认容量和最小容量的较大值。
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 不是初始情况则返回最小容量
        return minCapacity;
    }

	// 判断是否需要扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 如果所需最小容量大于集合数组长度,通过grow方法进行扩容
        // overflow-conscious code
        if (minCapacity - elementData.length > 0) grow(minCapacity);
    }

我们假设通过无参构造 ArrayList 后,进行 add() 操作,所以 ensureCapacityInternal() 方法的入参 minCapacity 值应该为 1。

进入 calculateCapacity() 方法,if 语句判断条件发现这是一个通过无参构造器创建的空集合,因此返回较大值 DEFAULT_CAPACITY = 10 ,接着调用 ensureExplicitCapacity() 方法,if 语句发现需要进行扩容,最后执行 grow() 方法。

    /**
     * 要分配的数组的最大大小。某些 VM 会在数组中保留一些标头字。尝试分配更大的阵列可能会导致内存不足错误:请求的阵列大小超过 VM 限制
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 增加容量以确保它至少可以容纳最小容量参数指定的元素数
     *
     * @param minCapacity – 所需的最小容量
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 位运算,右移一位,相当于除以二,所以 newCapacity 是 oldCapacity 的 1.5 倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 检查新容量是否足够,若还是小于所需最小容量,那么就把所需最小容量当作数组的新容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 如果新容量大于 MAX_ARRAY_SIZE 则调用 hugeCapacity() 方法进行比较,返回新容量
        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);
    }

    // 判断所需最小容量和 MAX_ARRAY_SIZE
    private static int hugeCapacity(int minCapacity) {
        // 如果 minCapacity 小于零,证明在前面已经溢出了,返回异常
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        
        // 对 minCapacity 和 MAX_ARRAY_SIZE 进行比较
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

接着上面的举例分析,执行 grow() 方法的入参是 10 ,因为 oldCapacity = 0 ,所以通过第一个 if 语句过后 newCapacity = 10 ,跳过第二个 if 语句后,将 elementData 扩容,最后 size = 10

overflow-conscious code

在上面扩容机制的源码中,看到了这样的注释,意思是这段代码考虑了数值溢出的情况。JDK 中有很多考虑溢出的代码,其中扩容机制中 grow() 方法是最为经典的。接着我们分析一下这段代码如何避免了数值溢出。

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

在上面的代码中,如果 oldCapacity 足够大,那么 1.5 倍之后的 newCapacity 很容易数值溢出从而变为负数,原因需要了解计算机中二进制的加减原则。

newCapacity 为负数的情况下,我们如果使用传统的 if (newCapacity < minCapacity) 来判断的话,很显然结果是 true ,那么就会进入 if

语句内部,将 newCapacity 变为 minCapacity 。这个情况很显然并不是我们想要发生的。

所以 JDK 在这里使用的是 if (newCapacity - minCapacity < 0) 进行判断,尽然 newCapacity 已经溢出变为负数,那么newCapacity - minCapacity 必然也会发生溢出,这就意味着相减的结果大于 0 ,那么就不会进入 if 语句内部。在第二个 if 判断时,相减后也必然大于 0 ,那么就会进入 if 内部重新获取 newCapacity 。这样 JDK 就解决了数据溢出的问题。

Java 中 int 溢出问题

在 Java 中,int 长度为 4 字节,也就是32位,在计算机(补码)中的最大值二进制表示为 0111 1111 1111 1111 1111 1111 1111 1111 ,(第一位表示为符号位,0代表正,1代表负),那么 int 类型溢出的情况就是在最大值的情况下再加 1,那么就变成了 1000 0000 0000 0000 0000 0000 0000 0000 由于还是补码,那么转为十进制就是 -2147483648

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值