1. ホーム
  2. ジャワ

探索中にArrayListのエラーが報告される理由を分析する。

2022-02-25 10:45:28
<パス

以前は、for(:){} を使用して List コレクションを反復処理し、同時に List コレクションの内容を変更すると ConcurrentModificationException エラーが報告されました。これは、メソッド内でオブジェクトが同時に変更されることを示すヒントですが、この例外はそのような変更が許可されていないときにスローされます。

1. コレクションをトラバースしながら同時修正をシミュレートする (a)

新しいリストコレクションを作成し、1~5の文字を順番にコレクションに追加し、コレクションをトラバースしながら、"2"を削除してください。

        public static void main(String[] args) {
             List<String> list = new ArrayList<String>();
                list.add("1");
                list.add("2");
                list.add("3");
                list.add("4");
                list.add("5");
                System.out.println("original list:" + list);
                for (String string : list) {
                    System.out.println(string);
                    //If the fetched content is "2", delete it
                    if ("2".equals(string)) {
                        list.remove(string);
                    }
                }
                System.out.println("modified list:: " + list);
        }


と出力されます。

    The original list:[1, 2, 3, 4, 5]
    1
    2
    Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
        at java.util.ArrayList$Itr.next(Unknown Source)
        at com.test.jh.TestListDemo.main(TestListDemo.java:16)

コレクションを要素 "2" まで走査すると ConcurrentModificationException が発生しますが、どのメソッドが例外をスローするのでしょうか。実は、例外をスローするのは Iterator クラスの next() メソッドなのです。

2. コレクションのforeach探索の原理の解析

Decompile the code in the above mian method by decompiling technique

    public static void main(String args[])
    {
        List list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");
        //The underlying implementation of string concatenation by + is also through the StringBuilder class
        System.out.println((new StringBuilder("original list:")).append(list).toString());
        //2. The underlying use of for (String string : list) to traverse a collection is implemented using Iterators
        for (Iterator iterator = list.iterator(); iterator.hasNext();)
        {
            //3. focus: here called next() method, in fact, the above error is called this method reported an error
            String string = (String)iterator.next();
            System.out.println(string);
            if ("2".equals(string))
                list.remove(string);
        }

        System.out.println((new StringBuilder("modified list:: ")).append(list).toString());
    }

foreachがどのようにコレクションを走査するかを分析した後、基礎となるコレクションをIteratorを使って実装し、String string = (String)iterator.next(); という行を実行したためにエラーが報告されました。

3. ArrayListのソースコード解析

Iterator の next() メソッドをチェックアウトします。

行の実行によりエラーが発生したため。String string = (String)iterator.next(); という行の実行が原因なので、Iterator クラスのコードを見てみましょう。

        // Assign the number of modifications to the collection (modCount) to expectedModCount and record it
        int expectedModCount = modCount;    

        public E next() {
            //It is mainly used to determine if the number of modifications to the collection is legal
            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];
        }


        // Mainly used to determine if the number of modifications to a collection is legal
        final void checkForComodification() {
            //modCount represents the number of times the collection has been modified (e.g., add 1 for every list.add() and add 1 for every list.remove())
            // the value of expectedModCount is equal to the number of times the collection was modified at the beginning of the iteration
            if (modCount ! = expectedModCount)
                throw new ConcurrentModificationException();
        }

checkForComodification()メソッドは、modCountとexpectedModCountの値が等しくない場合に例外を発生させます。では、この2つの値はどこで修正されるのでしょうか?実は、modCountの値はリストコレクションがaddやremoveなどを実行したときに、expectedModCountの値はIterator()の新規作成時にexpectedModCountレコードに代入されるのです。

ArrayList クラスのソースコードを表示する

次に、ArrayList クラスの remove() コードを見て、remove メソッドが本当に modCount++ をオンにしていることを確認します。

    public E remove(int index) {
        rangeCheck(index);
        // recorded the number of times the collection is modified, in fact, the collection of add () method also has modCount++; code
        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;
    }


賢い人なら、上記のコードから、なぜ、コレクションを反復してその内容を変更し、checkForComodification()の値が不等間隔になったときにConcurrentModificationExceptionが投げられるかがわかると思います。

