管中窥豹:由例子切入ArrayList及其Iterator的源码实现

管中窥豹:由例子切入ArrayList及其Iterator的源码实现

文章目录

  • 管中窥豹:由例子切入ArrayList及其Iterator的源码实现
      • 一个例子
      • ArrayList的add(E e)方法
      • ArrayList的无参构造、初始化与扩容
      • ArrayList的内部类Itr、并发修改异常与modCount
      • 改正例子中的错误代码
      • 迭代器的remove()方法的特别之处
      • foreach循环、ArrayList的remove方法
      • 来看看阿里巴巴Java开发规范

一个例子

  假设现在需要写这样一个最简单不过的程序:创建一个ArrayList,将3个元素加入其中,然后用Iterator遍历ArrayList将其输出。初学Java时,我们可能会写出如下错误代码:

public class Test {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        Iterator iterator = arrayList.iterator();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

  试着运行一下,发现报ConcurrentModificationException异常:
在这里插入图片描述
  可能会觉得奇怪,明明没有并发修改,为什么会报这个异常?我们进入源码看看到底发生了什么

ArrayList的add(E e)方法

  我们在错误代码中调用了arrayList.add(1)来向集合中加入元素,来看看add(E e)方法的源码:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第1张图片
  我们发现add(E e)方法中size和elementData都是ArrayList的成员变量:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第2张图片
  size表示集合的大小,即ArrayList中元素的数目,而ArrayList实际上是使用Object[]数组elementData来保存元素的。
  因此add(E e)方法中elementData[size++] = e即是在数组末尾添加元素e,同时使数组长度加1。那么我们不禁想问,如果add元素时当前数组已满,数组elementData是如何扩容的呢,elementData初始的大小又是多少?答案会不会在ensureCapacityInternal(size + 1)中?

ArrayList的无参构造、初始化与扩容

  我们来看看ensureCapacityInternal方法:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第3张图片
  DEFAULTCAPACITY_EMPTY_ELEMENTDATA是啥?我们发现它表示一个空的Object[]数组,并在ArrayList的无参构造时将其赋给elementData:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第4张图片
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第5张图片
  ensureCapacityInternal方法中出现的常量DEFAULT_CAPACITY的值为10。我们可以进一步发现ensureCapacityInternal方法出现在所有的add和addAll方法中,当ArrayList被无参构造初始化时,其elementData为一个空Object[]数组的引用,当用不同的参数调用add或addAll方法,都会调用ensureCapacityInternal方法,给其传递的参数为size + 新加入的元素个数,调用add方法新加入的元素个数就是1,调用addAll方法新加入的元素个数为参数中集合的元素个数。ensureCapacityInternal方法的参数minCapacity表示集合所需的最小容量(当集合元素个数>=10时,最小容量即正好装入所有元素,当集合元素个数<10时,最小容量为10,即最小容量有多余),当ArrayList刚被无参构造初始化,它第一次通过add或addAll方法加入元素时,都会计算一个minCapacity,它最小为10,即使我们初始化后第一次add就加了一个元素,minCapacity的值也为10。
  来看看minCapacity有什么用,在ensureCapacityInternal方法中minCapacity最后作为了ensureExplicitCapacity方法的参数,来看看ensureExplicitCapacity方法:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第6张图片
  逻辑很明显了,如果我们现在需要的集合容量比现有的大,那么调用grow方法,看名字就能猜到这是去扩容了,来看看grow方法:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第7张图片
  扩容方法grow的逻辑是:若集合所需的最小容量minCapacity大于当前集合大小的1.5倍(当前所需要的容量很大),则调用Arrays.copyOf()方法返回大小为minCapacity的新的数组对象(复制了原数组的已有元素),并使elementData成为该新数组的引用。若集合所需的最小容量minCapacity较小,小于等于当前集合大小的1.5倍,则干脆多给它点空间,将数组扩容到原来大小的1.5倍(为什么这样做源码注释里已经写了,一般我们添加元素都是一个个加,因此所需容量和原容量相差不大,直接多给点容量,可以减少反复扩容调用Arrays.copyOf()方法的开销)。若minCapacity非常大,大于Integer.MAX_VALUE - 8,则调用hugeCapacity方法将集合大小调到最大:Integer.MAX_VALUE,即ArrayList大小最大为Integer.MAX_VALUE
  根据以上逻辑,我们可以推断出ArrayList初始化之后,第一次调用add方法加入一个元素后,它内部数组的大小一定为10。

ArrayList的内部类Itr、并发修改异常与modCount

