0
点赞
收藏
分享

微信扫一扫

java List系列理论总结

老北京的热干面 2022-01-12 阅读 82

目录

List

Arraylist 和 Vector 的区别?

为什么要用Arraylist取代Vector呢?

Arraylist 与 LinkedList 区别?

Arraylist 与 LinkedList 选择哪个

双向链表和双向循环链表

ArrayList 

ArrayList基本性质

RandomAccess 接口

ArrayList 的构造函数

 ArrayList 的扩容机制

add 方法

ensureCapacityInternal() 方法

ensureExplicitCapacity() 方法

grow() 方法

hugeCapacity() 方法

jdk1.6/1.7/1.8之间ArrayList的区别

默认构造方法,ArrayList()

带初始容量的构造方法,public ArrayList(int initialCapacity)

Collection拷贝构造方法,public ArrayList(Collection c)

确保容量方法(扩容方法):ensureCapacity/ensureCapacityInternal

jdk1.6/1.7/1.8创建数组与扩容之间小结

System.arraycopy() 和 Arrays.copyOf()方法

System.arraycopy() 方法

Arrays.copyOf()方法

两者联系和区别

ensureCapacity方法

ArrayList的删除与遍历时删除

forEach 遍历删除

for 下标遍历删除

Iterator 迭代遍历删除


注意:本文参考   docs/java/collection/java集合框架基础知识&面试题总结.md · SnailClimb/JavaGuide - Gitee.com

docs/java/collection/arraylist-source-code.md · SnailClimb/JavaGuide - Gitee.com

List

List 最大的特点就是:有序可重复

Arraylist 和 Vector 的区别?

ArrayList 是 List 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全

Vector 是 List 的古老实现类,底层使用Object[ ] 存储,线程安全的,方法都加了synchronized。

newCapacity = oldCapacity + (oldCapacity >> 1)

ArrayList 的扩容实现,这个算术右移操作是把这个数的二进制往右移动一位,最左边补符号位,但是因为容量没有负数,所以还是补 0.

那右移一位的效果就是除以 2,那么定义的新容量就是原容量的 1.5 倍

newCapacity = oldCapacity + (capacityIncrement>0) ? capacityIncrement:oldCapacity

Vector 通常 capacityIncrement 我们并不定义,所以默认情况下它是扩容两倍

为什么要用Arraylist取代Vector呢?

一个线程访问Vector的话代码要在同步操作上耗费大量的时间。

并且Vector和Hashtable是早先的设计,并不符合collections framework,在结构上比较孤立,所以已经不推荐使用了,至于线程方面,可以通过Collections类中的相关静态方法来获取线程安全的实例

Arraylist 与 LinkedList 区别?

1 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

2 底层数据结构: Arraylist 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)

3 插入和删除是否受元素位置的影响:

ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。

LinkedList 采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响(add(E e)addFirst(E e)addLast(E e)removeFirst() 、 removeLast()),近似 O(1),如果是要在指定位置 i 插入和删除元素的话(add(int index, E element)remove(Object o)) 时间复杂度近似为 O(n) ,因为需要先移动到指定位置再插入。

4 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

5 内存空间占用: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

Arraylist 与 LinkedList 选择哪个

对于这类选择问题:

一是考虑数据结构是否能完成需要的功能

如果都能完成,二是考虑哪种更高效

功能方法ArrayListLinkedList
add(E e)O(1)O(1)
add(int index, E e)O(n)O(n)
remove(int index)O(n)O(n)
remove(E e)O(n)O(n)
set(int index, E e)O(1)O(n)
get(int index)O(1)O(n)

稍微解释几个:

add(E e) 是在尾巴上加元素,虽然 ArrayList 可能会有扩容的情况出现,但是均摊复杂度(amortized time complexity)还是 O(1) 的。

