剖析Java集合之ArrayList

新建列表

ArrayList有几个关键属性字段:

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;

其中size字段保存的是列表中有效数据的个数(等于列表的逻辑长度,不一定等于物理长度),elementData数组是真正保存数据的空间。EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA的长度都为0,但是前者是在明确指定初始大小为0的情况下创建的列表所共同指向的数组,而后者是希望扩容到DEFAULT_CAPACITY的列表所共同指向的数组。如果指定非零初始大小值,那么elementData的长度就是该值。如果使用无参构造器,那么elementData就被指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA。可以从下列构造方法源码中获得证据:

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    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);
        }
    }

扩容机制

ArrayList在添加元素之前,都会检测空间是否足够。不管是add还是addAll方法,在空间不够时,都会调用grow方法来扩容,有两个grow方法,一个有参,一个无参:

private Object[] grow() {
    return grow(size + 1);
}

private Object[] grow(int minCapacity) {
    return elementData = Arrays.copyOf(elementData,
                                       newCapacity(minCapacity));
}

第一个方法也是调用的第二个有参的,可以看到第一个增长幅度为1。当然这不是最终的扩容量。关键在于第二个方法内部,调用了newCapacity方法:

private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity <= 0) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return minCapacity;
    }
    return (newCapacity - MAX_ARRAY_SIZE <= 0)
        ? newCapacity
        : hugeCapacity(minCapacity);
}

将目前数组的长度(物理长度)扩展到1.5倍作为新容量,然后和传过来的参数进行比较:

  • 如果新容量仍然小于等于指定大小的话,而且之前采用的无参构造器,如果这是第一次添加,那么返回DEFAULT_CAPACITYminCapacity的最大值,否则返回minCapacity

  • 如果1.5倍的新容量大于指定大小的话,先和MAX_ARRAY_SIZE比较,这个一个非常非常大的正整数,只比最大整型数值小8。如果新容量比这个值小,那么就返回这个值,如果比这个值大,调用hugeCapacity方法进一步处理。

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

    其实这里的处理逻辑也不复杂,如果请求空间的长度确实比MAX_ARRAY_SIZE还大,那么就干脆直接返回Integer.MAX_VALUE,反正离溢出也不远了。

注意, 有两个地方判断了会溢出:

  • 第一个地方是newCapacity内部,newCapacity - minCapacity <= 0minCapacity < 0同时成立,说明newCapacity<0 满足,这是1.5倍后导致的。
  • 第二个地方同样要求minCapacity < 0成立,为什么传入的参数会是一个小于0的数呢?三个add方法都是调用的无参的grow方法,是绝对不会出现这种情况的,刚好,addAll方法内部就传入了minCapacity值,这个传入的值是两个容器的size之和,这就有可能导致溢出。

遍历

ArrayList有两个迭代器内部类,分别是ItrListItr,前者实现了Iterator接口,后者实现了ListIterator(继承了Iterator接口)接口,并继承了前者。其实ListItrItr的基础上,还实现了ListIterator定义的操作,包括向前遍历,因为Itr只有向后遍历。如果要获取普通Iterator变量,只需要通过调用iterator方法就可以,如果要获得ListItr对象,需要调用ListIterator方法,这个方法有两个实现,一个无参一个有参,参数可以指定迭代器的初始下标,否则默认为0。

迭代器也是一种设计模式,对于不同的集合类,有不同的数据结构,对应的遍历操作也不同,迭代器模式可以提供同一的遍历接口,然后再由具体的类实现,不仅保护了集合内部的结构,又实现了遍历操作的统一。

但是使用迭代器时,有些必须注意的地方。比如,同一个容器对象可能同时存在多个迭代器,而这些不同的迭代器又有着不同状态,如果其中某个迭代器发生结构性改变(添加或者删除元素),那么势必会影响到其他迭代器的工作。那么ArrayList是如何解决的呢?还有一点就是常听说在for-each循环里面不要进行remove操作,这又是为什么呢?

这里以Itr为例分析,对于ListLtr原理也是一样,只不过功能多一些而已。

Itr内部有三个属性:

int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

cursorlastRet分别是下一个要返回的对象和上一次返回的对象。lastRet的初始值为-1,表示没返回过任何对象。

其中modCount是在AbstractList里面定义的一个属性(注意理解注释):

/**
 * The number of times this list has been <i>structurally modified</i>.
 * Structural modifications are those that change the size of the
 * list, or otherwise perturb it in such a fashion that iterations in
 * progress may yield incorrect results.
 *
 * <p>This field is used by the iterator and list iterator implementation
 * returned by the {@code iterator} and {@code listIterator} methods.
 * If the value of this field changes unexpectedly, the iterator (or list
 * iterator) will throw a {@code ConcurrentModificationException} in
 * response to the {@code next}, {@code remove}, {@code previous},
 * {@code set} or {@code add} operations.  This provides
 * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
 * the face of concurrent modification during iteration.
 *
 * <p><b>Use of this field by subclasses is optional.</b> If a subclass
 * wishes to provide fail-fast iterators (and list iterators), then it
 * merely has to increment this field in its {@code add(int, E)} and
 * {@code remove(int)} methods (and any other methods that it overrides
 * that result in structural modifications to the list).  A single call to
 * {@code add(int, E)} or {@code remove(int)} must add no more than
 * one to this field, or the iterators (and list iterators) will throw
 * bogus {@code ConcurrentModificationExceptions}.  If an implementation
 * does not wish to provide fail-fast iterators, this field may be
 * ignored.
 */