  回到错误代码,首先我们调用了arrayList.iterator(),返回了一个实现了Iterator接口的对象,我们来看看这个方法的源码:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第8张图片
  我们发现,调用arrayList.iterator()方法,实际上是通过无参构造给我们返回了一个Itr对象,ltr对象是个什么鬼?继续往下看:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第9张图片
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第10张图片
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第11张图片
  我们看到Itr是ArrayList的一个内部类,它实现了Iterator接口,即实现了hasNext()、next()、remove()、forEachRemaining(Consumer action)四个方法,同时它还有自己的方法checkForComodification()
  在Itr内定义了3个int类型的成员变量,cursor表示下一个元素的下标,lastRet表示上一个元素的下标,初始为-1,expectedModCount暂时看不出表示什么(实际上它是造成错误代码抛异常的关键所在),我们接着看看hasNext()、next()方法的实现:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第12张图片

  hasNext()方法的实现十分简单,cursor表示下一个元素的下标,那么它不等于elementData的大小size时,即容器内还有可供访问的元素,返回true,否则元素已被遍历完,返回false。
  再来看看next()方法:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第13张图片
  逻辑也十分简单,返回数组中cursor指示下标对应的元素,并将lastRet设为当前返回元素的下标,并将cursor向后移一位,即cursor + 1。不过在next()方法的最开头,有个checkForComodification()方法,它是不是造成例子中错误代码抛异常的关键呢?来看看这个方法:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第14张图片
  逻辑非常简单,只要modCount != expectedModCount,就抛ConcurrentModificationException异常,expectedModCount作为Itr的成员变量,在成员变量初始化时就被赋成了modCount。ArrayList继承了AbstractList抽象类,而modCount是AbstractList的成员变量:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第15张图片
  源码注释中指明了modCount表示list被结构性修改的次数,结构性修改是指改变了list的大小(比如add、remove等操作),或者以其他方式扰乱list,以致迭代器正在进行的迭代可能会产生不正确的结果。如果此字段的值意外更改,则迭代器将抛出ConcurrentModificationException异常。通过modCount提供了fail-fast机制,在迭代期间面对并发修改时会马上抛出异常,而不是进行非确定性行为。
  我们再来回看一下add(E e)方法、ensureCapacityInternal方法以及ensureExplicitCapacity方法:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第16张图片
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第17张图片
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第18张图片
  add(E e)方法会调用ensureCapacityInternal方法,进而调用ensureExplicitCapacity方法,在这个方法中,modCount进行加1,表示进行了一次结构性修改。
  我们在错误代码中调用了三次add方法,modCount从最初的0变到3
  回看一下ArrayList的内部类ltr,它的成员变量expectedModCount在初始化时被我们赋成了0(因为在调用arrayList.iterator()无参构造Itr对象时,此时arrayList并没有进行任何结构性修改,它所继承的成员变量modCount = 0)
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第19张图片
  再来看看next方法中调用的checkForComodification()方法,造成异常的原因已经被我们找到了
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第20张图片
  当我们执行next()方法时,ltr对象内部的expectedModCount = 0,而实际的modCount = 3,因此抛出了并发修改异常。
  也就是说在arrayList.iterator()方法调用创建出ltr对象后,对arrayList的add操作都对该ltr对象不可见,若先创建了ltr对象,后对arrayList进行add操作,在调用next()方法遍历集合元素时会抛出异常

改正例子中的错误代码

  如何修改例子中的错误代码呢?非常简单,在add方法之后再调用arrayList.iterator()方法创建迭代器Iterator即可,这样ltr对象内部的expectedModCount = 3,而实际的modCount = 3,两者相等

public class Test {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        Iterator iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

  不会再抛异常了:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第21张图片

迭代器的remove()方法的特别之处

  通过之前的源码分析,我们知道了在arrayList.iterator()方法调用创建出ltr对象后,对arrayList的add操作都对该ltr对象不可见,若先创建了ltr对象,后对arrayList进行add操作,在调用next()方法遍历集合元素时会抛出异常。那么迭代器的remove()方法也和add(E e)方法一样吗,按照我们的经验,迭代器的remove()方法不会有上述特性,因为我们可以边用迭代器Iterator遍历,边删除元素:

public class Test {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        arrayList.add(4);
        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            int item = iterator.next();
            if (item % 2 == 0) {
                iterator.remove();
            }
        }
        iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

  迭代器的remove()方法删除迭代器最近访问的元素(实际上是lastRet指向的上一个元素),上述代码中,我们删除了偶数,只输出了剩余的奇数。注意我们在输出剩余元素之前先要重新调用arrayList.iterator()方法,原因经过之前的源码分析已经很清楚了,需要重新返回一个ltr对象,重置它的cursor等成员变量,否则我们将得不到输出
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第22张图片
  我们通过源码看看迭代器的remove()方法到底有何特殊之处:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第23张图片
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第24张图片