add(int index, E e)是在特定的位置上加元素,LinkedList 需要先找到这个位置,再加上这个元素,虽然单纯的「加」这个动作是 O(1) 的,但是要找到这个位置还是 O(n) 的。(这个有的人就认为是 O(1),和面试官解释清楚就行了,拒绝扛精。

remove(int index)是 remove 这个 index 上的元素,所以

ArrayList 找到这个元素的过程是 O(1),但是 remove 之后,后续元素都要往前移动一位,所以均摊复杂度是 O(n);

LinkedList 也是要先找到这个 index,这个过程是 O(n) 的,所以整体也是 O(n)。

remove(E e)是 remove 见到的第一个这个元素,那么

ArrayList 要先找到这个元素,这个过程是 O(n),然后移除后还要往前移一位,这个更是 O(n),总的还是 O(n);

LinkedList 也是要先找,这个过程是 O(n),然后移走,这个过程是 O(1),总的是 O(n).

那造成时间复杂度的区别的原因是什么呢?

因为 ArrayList 是用数组来实现的。

而数组和链表的最大区别就是数组是可以随机访问的(random access)

这个特点造成了在数组里可以通过下标用 O(1) 的时间拿到任何位置的数,而链表则做不到,只能从头开始逐个遍历。

也就是说在「改查」这两个功能上,因为数组能够随机访问,所以 ArrayList 的效率高。

那「增删」呢?

如果不考虑找到这个元素的时间,

数组因为物理上的连续性,当要增删元素时,在尾部还好,但是其他地方就会导致后续元素都要移动,所以效率较低;而链表则可以轻松的断开和下一个元素的连接,直接插入新元素或者移除旧元素。

但是呢,实际上你不能不考虑找到元素的时间啊。。。而且如果是在尾部操作,数据量大时 ArrayList 会更快的。

所以说:

改查选择 ArrayList;

增删在尾部的选择 ArrayList;

其他情况下,如果时间复杂度一样,推荐选择 ArrayList,因为 overhead 更小,或者说内存使用更有效率。

双向链表和双向循环链表

双向链表: 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。

另外推荐一篇把双向链表讲清楚的文章:https://juejin.cn/post/6844903648154271757

双向链表

双向循环链表: 最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环。

双向循环链表

ArrayList 

ArrayList基本性质

1、底层使用原生数组实现,实现RandomAccess接口,可以随机访问。

随机访问指的是下标索引操作index(i)的时间复杂度是O(1)。size、isEmpty、get、set、iterator和listIterator操作在O(1)内完成,add(e)操作平均在O(1)内完成,即添加n个元素需要O(n)时间(这个是Collection.add,是在尾部添加注意区分下List.add(index, e))。其他操作基本都是O(n)内完成。ArrayList与LinkedList实现相比,O(n)的各个方法的时间复杂度的常数因子更小。

2、因为底层数组 elementData 的容量是不能改变的,所以容量不够时,需要把 elementData 换成一个更大的数组,这个过程叫作扩容。实际的元素的数量size,总是不会超过底层数组的容量 elementData.length,因为扩容需要申请更大的内存,并且需要原来数组的进行一次复制,所以扩容是个耗时的操作。在添加大量元素之前,使用者最好是预估一个大致的数量,手动调用ensureCapacity进行一次扩容操作,避免一个个添加导致频繁扩容影响性能。 

3、ArrayList是未同步的,多线程并发读写时需要外部同步,如果不外部同步,那么可以使用Collections.synchronizedList方法对ArrayList的实例进行一次封装,或者使用Vector。

4、对存储的元素无限制,允许null元素。

5、ArrayList的iterator和listIterator方法返回的迭代器是快速失败的,也就是如果在创建迭代器之后的任何时间被结构性修改,除了通过迭代器自己的remove或add方法之外,迭代器将直接抛出一个ConcurrentModificationException,从而达到快速失败fail-fast的目的,尽量避免不确定的行为。

ArrayList的迭代器的快速失败行为不能被严格保证,并发修改时它会尽量但不100%保证抛出ConcurrentModificationException。因此,依赖于此异常的代码的正确性是没有保障的,迭代器的快速失败行为应该仅用于检测bug。

6、实现clone接口,可以调用其clone方法(虽然clone()是Object中的方法,但是它是protected,使用子类的clone()必须在子类中覆盖此方法)。clone方法复制一个ArrayList,底层数组elementData不共享,但是实际的元素还是共享的。

不过clone是ArrayList中覆盖的,不属于List中的方法,因此常见的声明形式

List<String> strs = new ArrayList<>();

声明出来的变量不能直接使用clone方法,本身也用得极少。

7、实现Serializable接口,可以被序列化。ArrayList"实现"了自定义序列化方法,这么做主要是为了节省空间 。对于占用空间的大头——元素list,仅仅序列化实际size大小的元素,同时不序列化对于新对象无用属性的——来自父类AbstractList的modCount。ArrayList的实际size不会超过底层数组的length,大多数情况下比底层数组length小,使用默认序列化的话,会直接序列化整个底层数组,序列化后字节流会变大,浪费空间。

RandomAccess 接口

public interface RandomAccess {
}

查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。

在 binarySearch() 方法中,它要判断传入的 list 是否 RamdomAccess 的实例,如果是,调用indexedBinarySearch()方法,如果不是,那么调用iteratorBinarySearch()方法

    public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢?我觉得还是和底层数据结构有关!ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!

ArrayList 的构造函数

(JDK8)ArrayList 有三种方式来初始化,构造方法源码如下:

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


    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     *默认构造函数,使用初始容量10构造一个空列表(无参数构造)
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 带初始容量参数的构造函数。(用户自己指定容量)
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {//初始容量大于0
            //创建initialCapacity大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {//初始容量等于0
            //创建空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {//初始容量小于0,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }


   /**
    *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
    *如果指定的集合为null,throws NullPointerException。
    */
     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;
        }
    }