protected transient int modCount = 0;

注释中的信息很丰富,如果AbstractList的子类需要提供自定义迭代器,该字段可以用来检测并发修改(注意修改的是结构),所有实现应该在每次发生结构性变化的时候就对该字段加1操作,注意不能加太多,否则会导致假的并发修改异常。列表的addsetremove方法以及它们的重写或重载版本,迭代器的nextprevious(如果有)会使用到这个变量。

Itr类的next方法如下:

public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

该方法第一句就调用了一个方法–checkForComodification:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

modCount是一个ArrayList对象所拥有的属性,expectedModCount是一个Iterator对象的属性,而一个数组列表对象又可以拥有多个迭代器,如果多个迭代器并发迭代列表,而此时会影响到其他的迭代器的迭代过程。这称作为快速失败实现,但是千万要注意,这种方案并不能真正做到线程安全。比如在checkForComodification验证通过后,当前线程可能被暂停允许,由其他线程继续运行可能产生结构性变化,从而导致错误结果。所以请不要在多线程环境下使用ArrayList

假如就在单线程环境下,为什么不要在for-each中进行remove操作呢?

public static void main(String[] args) {
    var list = new ArrayList<>(Arrays.asList(1,2,3,4,5));
    for(Integer i : list) {
        list.remove(i);
    }
}

这是一段简单的测试代码,不出所料,果然发生了异常,反编译一下代码:

public static void main(String args[])
{
    ArrayList list = new ArrayList(Arrays.asList(new Integer[] {
        Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4), 					Integer.valueOf(5)
    }));
    Integer i;
    for(Iterator iterator = list.iterator(); iterator.hasNext(); list.remove(i))
        i = (Integer)iterator.next();
}

可以看到代码中的for-each转为了普通的for循环,但是靠迭代器来控制的。

被调用的remove方法的代码为:

public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    found: {
        if (o == null) {
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        return false;
    }
    fastRemove(es, i);
    return true;
}

又调用了fastRemove方法,代码如下:

private void fastRemove(Object[] es, int i) {
    modCount++;
    final int newSize;
    if ((newSize = size - 1) > i)
        System.arraycopy(es, i + 1, es, i, newSize - i);
    es[size = newSize] = null;
}

代码进入for循环,先调用了next方法,此时迭代器内部的数据的值如下:

cursor=1;
lastRet=0;
expectedModCount=0

但是紧接在着下面调用remove方法,直接调用的是ArrayList对象的,而不是迭代器对象的remove方法,然后又调用了fastRemove方法,这个方法第一句就修改了modCount的值,然后修改完成就返回了。当下一轮再执行next方法的时候,调用checkForComodification方法,检测modCountexpectedModCount是否相等,明显不等,因为modCount的值为1,而expectedModCount的值为0。

上面的分析告一段落,但是remove方法没有这么简单,看下面的例子:

Iterator<Integer>iterator = list.iterator();
while(iterator.hasNext()) {
    iterator.next();
    iterator.remove();
}

这一个例子就能成功地运行完毕,同样使用迭代器,同样也是先调用next方法,再调用remove方法,但是这里和上面不同的是,这里的remove方法是通过迭代器调用的,而上面是调用的列表对象的remove方法。下面看一下Itr内部的remove方法源码:

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

发现内部调用了ArrayList实现的remove方法,而且和上面那个例子中调用的remove方法还不一样:

public E remove(int index) {
    Objects.checkIndex(index, size);
    final Object[] es = elementData;

    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    fastRemove(es, index);

    return oldValue;
}

同样,这里面调用了fastRemove方法,源码和上面一样。但是注意迭代器内部的remove方法的第10行:

expectedModCount = modCount;

这里同步了两个值,这就是差异。迭代器内部的remove方法调用外部的remove方法时,外部的remove方法一个参数是int型的,另一个参数是Object型的,为什么int变量不会自动装箱到Integer型然后调用参数为Object的那个方法呢?关于重载方法匹配优先级,请参考《深入理解Java虚拟机(第三版)》P306。

如果把上面第二个例子修改一下:

Iterator<Integer>iterator = list.iterator();
while(iterator.hasNext()) {
    // iterator.next();
    iterator.remove();
}

这段代码会产生java.lang.IllegalStateException异常,原因在于在迭代器内部的remove方法中,首先检测了lastRet的值,如果小于0,就会抛出该异常。

下面这个例子同样会产生并发修改异常:

Iterator<Integer> iterator = list.iterator();
list.remove(0);
while(iterator.hasNext()) {
    Integer next = iterator.next();
    System.out.println(next);
}

因为在执行第二句的时候,listmodCount改变了,但是迭代器声明是在第一句,也就是在改变之前就已经创建对象了,对象内部的expectedModCount还等于0,所以就造成不一致了。

不能分析完所有的例子,但是原理都是一样的,通过上面几个例子理解原理就行。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值