インタビュー - foreach で要素の追加と削除が許可されない理由

1. Foreach は、ArrayList を走査するプロセスで add と delete を使用します。

まず、foreach を使用して ArrayList を走査するプロセスで add と delete を使用した結果を見て、それを分析してみましょう。

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
        list.add(i);
    }
    for (Integer j : list) {
        if (j.equals(3)) {
            list.remove(3);
        }
        System.out.println(j);
    }
}

操作結果:

0
1
2
3
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at test.Test.main(Test.java:12)

結果は ConcurrentModificationException 例外であり、例外がスローされた場所を追跡します (ArrayList.java:911)。

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

ここでは、modCount が ExpectedModCount と等しくない場合、この例外メッセージがスローされることがわかります。では、これら 2 つのパラメーターは何を表すのでしょうか? 等しくない場合に例外が発生するのはなぜですか?

2. ソースを追跡する

2.1. modCountとは何ですか?

ここでソースコードを見てみると、この変数をクリックすると、modCountがAbstractListクラスのメンバ変数であり、その値がListの変更回数を示すコメントが表示されます。

このとき、removeメソッドでこの変数が増加したか減少したかを見てみましょう。

ご覧のとおり、remove メソッドでは、実際には modCount に対して ++ のみが実行されます。では、expectedModCount とは何でしょうか?

2.2. ExpectModCount とは何ですか?

ExpectedModCount は ArrayList の内部クラス (Itr のメンバー変数) です。別の内部クラス Itr を取り出す方法を見てみましょう。

逆コンパイルすると、コンパイル後に反復子を使用して foreach が内部的に実装されていることがわかります。

 Iterator は list.iterator() を通じてインスタンス化され、list.iterator() は内部クラス Itr のオブジェクトを返します。ソース コードから、Itr が Iterator インターフェイスを実装し、メンバー変数 ExpectModCount を宣言していることがわかります。 ExpectedModCount ArrayList の変更回数の期待値。初期値は modCount です。

2.3. おなじみの checkForComodification メソッド

ソース コードを見ると、このクラスの next メソッドと Remove メソッドが checkForComodification メソッドを呼び出していることがわかります。checkForComodification についてご存知ですか? ここが例外がスローされる場所ではないでしょうか?

checkForComodification メソッドは、modCount と ExpectedModCount が等しいかどうかを判断して、同時変更例外をスローするかどうかを決定します。

2.4. プロセスのレビュー

コンパイルされたクラス ファイルを見ると、一般的なプロセスは次のとおりであることがわかります。 j が 3 の場合、remove メソッドが呼び出され、remove メソッド内で modCount 値が変更され、j の値が出力されます。次に次のサイクルに入ります。このとき hasNext は true です。ループ本体にコードの最初の行を入力し、next メソッドを呼び出し、次に checkForComodification メソッドを呼び出します。その後、expectedModCount と modCount が矛盾していることを確認し、最後にスローします。 ConcurrentModificationException。

 つまり、expectedModCountはmodCountに初期化されますが、expectedModCountは後で変更されず、modCountは削除と追加のプロセスで変更され、実行時に2つの値が等しいかどうかを判断するcheckForComodificationメソッドにつながり、それらが等しい場合は問題ありませんが、等しくない場合は例外がスローされます。

これは、平たく言えばフェイルファストメカニズム、つまり、迅速な検出失敗メカニズムと呼ばれるものです。

3. フェイルファストメカニズムを回避する

3.1. listIterator またはイテレータを使用する

フェイルファストメカニズムも回避できます。たとえば、上記のコードを削除します。

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 5; i++) {
        list.add(i);
    }

    System.out.println("没有删除元素前"+list.toString());
    // 迭代器使用listIterator和iterator均可
    ListIterator<Integer> listIterator = list.listIterator();
    while(listIterator.hasNext()){
        Integer integer = listIterator.next();
        if(integer==3){
            listIterator.remove();
            listIterator.add(9);
        }
    }
    System.out.println("删除元素后"+list.toString());
}

 この場合、問題なく実行できることがわかります。実行結果を見てみましょう。

没有删除元素前[0, 1, 2, 3, 4]
删除元素后[0, 1, 2, 9, 4]

 結果も明らかで、foreach に追加と削除の操作を実装しました。

ここで注意点があります イテレータには listIterator と iterator の両方を使用することができます 実際に listIterator で使用される ListItr の内部クラスはソースコードを見ると確認できます ListItr は Itr クラスを継承し、 などのいくつかのメソッドをシールしていますadd、hasPrevious、previous など。したがって、コード内のremoveメソッドはItrクラスのものであり、addメソッドはListItrクラスのものです。

 listIterator とイテレータの違い:

  1. 使用範囲は異なります。Iterator はすべてのコレクション、Set、List、Map、およびこれらのコレクションのサブタイプに適用できます。また、ListIterator は List とそのサブタイプにのみ使用できます。
  2. ListIterator にはオブジェクトを List に追加できる add メソッドがありますが、Iterator には追加できません。
  3. ListIterator と Iterator の両方には、順次逆方向トラバーサルを実現できる hasNext() メソッドと next() メソッドがありますが、ListIterator には、逆方向 (順方向) トラバーサルを実現できる hasPrevious() メソッドとPrevious() メソッドがあります。イテレータはできません。
  4. ListIterator は現在のインデックスの位置を特定でき、nextIndex() とpreviousIndex() を実装できます。イテレータにはこの機能はありません。
  5. どちらも削除操作を実装できますが、ListIterator はオブジェクトの変更を実装でき、set() メソッドはそれを実装できます。イテレータはトラバースのみ可能であり、変更することはできません。