细心的同学一定会发现 :以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。 下面在我们分析 ArrayList 扩容时会讲到这一点内容!

补充:JDK6 new 无参构造的 ArrayList 对象时,直接创建了长度是 10 的 Object[] 数组 elementData 。

 ArrayList 的扩容机制

这里以无参构造函数创建的 ArrayList 为例分析

add 方法

    /**
     * 将指定的元素追加到此列表的末尾。
     */
    public boolean add(E e) {
   //添加元素之前,先调用ensureCapacityInternal方法
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //这里看到ArrayList添加元素的实质就相当于为数组赋值
        elementData[size++] = e;
        return true;
    }

注意 :JDK11 移除了 ensureCapacityInternal() 和 ensureExplicitCapacity() 方法

ensureCapacityInternal() 方法

(JDK7)可以看到 add 方法 首先调用了ensureCapacityInternal(size + 1)

   //得到最小扩容量
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
              // 获取默认的容量和传入参数的较大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

当 要 add 进第 1 个元素时,minCapacity 为 1,在 Math.max()方法比较后,minCapacity 为 10。

此处和后续 JDK8 代码格式化略有不同,核心代码基本一样。

ensureExplicitCapacity() 方法

如果调用 ensureCapacityInternal() 方法就一定会进入(执行)这个方法,下面我们来研究一下这个方法的源码!

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //调用grow方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }

我们来仔细分析一下:

当我们要 add 进第 1 个元素到 ArrayList 时,elementData.length 为 0 (因为还是一个空的 list),因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为 10。此时,minCapacity - elementData.length > 0成立,所以会进入 grow(minCapacity) 方法。

当 add 第 2 个元素时,minCapacity 为 2,此时 e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不会进入 (执行)grow(minCapacity) 方法。

添加第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10。

直到添加第 11 个元素,minCapacity(为 11)比 elementData.length(为 10)要大。进入 grow 方法进行扩容。

grow() 方法

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

    /**
     * ArrayList扩容的核心方法。
     */
    private void grow(int minCapacity) {
        // oldCapacity为旧容量,newCapacity为新容量
        int oldCapacity = elementData.length;
        //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
        //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
       // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
       //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
        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);
    }

int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)! 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数.

">>"(移位运算符):>>1 右移一位相当于除 2,右移 n 位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了 1 位所以相当于 oldCapacity /2。对于大数据的 2 进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源

我们再来通过例子探究一下grow() 方法 :

当 add 第 1 个元素时,oldCapacity 为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX_ARRAY_SIZE 大,则不会进入 hugeCapacity 方法。数组容量为 10,add 方法中 return true,size 增为 1。

当 add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。

以此类推······

这里补充一点比较重要,但是容易被忽视掉的知识点:

java 中的 length属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.

java 中的 length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法.

java 中的 size() 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!

hugeCapacity() 方法

从上面 grow() 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果 minCapacity 大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //对minCapacity和MAX_ARRAY_SIZE进行比较
        //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
        //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
        //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

jdk1.6/1.7/1.8之间ArrayList的区别

默认构造方法,ArrayList()

关于默认构造方法,你可能在别的地方看见过这种话:

无参构造方法(默认构造方法)构造的ArrayList的底层数组elementData大小(容量)默认为10。这里告诉你,这不一定是对的。这句话在1.6版本中是对的(更之前的版本我没看),从1.7开始这句话就有问题了。下面我贴出了三个版本的代码:

jdk1.6的,初始化成10个容量。

// jdk1.6的
/** Constructs an empty list with an initial capacity of ten. */
    public ArrayList() {
    this(10);
}

jdk1.7的,相对1.6版本,引入了一个新的常量EMPTY_ELEMENTDATA,它是一个空数组,因此容量为0。

