1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【 Java 】拡張For文でremoveするとコケるよ〜って話

Last updated at Posted at 2023-04-15

■ はじめに

現場で拡張For文を使っているリストを操作している処理の中にremove入れたかったのですが、
調べてみるとどうやらコケるらしいので、簡易的な例文で挙動を確認してみました。

備忘録のため誤字脱字等意識せず書いてます。
あらかじめご了承くださいまし。。

■ 例文

今回String型を格納するリストを用意しました。
A、B、Cの文字列を格納しており、拡張文の中でAの文字列が格納されているリストは削除する処理を書いています。

① 実行ソース
    public static void main(String[] args) {
		
		List<String> list = new ArrayList<>();
		list.add("A");
		list.add("B");
		list.add("C");
		
		for (String s : list) {
			if ("A".equals(s)) {
				list.remove(s);
			}
		}
	}

・①を実行するとExceptionが発生して処理がこけてしまいました。
 コンソールに出力された内容は下記になります。 
 これを見たところ、最後の行で「Iterator」の文字列が確認できます。
 Iteratorとはなんぞや状態だったので調べてみたところ、ListとかMapとかのコレクション要素に繰り返しアクセスする際に使われるIterableインタフェースのメソッドとのこと。

② コンソール
    Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
	at Iterator.main(Iterator.java:13)

・どういう流れかさっぱりなので、Exceptionまでの流れをデバッグで確認してみました。
 最初に、①の拡張For文を書いているところでデバックしてみると、下記ソースが実行されていました。
 拡張For文の処理開始なのでIteratorの作成をしているようです。
 この処理で変数listが、Iteratorとなって返ってきます。

③ Iteratorの作成
    public Iterator<E> iterator() {
        return new Itr();
    }

   /*
    * iterator()戻り値
    * cursor          :0 // 次に返す要素のインデックス
    * expectedModCount:3 // イテレータによりコレクションが変更された回数
    * lastRet         :-1 //最後の要素 なければ-1を返す
    */

・Iterator作成後拡張For文に戻りますが、ステップインをすると以下ソースが実行されていました。
 hasNextメソッドは、反復にさらに要素がある場合は true を返すメソッドとのこと。
 ここでは、アクセスしている要素のインデックス(cursor)が要素数(size)と一致していないことを確認していることが確認できます。
 今回実行しているソースでは、インデックス0にアクセスしていて、要素数は3つなため「true」が返ってきます。

④ ArrayList.class.hasNext()
    public boolean hasNext() {
        return cursor != size;
    }
    
    /*
     * hasNext()戻り値
     * true
     */

・hasNextの処理後は再度拡張For文に戻ってきます。
 ステップインを行うと次はnextメソッドに処置が移ります。
 nextメソッドは次の要素を取り出すメソッドとのこと。
 そのため、今回は"A"が戻り値として返ってきます。

⑤ ArrayList.class.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];
    }

    /*
     * next()戻り値
     * "A"
     */

 ・nextメソッドで"A"の戻り値を受け取った後、①のequalメソッドが実行されtrueが返ってきます。
 次の処理removeメソッドに移ります。
 ステップインすると以下ソースが実行されていました。
 ⑦のfastRemoveメソッドで"A"が格納されているリストが削除されています。

⑥ ArrayList.class.remove(Object o)
    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;
    }
⑦ ArrayList.class.fastRemove(Object[] es, int i)
    private void fastRemove(Object[] es, int i) {
        // コレクション操作を行うため、modContの値を更新
        modCount++; // 3 → 4 
        final int newSize;
        if ((newSize = size - 1) > i)
            System.arraycopy(es, i + 1, es, i, newSize - i);
        es[size = newSize] = null;
    }

・"A"のリストの削除が完了し、次の"B"のリストの処理が始まります。
 先ほど同様、④のhasNextメソッドが実行されます。
 まだ最後まで処理は終わっていないので戻り値はtrueです。
 そして次は⑤ソースのnextメソッドが実行されます。
 ここでcheckForComodificationメソッド内で問題が起きました。

 if文の条件分岐でfalseが返ってきてConcurrentModificationExceptionが発生しました。
 これは、modCount(コレクションが操作された回数)とexpectedModCount(イテレータによりコレクションが変更された回数)を比較した結果、値が一致していなかったためExceptionとなったようです。

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

・では、どうしたら正常に処理が完了できるのか、、、結論Iteratorで用意されているremoveメソッドを使えばexpectedModCountが更新されて正常に処理がされるそう。。
 List操作からIterator操作にソースを修正します。

⑨ 実行ソース List → Iteratorに書き換え
    public static void main(String[] args) {
		
		List<String> list = new ArrayList<>();
		
		list.add("A");
		list.add("B");
		list.add("C");
		
		// 変数listのIteratorを作成
		Iterator<String> itr = list.iterator();
		
		// 次の要素が存在するか判定
		while (itr.hasNext()) {
			// 次の要素の値を変数strに代入
			String str = itr.next();
			if ("A".equals(str)) {
				// Iteratorクラスのremoveメソッドで削除処理
				itr.remove();
			}
		}
	}

 
・実行してみると削除処理は、hasNextメソッドやnextメソッドを使っていたIteratorクラスを実現したitrクラスのremoveメソッドに切り替わっていました。
 fastRemoveメソッドでmodCountが更新された後、removeメソッド内でexpectedModCountに代入しているので差異が発生せず、次回以降Exceptionが発生しないということがわかりました。

⑩ ArrayList.class.remove()
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            // ⑦ fastRemoveメソッドが実行され、modCountの値が4に更新される
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            // 変更前: 3 = 4
            // 変更後: 4 = 4
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

■ ArraryList.class itrクラス

        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;

        // prevent creating a synthetic constructor
        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];
        }

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

        @Override
        public void forEachRemaining(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i < size) {
                final Object[] es = elementData;
                if (i >= es.length)
                    throw new ConcurrentModificationException();
                for (; i < size && modCount == expectedModCount; i++)
                    action.accept(elementAt(es, i));
                // update once at end to reduce heap write traffic
                cursor = i;
                lastRet = i - 1;
                checkForComodification();
            }
        }

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

■ 参照

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?