  调用迭代器的remove()方法,实际上还是调用ArrayList的remove(int index)方法,不过传入的参数index = lastRet,表示上一个元素的下标。在remove(int index)方法中,首先将结构性修改次数modCount加1,然后进入删除的过程,删除的过程十分简单粗暴,若数组大小>1,我们要删除下标为index的元素,那么我们将index之后的所有元素都往前挪一位,这样原下标为index的元素被下标为index + 1的元素所替代,之后的元素也是以此类推,最后会空出原最后一个元素所占的位置,将其置为null,等着被GC。若数组大小为1,则直接将elementData[0]赋为null即可,最后数组大小size - 1,返回被删除的元素。
  删除完元素返回到迭代器的remove()方法后,看看迭代器的remove()方法接下来做了什么,它首先将cursor=lastRet,这样cursor指向所删元素的下一个元素,然后将lastRet=-1,表示刚刚访问的元素已经不存在了(被删除了),接下来就关键了:expectedModCount = modCount,这表明在迭代器的remove()方法中,ltr对象的成员变量expectedModCount会与modCount进行同步,这样我们下次执行next()方法时将不会抛出异常,因此在arrayList.iterator()方法调用创建出ltr对象后,对arrayList执行迭代器的remove()方法都是对该ltr对象可见的,这是迭代器的remove()方法和其add方法之间的重要差别。

  为什么要这样设计的道理也很简单,因为迭代器的remove()删除操作是很大可能和next操作配合完成的,我们一般都是遍历整个集合的元素,将一些特定元素删除,迭代器的remove()方法必须能和next方法配合使用,那么它一定要进行expectedModCountmodCount的同步,否则将抛出异常。而add方法是极小概率会和next方法配合的,如果造成了文章最开头例子中的错误,那就是初学java不熟练的原因了

foreach循环、ArrayList的remove方法

  foreach循环作为语法糖,可以替代迭代器进行集合的遍历,那么它是通过迭代器实现的吗?我们还是来删除ArrayList中的偶数,保留奇数,这次我们用foreach循环实现:

public class Test {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        arrayList.add(4);
        for (Integer item : arrayList) {
            if (item % 2 == 0) {
                arrayList.remove(item);
            }
        }
        for (Integer item2 : arrayList) {
            System.out.println(item2);
        }
    }
}

  执行一下,我们的老朋友ConcurrentModificationException异常又出现了:
在这里插入图片描述
  什么情况?难道和foreach循环语法糖的实现有关吗?我们用XJad工具反编译上述代码,解除语法糖,看看foreach循环是怎么实现的:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第25张图片
  我们发现foreach循环其实就是帮我们封装了迭代器iterator遍历集合的过程,它是用迭代器iterator实现的。那么为什么会抛异常呢,注意当我们在错误代码中使用foreach循环时,我们使用的是ArrayList的remove(Object object)方法,而不是使用迭代器的remove()方法,这两者有什么不同?我们来看看ArrayList的remove(Object object)方法源码:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第26张图片
  逻辑十分简单,若传入参数为null,则遍历数组elementData,找到第一个为null的元素,将其删除,若传入参数不为空,同样也是遍历数组,然后找到第一个需删除的元素。删除元素调用的是fastRemove方法:
管中窥豹:由例子切入ArrayList及其Iterator的源码实现_第27张图片
  和我们之前看到的ArrayList的remove(int index)方法删除原理相同,通过元素统一往前挪一位实现删除,同样将结构性修改次数modCount加1,至于为什么单独分出这个方法,源码注释中已经提到了,这个方法与ArrayList的remove(int index)方法的不同之处在于不进行边界判断以及不返回被删除的元素,所以叫fastRemove。
  我们又看到了什么不得了的东西:modCount++,之前所有错误代码抛出并发修改异常总和这个modCount有关,这次也是这样。我们调用ArrayList的remove(Object object)方法,迭代器ltr中的expectedModCount这次又没有和modCount同步,因此在调用ArrayList的remove(Object object)方法删除元素后,再调用迭代器的next()方法,抛出了异常。

来看看阿里巴巴Java开发规范

  阿里Java开发规范中有一条强制性规定:
在这里插入图片描述
  经过之前的源码分析,这条强制性规范也就不难理解了。在foreach循环中调用ArrayList的remove或add方法进行元素的remove/add操作是一个非常危险的行为,一定会抛出ConcurrentModificationException异常,因为调用ArrayList的remove或add方法,modCount会加1,而ltr中的expectedModCount不变,在之后调用迭代器的next()方法时会抛出异常。这样的代码危险之处在于IDEA不会这么智能,给我们提示说代码会抛异常,如果安装了阿里编码规范插件,插件也判断不出这个错误(代码还做不到这么智能),因此如果在项目中使用错误代码而没有做单元测试的话,就等着背锅吧。
  remove元素时为什么要使用Iterator迭代器,也很容易理解了。因为ltr的remove()方法会将自己的expectedModCountmodCount同步,从而支持之后调用next()方法,不会抛出异常。
  并发操作为啥要给Iterator对象加锁?Iterator迭代器并不是线程安全的,不给它加锁,ltr中的expectedModCount、cursor和ArrayList中的modCount等就都乱套了,一定会抛ConcurrentModificationException异常

你可能感兴趣的:(java基础,java,开发语言,后端,源码)