// jdk1.7的
/** Shared empty array instance used for empty instances. */
private static final Object[] EMPTY_ELEMENTDATA = {};
 
...
 
/** Constructs an empty list with an initial capacity of ten. */
public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}

jdk1.8的,相对1.7版本,又引入了一个新的常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA ,它也是一个空数组,因此容量也为0。至于两个空数组有什么区别,看下面一点说的。

/** Shared empty array instance used for empty instances. */
private static final Object[] EMPTY_ELEMENTDATA = {};
 
/**
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 
...
 
/** Constructs an empty list with an initial capacity of ten. */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

对比下可以看出:jdk1.6的无参构造方法(默认构造方法)构造的ArrayList的底层数组elementData大小(容量)默认为10;从1.7开始,无参构造方法构造的ArrayList的底层数组elementData大小默认为0。

java集合类在jdk1.7版本基本上都有一种改动:懒初始化。懒初始化指的是默认构造方法构造的集合类,占据尽可能少的内存空间(对于ArrayList来说,使用空数组来占据尽量少的空间,不使用null是为了避免null判断),在第一次进行包含有添加语义的操作时,才进行真正的初始化工作。

1.7开始的ArrayList,默认构造方法构造的实例,底层数组是空数组,容量为0,在进行第一次add/addAll等操作时才会真正给底层数组赋非empty的值。如果add/addAll添加的元素小于10,则把elementData数组扩容为10个元素大小,否则使用刚好合适的大小(例如,第一次addAll添加6个,那么扩容为10个,第一次添加大于10个的,比如24个,扩容为24个,刚好合适);1.8版本,默认构造的实例这个行为没有改变,只是用的数组名字变了。

带初始容量的构造方法,public ArrayList(int initialCapacity)

// 1.6
public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    this.elementData = new Object[initialCapacity];
}
 
// 1.7 跟1.6的一样
public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    this.elementData = new Object[initialCapacity];
}
 
// 1.8
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);
    }
}

678三个版本的这个构造方法的实际行为基本一致:如果initialCapacity >= 0,把底层数组elementData赋值为一个大小为initialCapacity的数组,数组的所有元素都是默认值null。1.8稍微进行了一点优化,也是赋值为空数组,但是重用了常量对象。

下面写个简单的例子看一个细微的区别。

// jdk1.6,两个构造的区别很明显
public class TestArrayList {
    public static void main(String[] args) {
        List<String> la = new ArrayList<String>(0); // la.elementData = new Object[0], la.elementData.length = 0
        la.add("111"); // la.elementDate.length = 1,这里一次性扩容了1个,后续再按照通用扩容策略执行扩容操作
 
        List<String> lb = new ArrayList<String>(); // lb.elementData = new Object[10], lb.elementData.length = 10
        lb.add("111"); // lb.elementDate.length = 10,这里没有进行扩容,后续再按照通用扩容策略执行扩容操作
    }
}
 
// jdk1.7,两个构造在第一次进行添加时才看得出区别
public class TestArrayList {
    public static void main(String[] args) {
        List<String> la = new ArrayList<>(0); // la.elementData = new Object[0], la.elementData.length = 0
        la.add("111"); // la.elementDate.length = 1,这里一次性扩容了1个,后续再按照通用扩容策略执行扩容操作
 
        List<String> lb = new ArrayList<>(); // lb.elementData = EMPTY_ELEMENTDATA, lb.elementData.length = 0
        lb.add("111"); // lb.elementDate.length = 10,这里一次性扩容了10个,后续再按照通用扩容策略执行扩容操作
    }
}
 
// jdk1.8,同1.7
public class TestArrayList {
    public static void main(String[] args) {
        List<String> la = new ArrayList<>(0); // la.elementData = EMPTY_ELEMENTDATA, la.elementData.length = 0
        la.add("111"); // la.elementDate.length = 1,这里一次性扩容了1个,后续再按照通用扩容策略执行扩容操作
 
        List<String> lb = new ArrayList<>(); // lb.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA, lb.elementData.length = 0
        lb.add("111"); // lb.elementDate.length = 10,这里一次性扩容了10个,后续再按照通用扩容策略执行扩容操作
    }
}

jdk1.6中new ArrayList<?>()跟new ArrayList<?>(10)的行为是一模一样的,所以跟new ArrayList<?>(0)有很明显区别,这个好理解 。从1.7版本开始,new ArrayList<>()和new ArrayList<>(0),虽然创建后底层内容和容量都一样,但是实际的行为有些细小的差别,那就是这两个在第一次自动扩容时策略不一样。不过这一点影响比较小,基本不影响使用。