4. Listコレクションを反復処理し、同時にコレクションの内容を変更するとエラーが発生する原因

    public static void main(String args[])
    {
        List list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");

        System.out.println((new StringBuilder("original list:")).append(list).toString());
        //3. Focus: list.iterator()
        for (Iterator iterator = list.iterator(); iterator.hasNext();)
        {
            String string = (String)iterator.next();
            System.out.println(string);
            if ("2".equals(string))
                list.remove(string);
        }

        System.out.println((new StringBuilder("modified list:: ")).append(list).toString());
    }

1. コードがトラバーサル中に list.iterator() を実行すると、Iterator オブジェクトを作成するときに、modCount の値を expectedModCount に代入します。その時点で expectedModCount の値はコレクションが現在変更された回数となります。

     //Executing list.iterator() calls this method
     public Iterator<E> iterator() {
        return new Itr();
     }

     private class Itr implements Iterator
 {

        int cursor; // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        // The value of modCount was assigned to expectedModCount when the Iterator object was created, and the value of expectedModCount is the current number of modifications to the set
        int expectedModCount = modCount;

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

        .....
    }

2. list.remove()を実行する際、以下の文に差し掛かると、modCount++;によってmodCountがexpectedModCountと不等号になります。次にString string = (String)iterator.next(); を呼び出すと、next() メソッドの checkForComodification() メソッドの modCount と expectedModCount の値が等しくないと例外がスローされることになります。ConcurrentModificationExceptionです。

    if ("2".equals(string))
                list.remove(string);



  • 概要

    実際には、例外として、コレクションをトラバースしている間にコレクションの内容を変更したため、modCountの値が増加し、expectedModCountの値が増加しなかったため、next()メソッドのcheckForComodification()メソッドが呼び出されてコレクションの変更回数が合法かどうか判断しエラーが報告されます。

6. コレクションをトラバースしながら修正するシミュレーション (II)

新しいリストコレクションを作成し、1~5の文字を順番にコレクションに追加し、コレクションをトラバースしながら"4"(最後から1つ目)を削除してください。

        public static void main(String[] args) {
             List<String> list = new ArrayList<String>();
                list.add("1");
                list.add("2");
                list.add("3");
                list.add("4");
                list.add("5");
                System.out.println("original list:" + list);
                for (String string : list) {
                    System.out.println(string);
                    //Key: is to delete "4", not "2", then will report an error? Actually, no.
                    if ("4".equals(string)) {
                        list.remove(string);
                    }
                }
                System.out.println("modified list:: " + list);
        }

と出力されます。

    The original list:[1, 2, 3, 4, 5]
    1
    2
    3
    4
    modified list:: [1, 2, 3, 5]

上記からわかるように、クソ、エラーも報告せずに正常に削除された?どうなってるんだ?

原因の分析

1. 以下のコードを実行すると、list.remove()で要素が削除され、サイズも1小さくなる。

    //current cursor=4,size=5
    if ("4".equals(string)) {
         list.remove(string);//removes a size-1 equals 4
    }

2. そして、再びhasNext()を決定するためにforループが強化されたとき。

    public boolean hasNext() {
            //when traversing to "4", the current cursor=4, the current size=4
            return cursor ! = size;
    }

3. iterator.hasNext() for (Iterator iterator = list.iterator(); iterator.hasNext();) を実行すると、戻り値が false になってしまうのはなぜですか?現在のカーソル=4,サイズ=4で、"4"までトラバースすると、hasNext()が返す値がfalseとなり、forループが直接終了し、5ループ目(最後のループ)を逃し、結果としてString string = ( String)iterator.next(); を実行できず、エラーは報告されない。

4. 削除したものが "4" ではなく "3" の場合、現在のカーソル = 3、現在のサイズ = 4 となり、次に iterator.hasNext() を実行すると、返ってくる結果は true となり、String string = (String)iterator.Iterator となる。 next();が実行される、要素の削除後、modCountの値が増加している結果、期待されるModCountの値が増加していないため、チェックForComodification()の呼び出しnext()メソッドでコレクションの修正カウントが合法かどうかを判断するエラーが報告されるでしょう。