走进 JDK 之 ArrayList(二)

简介: 走进 JDK 之 ArrayList(二)

上篇文章 走进 JDK 之 ArrayList(一) 简单分析了 ArrayList 的源码,文末留下了一个问题,modCount 是干啥用的?下面我们通过一个小例子来引出今天的内容。


public static void main(String[] args){
    List<String> list= new ArrayList<>();
    list.add("java");
    list.add("kotlin");
    list.add("dart");
    for (String s:list){
        if (s.equals("dart"))
            list.remove(s);
    }
}
复制代码


大多数人应该都这么干过,然后得到一个鲜红的 ConcurrentModificationException,具体错误堆栈信息如下:

Exception in thread "main" java.util.ConcurrentModificationException
  at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
  at java.util.ArrayList$Itr.next(ArrayList.java:859)
  at collection.ArrayListTest.main(ArrayListTest.java:15)
复制代码


报错位置是 ArrayList 的内部类 Itr 中的 checkForComodification() 方法。至于如何调用到这个方法的,我们首先得知道上面的代码中发生了什么。看字节码的话太麻烦了又不容易理解,推荐一个反编译神器 jad,javac 编译得到 class 文件之后执行如下命令:

./jad ArrayListTest.class
复制代码


得到 ArrayListTest.jad 文件,直接用文本编辑器打开即可:

public class ArrayListTest {
    public ArrayListTest() { }
    public static void main(String args[]) {
        ArrayList arraylist = new ArrayList();
        arraylist.add("java");
        arraylist.add("kotlin");
        arraylist.add("dart");
        Iterator iterator = arraylist.iterator(); // 1
        do {
            if(!iterator.hasNext()) // 2
                break;
            String s = (String)iterator.next(); // 3
            if(s.equals("dart"))
                arraylist.remove(s);
        } while(true);
    }
}
复制代码


从反编译得到的代码我们可以发现,增强型 for 循环只是一个语法糖而已,编译器帮我们进行了处理,其实是调用了迭代器来进行循环。着重看一下上面标注的三句代码,是整个迭代过程的核心。


第一句,获取 ArrayList 的迭代器。

public Iterator<E> iterator() {
    return new Itr();
}
复制代码


AbstractList 中定义了一个迭代器 Itr,但是它的子类 ArrayList 并没有直接使用父类的迭代器,而是自己定义了一个优化版本的 Itr。循环体中第二句代码首先会判断是否 hasNext(),存在的话调用 next 获取元素,不存在的话跳出循环。增强型 for 循环的基本实现就是这样的。hasNext()next() 方法源码如下:

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;
    Itr() {}
    public boolean hasNext() {
        return cursor != size;
    }
    @SuppressWarnings("unchecked")
    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];
    }
    ......
    // 省略其他代码
    }
复制代码


cursur 表示当前游标位置,hasNext() 方法就是根据 cursor 是否等于集合大小 size 判断是否还有下一个元素。成员变量中有个 expectedModCount,定义如下:

int expectedModCount = modCount;
复制代码


终于发现了 modCount 的踪影,它被赋值给了 expectedModCount 变量,字面意思就是 期望的修改次数。具体它有什么用,接着看 next() 方法中的第一行代码,调用了 checkForComodification() 方法,这是用来做并发检测的:

final void checkForComodification() {
    if (modCount != expectedModCount) // 在迭代的过程中 modCount 发生了改变
        throw new ConcurrentModificationException();
}
复制代码


异常就是这样抛出来的,modCountexpectedModCount 不相等,即实际的修改次数与期望的修改次数不相等。expectedModCount 是在迭代器初始化的过程中赋值的,其值等于 modCount。在迭代过程中又不相等了,那就只可能是在迭代过程中修改了集合,造成了 modCount 变化。那么,哪些操作会导致 modCount 发生变化呢?JDK 源码注释中做了以下说明(modCount 在 AbstractList 中声明):


The number of times this list has been structurally modified. 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.


集合的结构修改次数。结构修改指的是集合大小的变化。所以只要是涉及到增加或者删除元素的方法,都要改变 modCount。以 ArrayList 的 remove() 方法为例:


public E remove(int index) {
    rangeCheck(index); // 边界检测
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0) // 移动 index 之后的所有元素
        System.arraycopy(elementData, index+1, elementData, index,
                        numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}
复制代码


通过 modCount++ 使其自增 1。

由于 ArrayList 并不是线程安全的,一边迭代一边改变集合,的确可能导致多线程下代码表现不一致。可能有人会有这样的疑问,文章开头的测试代码并没有涉及到并发操作啊,为什么还是抛出了异常?这就是集合的 fail-fast(快速失败) 机制。


fail-fast 错误机制并不保证错误一定会发生,但是当错误发生的时候一定可以抛出异常。它不管你是不是真的并发操作,只要可能是并发操作,就给你提前抛出异常。针对非线程安全的集合类,这是一种健壮的处理方式。但是你如果真的想在单线程中这样操作应该怎么办?没关系,让 modCountexpectedModCount 相等就完事了,ArrayList 的迭代器为我们提供了这样的 add()remove() 方法:


public void add(E e) {
    checkForComodification();
    try {
        int i = cursor;
        ArrayList.this.add(i, e); // add 之后要修改 modCount
        cursor = i + 1;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();
    try {
        ArrayList.this.remove(lastRet); // remove 之后要修改 modCount
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}
复制代码


上面的代码实现在修改了集合结构之后都会给 expectedModCount 重新赋值,使其与 modCount 相等。修改一下文章开头的测试代码:


public static void main(String[] args){
    List<String> list= new ArrayList<>();
    list.add("java");
    list.add("kotlin");
    list.add("dart");
//  for (String s:list){
//      if (s.equals("dart"))
//          list.remove(s);
//  }
    Iterator<String> iterator=list.iterator();
    while (iterator.hasNext()){
        String s= iterator.next();
        if (s.equals("dart"))
            iterator.remove();
    }
}
复制代码


这样就不会再报错了。

最后最后再给你出一道题,仔细看一下:

public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("java");
    list.add("kotlin");
    list.add("dart");
    for (String s : list) {
        if (s.equals("kotlin"))
            list.remove(s);
    }
}
复制代码


如果没看出来和文章开头那道题的区别,那就再翻上去仔细观察一下。之前我们要删的是 dart,集合中的最后一个元素。现在要删的是 kotlin,集合中的第二个元素。执行结果会怎么样?你要是精通脑筋急转弯的话,肯定能给出正确答案。没错,这次成功删除了元素并且没有任何异常。这是为什么呢?删除 dart 就报异常,删除 kotlin 就没问题,这是歧视 dart 吗。再把迭代器的代码掏出来:


public boolean hasNext() {
    return cursor != size;
}
@SuppressWarnings("unchecked")
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];
}
复制代码


集合中添加了 3 个元素,所以初始化迭代器之后,expectedModCount = modCount = 3cursor 此时为 0 。先来分析文章开头的代码,删除集合中最后一个元素的情况:


  • 执行完第一次循环,cursor 为 1,未产生删除操作,modCount 为 3,expectedModCount 为 3,size 为 3。cursor != sizehasNext() 判断还有元素。
  • 执行完第二次循环,cursor 为 2,仍未产生删除操作,modCount 为 3,expectedModCount 为 3,size 为 3。cursor != sizehasNext() 判断还有元素。
  • 执行完第三次循环,cursor 为 3,由于产生删除了操作,modCount 为 4,expectedModCount 仍为 3,size 变为 2。cursor != sizehasNext() 判断还有元素,继续迭代,其实已经没有元素了。
  • 继续迭代,调用 next() 方法,此时 expectedModCount != modCount,直接抛出异常。


循环次数 cursor modCount expectedModCount size
1 1 3 3 3
2 2 3 3 3
3 3 4 3 2


再来看看删除 kotlin 的执行流程:

  • 执行完第一次循环,cursor 为 1,未产生删除操作,modCount 为 3,expectedModCount 为 3,size 为 3。cursor != sizehasNext() 判断还有元素。
  • 执行完第二次循环,cursor 为 2,产生删除操作,modCount 为 4,expectedModCount 为 3,size 为 2。cursor == sizehasNext() 判断没有元素了,不再调用 next() 方法。


并不是 fail-fast 失效了,仅仅只是恰好 cursor == sizehasNext() 方法误以为集合中已经没有元素了,其实还有一个元素。循环两次之后就终止循环了,不再调用 next() 方法,也就不存在并发检测了。


循环次数 cursor modCount expectedModCount size
1 1 3 3 3
2 2 3 3 2


本文由一个 ConcurrentModificationException 的例子,顺藤摸瓜,解析了 ArrayList 迭代器的源码,同时说明了 Java 集合框架的 fail-fast 机制。最后也验证了增强型 for 循环中删除元素并不是百分之百会触发 fail-fast

ArrayList 就说到这里了,下一篇来看看 List 中同样重要的 LinkedList



相关文章
|
存储 Java API
ArrayList类【JDK源码分析】
ArrayList类【JDK源码分析】
50 0
|
存储 安全 Java
高频面试题-JDK集合源码篇(String,ArrayList)
都是List的子集合,LinkedList继承与Dqueue双端队列,看名字就能看出来前者是基于数组实现,底层采用Object[]存储元素,数组中的元素要求内存分配连续,可以使用索引进行访问,它的优势是随机访问快,但是由于要保证内存的连续性,如果删除了元素,或者从中间位置增加了元素,会设计到元素移位的操作,所以增删比较慢。
77 0
|
设计模式 Java 数据库连接
设计模式之组合模式 附JDK ArrayList中的设计模式应用
设计模式之组合模式 附JDK ArrayList中的设计模式应用
95 0
|
存储 安全 Java
Java集合源码剖析——基于JDK1.8中ArrayList的实现原理
Java集合源码剖析——基于JDK1.8中ArrayList的实现原理
Java集合源码剖析——基于JDK1.8中ArrayList的实现原理
|
存储 安全 Java
走进 JDK 之 ArrayList(一)
这篇本来是准备写 Java 集合框架概述 的,就是写起来效果不怎么样,可能是对整个 Java 集合框架还没有做到了然于心。所以还是先来源码分析,写完所有集合类的分析之后,再来总体概述。今天就从最最常用的 ArrayList 说起。
走进 JDK 之 ArrayList(一)
|
安全 Java vr&ar
Java进阶:【集合】1.8 jdk Arraylist和Vector集合源码分析
Java进阶:【集合】1.8 jdk Arraylist和Vector集合源码分析
Java进阶:【集合】1.8 jdk Arraylist和Vector集合源码分析
|
安全 Java
|
安全 Java 索引
【JDK源码】ArrayList的代码实现
ArrayList 底层数据结构就是一个数组……
82 0
|
存储 Java
JDK源码分析系列之三:ArrayList源码分析
Java中的List集合属于一种线性的数据结构,它继承了Collection接口。常见的List集合实现有ArrayList以及LinkedList,本文将从源码分析以及使用场景等方面对ArrayList进行具体的阐述。 源码分析 使用场景 总结
JDK源码分析系列之三:ArrayList源码分析
|
存储 安全 Java
JDK集合源码之ArrayList解析(附带面试题举例)
JDK集合源码之ArrayList解析(附带面试题举例)
JDK集合源码之ArrayList解析(附带面试题举例)