1.8中使用两个空数组,正如注释所说的,是在优化(避免创建无用的空数组)的同时,保留其扩容初始策略区别。只用一个空数组就不能再优化的同时,继续保持这个小区别了。

Collection拷贝构造方法,public ArrayList(Collection<? extends E> c)

678三个版本的关系和第2点一样。

// jdk 1.6
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}
 
// jdk 1.7
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}
 
// jdk 1.8
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;
    }
}

关于中间这行注释,可以看下这篇博文:JDK1.6集合框架bug:c.toArray might (incorrectly) not return Object[] (see 6260652)_aty-CSDN博客

此构造方法会有和Array.copyOf/System.arraycopy一样的问题,那就是它只是新建一个elementData数组,数组的内容对应相等,但是不拷贝实际的元素,实际的元素占据的内存空间还是共享的。

确保容量方法(扩容方法):ensureCapacity/ensureCapacityInternal

提前声明下:这两个方法只是确保容量,不一定会扩容,但是为了好理解,下面的文字中所说的"扩容"指的就是这两个方法。

因为原生的数组的容量不能改变,要改变数组的容量,只能是新建一个数组,并把原来数组的内容复制到新数组对应位置上去。数组拷贝使用的是Arrays.copyOf,底层用的是System.arraycopy,比循环赋值效率高。扩容示意图如下。

扩容方法四个位置用到:两个add方法,两个addAll方法,一个反序列化方法,还有就是手动扩容方法ensureCapacity(称之为手动,是因为此方法是public的,可以外部手动调用)。在1.6版本是只有这个手动的方法,内部自动操作也是调用这个方法,1.7开始进行了区分,并且进一步改进了扩容操作。

下面的是jdk1.8的代码,1.7的和1.8的基本相同,唯一的一点区别就是1.8用两个空数组,导致这里的空数组的名字不一样,两个版本的代码可以看作是一样的。

// 手动扩容方法(可以外部调用,不过大多数情况都是List<?> = new ArrayList<>(),这样是调用不到这个方法的)// 这个方法只是简单区别下list是不是通过 new ArrayList() 来创建的,这一点前面说了
// 如果是,则尝试最小扩容10个,不是则尝试扩容指定个,具体也是通过内部扩容方法完成容量确保
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);
    }
}
 
// 下面是内部扩容相关的几个方法的代码
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
 
    ensureExplicitCapacity(minCapacity);
}
 
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
 
    // overflow-conscious code 考虑int型溢出
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
 
private void grow(int minCapacity) {
    // overflow-conscious code 考虑int型溢出
    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);
}
 
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow int型溢出,直接报错
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

下面这是1.6的相关代码,可以对比看下:

public void ensureCapacity(int minCapacity) {
    modCount++;
    int oldCapacity = elementData.length;
    if (minCapacity > oldCapacity) {
        Object oldData[] = elementData;
        int newCapacity = (oldCapacity * 3)/2 + 1;
        if (newCapacity < minCapacity)
            newCapacity = minCapacity;
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
}

区别就是:1.6的方法只是简单进行了逻辑上的操作,没有过多考虑int型溢出的问题,从1.7(上面贴的是1.8的)开始对这个进行了完善。

最主要的问题:就是minCapacity>oldCapacity的负数溢出问题,和从oldCapacity变成newCapacity的过程中,的过早溢出。

先仔细看看1.6的问题,整体来说都是int型溢出的问题。

1、没考虑入参minCapacity可能因为int溢出变为负数。这个方法可以外部手动调用,手动扩容传入负数这个肯定是应该拦截掉的。但是自动扩容会因为int溢出产生负数,碰到这种情况时应该特殊处理,而不是什么都不做,等着后面抛出一个ArrayIndexOutOfBoundsException。

2、就是这句代码不太好,过早溢出

int newCapacity = (oldCapacity * 3)/2 + 1;

虽然上面这行代码和1.7开始的oldCapacity + (oldCapacity >> 1) 看上去一样,都是相当于1.5倍,但实际上是有区别的。

两个区别,第一个小区别是jdk1.6的那种乘除运算的数学结果比后面一个大1比如oldCapacity=10,1.6的算法得到16,1.7开始的算法得到15,这个影响不大;

第二个区别就是两者在数字比较大是运算结果不一样,比如oldCapacity=10^9,这个数和Integer.MAX_VALUE位数一样,用1.6的算法得到的会是错误的-647483647(因为一开始*3溢出,而*1.5不溢出),用1.7的则是正确的1500000000,这时候明明可以1.5倍扩容,但是jdk1.6却用的是按需扩容。

在计算机里面对于int型的两个不同的数a和b,有

a-b>0 不等价于 a>b

因为,a-b>0会被int溢出影响,a>b不会受int溢出影响。无符号的int型中a-b>0是一定成立的;有符号的int型,负数可以看成是正数的溢出,假设a = Integer.MAX_VALUE + 10,b = Integer.MAX_VALUE - 10,很明显a是负数,b是正数,运行一遍a>b得到false,再运行一遍a-b得到的是20,a-b>0得到true。因此对于int型,a>b和a-b>0在if判断中有不同的功能,前者是纯粹比较大小,正数一定大于负数;后者可以判断溢出,正数不一定大于负数。

所以1.7版本对上面两个问题做了修改。

1、从1.7开始将内部扩容和外部可以调用的扩容方法分开了,通过源码(上面贴的是1.8的代码,可以看出是一样的)可以看出:外部调用的手动扩容方法ensureCapacity要多一个判断条件 minCapacity > minExpand,这个判断条件拦截掉负数的minCapacity,这样调用内部扩容ensureCapacityInternal方法时,minCapacity一定是正数

内部扩容方法直接就用minCapacity - elementData.length > 0判断,此条件可以检测出int型溢出,碰到溢出最后会抛出一个OOM错误。jdk1.7用OOM,这比jdk1.6用ArrayIndexOutOfBoundsException更好,因为此时数组大小超出了虚拟机对数组的限制,虚拟机无法处理这种情况了,抛出一个ERROR是合理的。

2、使用这行代码

newCapacity = oldCapacity + (oldCapacity >> 1);

这行不仅仅是是用位运算加快执行速度,上面说了,这种做法才是对的,是真正的1.5倍。不仅仅因为那一个大小的差别,更重要的是避免过早出现int溢出的情况,保证了内部自动扩容会尽量按规定的策略执行。同时整个扩容处理流程中多增加了几处if判断,对各种情况处理更加完善。

还有数组最大长度问题

还有一个问题就是,MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8。这个是1.7开始才有的,注释说这个是因为在一些虚拟机的数组实现中,会有array header这个保留部分,所以数组最大长度并不是实际的Integer.MAX_VALUE。这个在1.8自带的HotSpot上测试(环境Win7 64位,Java HotSpot(TM) 64-Bit Server VM (build 25.111-b14, mixed mode)),准确值应该是Integer.MAX_VALUE - 2,比这个值大就会出现OutOfMemoryError: Requested array size exceeds VM limit。此Error和hugeCapacity中抛出的OOM基本上差不多,这两个跟一般的OOM还是有区别的。抛出这个异常时,一般是程序有问题,此时虚拟机看看数组大小,就知道了它是不能完成这样的内存分配的,跟剩余的内存空间大小没关系。

测试下实际MAX_ARRAY_SIZE(都是64bit的)

jdk1.8 Integer,MAX_VALUE - 2,超过这个值(实际上也只有两个数可选,Integer.MAX_VALUE和Integer.MAX_VALUE - 1)就抛出OutOfMemoryError: Requested array size exceeds VM limit

jdk1.7 Integer.MAX_VALUE - 2

jdk1.6 Integer.MAX_VALUE,使用这个值能够成功创建数组(使用boolean数组)

在带初始容量的构造方法 public ArrayList(int initialCapacity) 中,并没有判断初始容量是否大于MAX_ARRAY_SIZE。个人觉得还是判断下好,不判断可能在扩容时会有点问题。下面给一组变量值大家试下,看下是不是真有问题。

初始数组长度 elementData.length = Integer.MAX_VALUE - 5 = 2147483642;

还要继续添加的长度 expand = Integer.MAX_VALUE - 2 = 2147483645;

最小容量 minCapacity = expand + elementData.length = -9;

ensureExplicitCapacity方法中 minCapacity - elementData.length = 2147483645 > 0,会继续执行grow方法;

grow方法中 newCapacity = elementData.length + (elementData.length >> 1) = -1073741833;

grow方法中的第一个if,newCapacity - minCapacity = -1073741824 < 0,执行第一个if中的 newCapacity = minCapacity,newCapacity = -9;

grow方法中的第二个if,newCapacity - MAX_ARRAY_SIZE = -2147483648 < 0,不执行第二个if中的语句;

执行最后的Arrays.copyOf,因为newCapacity = -9 < 0,会抛出异常NegativeArraySizeException。

grow方法中第二个if,如果newCapacity是负数,只有是-9到-1这几个负数,才会不执行hugeCapacity方法而是直接执行Arrays.copyOf抛出异常。如果构造方法中拦截判断下是否大于MAX_ARRAY_SIZE,一开始的数组长度就限制在Integer.MAX_VALUE - 8,应该是无法通过一个正数的expand,使得minCapacity在[-9,-1]内。严格证明暂时给不出,实际运行中由于内存限制无法演示。

jdk1.6/1.7/1.8创建数组与扩容之间小结

简单总结下,jdk1.7和1.8之间大致相同,jdk1.7版本对比1.6版本,三个改动:

1、懒初始化,因此默认构造方法创建的是空数组,不再是10个大小的数组;

2、扩容相关的完善;

3、一些方法不再直接使用父类的通用实现,改为利用数组特性的更有效率的实现。

System.arraycopy() 和 Arrays.copyOf()方法

阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)toArray() 等方法中都用到了该方法!

