Java集合(1)—— ArrayList源码分析(jdk1.8)

概论

集合类是java.util下面的类,挑了几个常用的重点学习,大体体系结构如下(思维导图是用随便下载的Mindjet做的):
最右的分支就是接下来要介绍的类
注意: 这个图里的关系并不是严格按照jdk1.8源码画的,比如在源码里其实LinkedHashSet类是HashSet类的子类,但在图中二者属于并列关系。这张图只是为了给各种集合类进行逻辑关系上的整理。

ArrayList

  • 继承的类:AbstractList(在此类的基础上,ArrayList主要是增加了扩容机制)
  • 实现的接口: List、Cloneable、RandomAccess、java.io.Serializable。其中,RandomAccess接口并未给出具体实现,只是单纯的标识这是一个可以进行随机访问的类。也就是说——并不是实现了该接口就可以实现随机访问功能,而是本身就有随机访问功能的类用该接口标识自己的功能。
  • 字段(具体解释见注释)
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 默认容量为10.
     * 注意:不带参数创建一个ArrayList后,容量其实为0。开始添加元素后才变成10.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 静态成员,所有对象实例维护同一个EMPTY_ELEMENTDATA
     * 创建一个长度为0的空ArrayList时,维护的数组就是这个空数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 创建一个具有默认容量(也就是不带参数)的Arraylist所维护的数组就是该数组
     * 虽然跟上面那个都是空数组,但是在第一次扩容时,两者扩大的容量不一样。这个是扩
     * 大到默认容量,另一个则遵从其他的扩容规则,后面会说。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * ArrayList底层用来存储数据的Object数组是elementData
     * elementData的长度即为ArrayList对象的容量
     * 不带参数创建一个ArrayList时elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,长度为0
     * 加入第一个元素后长度会变为DEFAULT_CAPACITY,即为10
     * */
     transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 含有的元素个数(不是整个ArrayList的容量,而是加入的元素个数)
     */
    private int size;
  • 方法
  1. 构造函数有三种:第一种指定初始容量,第二种是不带参数的构造函数,第三种用另一个集合对象实例来初始化。
   /**
     * 给定一个初始容量构造ArrayList
     * 根据指定的初始容量initialCapacity创建一个大小为该数值的数组
     */
    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);
        }
    }

    /**
     * 缺省构造函数:将对象维护的数组上面设为提到的默认容量空数组。
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 用另一个集合初始化Arraylist对象
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
  1. 扩容
    先看一下添加元素的add方法——需要先执行一个ensureCapacityInternal方法,保证容量够用,再添加元素。
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

那么问题来了,这个ensureCapacityInternal方法是如何保证容量够用的呢?在容量不够的时候如何扩容?

首先我们看到这个方法的int型参数minCapacity,较为直观的理解起来就是需要的最小容量,达到这个容量我们才能继续添加元素。比如当前有100个元素,如果我想再添加一个,那至少就得有101个位置,也就是minCapacity=101。

但是,如果根据这个最小容量一个一个的扩充数组大小的话,效率是很低的,因为其实arrayList扩容前后的数组其实是两个数组。扩容时,我们需要先确定新数组的大小,创建新数组,然后再把旧数组的元素复制过来。如果每次添加元素都要进行这样的操作,无疑开销很大。因此,一个比较科学的方法就是在必要的时候(容量不够用了)进行一次扩充,增添n个位置,在这n个位置用完了后再次扩充。而这里的最小容量minCapacity则是用来衡量“容量是否够用”的。如果目前的容量大于等于所需的最小容量,则无需扩容。

除了上面这一段中提及的规则,代码里还考虑了一些特殊的情况。比如,通过下面的代码可以看到,如果一个对象刚刚被创建,还未添加任何元素,并且是由无参数的构造函数构造的(只有在这种情况下elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA),则minCapacity若小于默认容量则变为默认容量,否则不变。也就是从0到1的过程中,容量直接扩为10。而元素个数从10增加到11时,以下这个方法不产生任何影响,minCapacity此时仍然为11。

我个人的理解是,由于ArryList的扩容机制是每次扩大成原来容量的1.5倍,对于很小的数字比如1、2、3、4、5来说,他们的0.5倍是1或2,如果每次扩大1个、2个,就出现了刚才说到的一旦添加元素就要扩容的问题,效率比较低。因此在源码中,出于效率考虑,对于缺省构造函数构造的arrayList,一开始直接扩容为10.

考虑完这样的情况后,调用ensureExplicitCapacity方法。

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

modCount记录该对象被修改的词数,增、删、改时都要加1,查询则不需要。

在ensureExplicitCapacity(int minCapacity)中,若minCapacity(需要的最小容量)大于此时底层数组的长度(这里的length是数组实际容量,字段size是数组中存放的元素个数,size<=elementData.length ), 则调用grow()方法,即扩容。否则什么都不做,也就是ensureCapacityInternal(size + 1)方法结束,执行elementData[size++] = e;, 完成元素的增加。

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

接下来我们看grow()函数:

首先用变量oldCapacity记录原数组容量,然后用变量newCapacity记录旧容量的1.5倍作为增长后的新容量(这里用位运算右移代替了除法,效率更高)。若果扩容1.5倍后仍然达不到所需的最小容量minCapacity,则将新容量直接设置为minCapacity。

这里举一个例子,如果创建一个初始容量为1的arrayList(注意:这里讨论的是指定了初始容量的构造函数,因此不会默认扩容到10),在添加第二个元素时,minCapacity为2,而扩容1.5倍后的newCapacity=1+1*0.5,小于2,这就是刚才所描述的情况,此时直接将新容量newCapacity设置为最小容量minCapacity,也就是2。

此外,当数组当前的容量非常大时,1.5倍的length大于Interger.MAX_VALUE,发生溢出,此时的newCapacity为负数,小于minCapacity,则将其变为minCapacity。 也就是说,当数组的容量足够大时,扩容过程变为每次容量加一。

如果新容量newCapacity大于MAX_ARRAY_SIZE,则将newCapacity赋值为hugeCapacity(minCapacity)。

我们先来看一看MAX_ARRAY_SIZE,这是ArrayList类的一个静态变量,表明最大容量。ArrayList的size、minCapacity等变量均是int类型,说到最大容量,大家的第一反应可能是Integer.MAX_VALUE。事实上,有一些虚拟机会在数组中存放一些信息,也就是说虚拟机能分配给一个arrayList对象的内存是size(E)*Integer.MAX_VALUE, 而如果数组中存放Integer.MAX_VALUE个E类型数据,加上数组中存放的额外信息,总开销大于size(E)*Integer.MAX_VALUE,会发生溢出。因此在源码中,MAX_ARRAY_SIZE设置为略小于 Integer.MAX_VALUE,即Integer.MAX_VALUE - 8。

    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

当minCapacity小于0时,说明数组当前容量已经达到Integer.MAX_VALUE, 加一就溢出变成了负数,此时抛出OOF。
当设置的新容量大于最大容量时,调用hugeCapacity方法,接下来我们来看看这个hugeCapacity方法:如果需要的最小容量大于刚才所说的最大容量,则将新容量设为Integer.MAX_VALUE,否则将新容量设置为最大容量MAX_ARRAY_SIZE。

关于这个方法,stackOverflow上有一个答主解释的比较清楚:https://stackoverflow.com/questions/35582809/java-8-arraylist-hugecapacityint-implementation

在这里用中文解释一下他的意思:当一个数组容量不够了需要扩容,扩容1.5倍后发现新容量大于我们设置的最大容量(Integer.MAX_VALUE-8),但我们所需要的最小容量并没有超过最大容量时,由于数组长度超过最大容量在很多JVM上会引发out of memory,所以我们将新容量设置为最大长度,这样的话至少在下次扩容前,元素的添加都是安全的。而试想当我们的数组长度已经添加到了最大容量(Integer.MAX_VALUE-8)时,我们还想再添加元素。此时minCapacity和newCapacity都为最大容量+1,虽然可能会引发oof,但还是将容量直接扩为Integer.MAX_VALUE,可以添加元素,但不保证不发生内存溢出的问题。

  private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

至此,就确定了新容量newCapacity,创建一个容量为新容量的数组,并将原有元素复制进去。

    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    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);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
  1. 缩容
    每次扩容成1.5倍带来的一个问题是,当数组不再添加元素后,所需容量可能小于实际容量,造成了空间的浪费,此时可以调用trimToSize(),令容量等于元素个数。
public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }
  1. 减少扩容带来的开销
    扩容需要创建新数组+复制原有元素,频繁扩容会到来一定的时间开销。因此,如果预先能估计出所需的容量,可提前用ensureCapacity(c)来将ArrayList对象的容量扩充到c。
 /**
     * Increases the capacity of this <tt>ArrayList</tt> instance, if
     * necessary, to ensure that it can hold at least the number of elements
     * specified by the minimum capacity argument.
     *
     * @param   minCapacity   the desired minimum capacity
     */
    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }
    ```
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值