3.2. CopyOnWriteArrayList を使用する

CopyOnWriteArrayList クラスでもフェイルファストの問題を解決できます。試してみましょう。

public static void main(String[] args) {
    CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
    for (int i = 0; i < 5; i++) {
        list.add(i);
    }
    System.out.println("没有删除元素前"+list.toString());
    for (Integer integer : list) {
        if(integer.equals(3)){
            list.remove(3);
            list.add(9);
        }
    }
    System.out.println("删除元素后"+list.toString());
}

 操作結果:

没有删除元素前[0, 1, 2, 3, 4]
删除元素后[0, 1, 2, 4, 9]

CopyOnWriteArrayList はこの要素の途中で削除と追加の操作を実装します。その内部ソース コードはどのように実装されているのか、実際には非常に単純です

つまり、新しい配列を作成し、古い配列を新しい配列にコピーします。しかし、このアプローチを推奨する人がほとんどいない理由は、根本的な理由はコピーすることです 

コピーを使うので同じ内容を格納するスペースが2つ必要で容量を食う 最後にGCを行うとクリーンアップに時間がかかるので個人的にはオススメしませんが書き込みはありますまだ出てくる必要がある。

3.2.1、CopyOnWriteArrayListのaddメソッド

public boolean add(E e) {
    // 可重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 元素数组
        Object[] elements = getArray();
        // 数组长度
        int len = elements.length;
        // 复制数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 存放元素e
        newElements[len] = e;
        // 设置数组
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

処理の流れは以下の通りです。

  • ロックを取得し(複数のスレッドから安全にアクセスできるようにするため)、現在のObject配列を取得し、そのObject配列の長さをlengthとして取得し、手順②に進みます。

  • Object 配列に合わせて長さ +1 の Object 配列を newElements としてコピーし (このとき newElements[length] は null)、次のステップに進みます。

  • 添字の長さを要素 e として配列要素 newElements[length] を設定し、次に現在の Object[] を newElements として設定し、ロックを解放して戻ります。これで要素の追加は完了です。

3.2.2、CopyOnWriteArrayListのremoveメソッド

public E remove(int index) {
    // 可重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 获取数组
        Object[] elements = getArray();
        // 数组长度
        int len = elements.length;
        // 获取旧值
        E oldValue = get(elements, index);
        // 需要移动的元素个数
        int numMoved = len - index - 1;
        if (numMoved == 0) // 移动个数为0
            // 复制后设置数组
            setArray(Arrays.copyOf(elements, len - 1));
        else { // 移动个数不为0
            // 新生数组
            Object[] newElements = new Object[len - 1];
            // 复制index索引之前的元素
            System.arraycopy(elements, 0, newElements, 0, index);
            // 复制index索引之后的元素
            System.arraycopy(elements, index + 1, newElements, index,
                                numMoved);
            // 设置索引
            setArray(newElements);
        }
        // 返回旧值
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

処理の流れは以下の通りです。

  1. ロックを取得し、配列要素を取得します。配列の長さは length です。インデックス値 elements[index] を取得します。移動する必要がある要素の数 (長さ - インデックス - 1) を計算します (数値が 0 の場合)。削除されたものが配列であることを意味します 最後の要素については、要素配列をコピーし、コピー長は長さ-1で配列を設定し、手順③に進みます。それ以外の場合は、手順②に進みます
  2. まずインデックス Index より前の要素をコピーし、次にインデックス Index より後の要素をコピーして、配列を設定します。
  3. ロックを解放し、古い値を返します。

知らせ

CopyOnWriteArrayList は、イテレータを介して要素を削除または追加することによってフェイルファストの問題を解決するのではなく、リスト自体の削除および追加メソッドを通じて、追加される要素の位置も異なります。イテレータは現在の位置の後ろにあります。 CopyOnWriteArrayList は最後に直接配置されます。

アイデアがある学生は、listIterator と CopyOnWriteArrayList のイテレータを見てみましょう。これらは実際には同じであり、すべて COWIterator の内部クラスが返されます。

 削除、設定、追加の操作は COWIterator の内部クラスではサポートされていません。少なくとも私が使用している jdk1.8 ではサポートされていないため、UnsupportedOperationException が直接スローされます。

 まずはここに書いて、時間ができたら追記していきます。

おすすめ

転載: blog.csdn.net/qq_34272760/article/details/120953988