System.arraycopy() 方法

源码:

    // 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义
    /**
    *   复制数组
    * @param src 源数组
    * @param srcPos 源数组中的起始位置
    * @param dest 目标数组
    * @param destPos 目标数组中的起始位置
    * @param length 要复制的数组元素的数量
    */
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);
场景:

    /**
     * 在此列表中的指定位置插入指定的元素。
     *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
     *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //arraycopy()方法实现数组自己复制自己
        //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        elementData[index] = element;
        size++;
    }
我们写一个简单的方法测试以下:

public class ArraycopyTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[] a = new int[10];
		a[0] = 0;
		a[1] = 1;
		a[2] = 2;
		a[3] = 3;
		System.arraycopy(a, 2, a, 3, 3);
		a[2]=99;
		for (int i = 0; i < a.length; i++) {
			System.out.print(a[i] + " ");
		}
	}

}
结果:

0 1 99 2 3 0 0 0 0 0

Arrays.copyOf()方法

源码:

    public static int[] copyOf(int[] original, int newLength) {
    	// 申请一个新的数组
        int[] copy = new int[newLength];
	// 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }
场景:

   /**
     以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。
     */
    public Object[] toArray() {
    //elementData:要复制的数组;size:要复制的长度
        return Arrays.copyOf(elementData, size);
    }
个人觉得使用 Arrays.copyOf()方法主要是为了给原有数组扩容,测试代码如下:

public class ArrayscopyOfTest {

	public static void main(String[] args) {
		int[] a = new int[3];
		a[0] = 0;
		a[1] = 1;
		a[2] = 2;
		int[] b = Arrays.copyOf(a, 10);
		System.out.println("b.length"+b.length);
	}
}
结果:

10

两者联系和区别

联系:

看两者源代码可以发现 copyOf()内部实际调用了 System.arraycopy() 方法

区别:

arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组。

ensureCapacity方法

ArrayList 源码中有一个 ensureCapacity 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢?

    /**
    如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
     *
     * @param   minCapacity   所需的最小容量
     */
    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);
        }
    }

最好在 add 大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数

我们通过下面的代码实际测试以下这个方法的效果:

public class EnsureCapacityTest {
	public static void main(String[] args) {
		ArrayList<Object> list = new ArrayList<Object>();
		final int N = 10000000;
		long startTime = System.currentTimeMillis();
		for (int i = 0; i < N; i++) {
			list.add(i);
		}
		long endTime = System.currentTimeMillis();
		System.out.println("使用ensureCapacity方法前:"+(endTime - startTime));

	}
}
运行结果:

使用ensureCapacity方法前:2158
public class EnsureCapacityTest {
    public static void main(String[] args) {
        ArrayList<Object> list = new ArrayList<Object>();
        final int N = 10000000;
        list = new ArrayList<Object>();
        long startTime1 = System.currentTimeMillis();
        list.ensureCapacity(N);
        for (int i = 0; i < N; i++) {
            list.add(i);
        }
        long endTime1 = System.currentTimeMillis();
        System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1));
    }
}
运行结果:

使用ensureCapacity方法后:1773

通过运行结果,我们可以看出向 ArrayList 添加大量元素之前最好先使用ensureCapacity 方法,以减少增量重新分配的次数。

ArrayList的删除与遍历时删除

在聊 ArrayList 的删除删除操作之前,先来说说它的遍历方法。

一个 list 的遍历方法主要有三种:

Iterator 迭代器遍历

遍历下标 for 循环遍历

forEach 遍历

对于这三种遍历方法,产生的删除操作 (remove) 结果也会不一样。

forEach 遍历删除

我们先来看一段代码

public static void main(String[] args) {
    List<String> list1 = new ArrayList<>(3);
    list1.add("name");
    list1.add("age");
    list1.add("phone");
    for (String str : list1){
        if ("age".equals(str)){
            list1.remove(str);
        }
    }
}

这样子进行删除操作是不会出现问题的,然而当我们把删除的判断条件换成 phone 后就会报异常。

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at com.wiceflow.collection.List.ListRemove.main(ListRemove.java:18)

我们知道,forEach 循环其实是走 list 的迭代器进行遍历的,我们先看 ArrayList 内部的 forEach 方法。

在 ArrayList 中有一个内部类 Itr 实现了 Iterator ,还有一个 ListItr 继承了 Itr (这个类初始化的时候会将 ArrayList 对象的 modCount 属性的值赋值给 expectedModCount)。

先看迭代器的 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];
}  
/**
* 在对一个集合对象进行跌代操作的同时,并不限制对集合对象的元素进行操作
* 这些操作包括一些可能引起跌代错误的add()或remove()等危险操作。
* 在AbstractList中,使用了一个简单的机制来规避这些风险。 
* 这就是modCount和expectedModCount的作用所在
*/
final void checkForComodification() {
    if (modCount != expectedModCount)
         throw new ConcurrentModificationException();
}

我们可以看到,list 每次获取下一个对象前都要去检查一下光标是否越界。在 ArrayList 的所有涉及结构变化的方法中都增加 modCount 的值,包括:add()、remove()、addAll()、removeRange() 及 clear() 方法。这些方法每调用一次,modCount 的值就加 1。而变量 expectedModCount 在迭代开始时便会被赋值成 modCount 的值。所以在循环遍历中,改变结构变化的方法,例如 add()、remove() 都会是 modCount 增长 1 ,而 expectedModCount 却不会变化。

注意,以上讲的涉及到结构变化的方法是 ArrayList 的方法,不是其内部类 Itr 的方法。

来看一下 ArrayList 的 remove 方法

public E remove(int index) {
    rangeCheck(index);
    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;
}  

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
}

从上面源码中我们不难发现,ArrayList 中两个 remove() 方法都对 modCount 进行了自增,那么我们在用迭代器迭代的时候,若是删除 末尾 的元素,则会造成 modCount 和 expectedModCount 的不一致导致异常抛出。

为什么对倒数第二个元素进行删除不会报异常,而对其他位置的删除会报异常?

我们来看一下 ArrayList 中的内部类 Itr 。我们在调用迭代器的 Next() 方法之前会先调用 hasNext() 方法。

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

从代码上我们可以看出判断条件是当 cursor != size 的时候,才会进行下一次循环,而 cursor 参数是我们迭代循环的下标,在我们删除倒数第二个元素后,此时 list 的大小减了 1,再进入下一次循环后会出现 cursor == size ,也就是 hasNext() 便会返回 false 终止了循环。实际上 modCount 的数值也增加了 1,只不过循环没发执行到那里,所以异常也就不会被抛出来了。

for 下标遍历删除

从源码上我们可以看出,在利用 for 下标进行遍历的时候,并不会触发 checkForComodification() 方法,所以此时只要要删除的位置比列表大小小时都不会出错。

public E remove(int index) {
    rangeCheck(index);
    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;
} 

在 ArrayList 源码介绍中,作者是推荐使用 for ( int i; i < size; i++) 方式去遍历,而不是 foreach 或者迭代,这个主要是因为 list 接口实现了 RandomAccess 接口。 实现这个接口的集合是随机无序的,所以遍历的时候一般使用上述的 for,记住一点就可以了所有实现了 RandomAccess 接口的集合都是用一般 for 就可以了(可以通过 api 查看那些集合实现了 RandomAccess)。

Iterator 迭代遍历删除

这里我们将的 Iterator 遍历删除调用的方法不是 ArrayList 的 remove 方法,而是其内部类的 remove 方法

我们看源码不难发现,在 Itr 类中,属性 expectedModCount 在调用外部的 remove() 方法后再次被赋值,此时 expectedModCount 是等于 modCount 的。

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    // 这里检查时候还没有进行删除操作
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        // 先进行了 remove 操作后 再重新对 expectedModCount 进行赋值
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

所以在使用 Iterator 进行遍历删除时不会出现 ConcurrentModificationException 异常。

举报

相关推荐

0 条评论