イテレータが例外をスローせず、iterator.remove()
が例外をスローするように、list.remove()
はlist.remove()
とは異なる動作をしますか?結局、両方がコレクションのサイズを変更しています。
ここではマルチスレッドを無視してください。私はfor-eachループとイテレータループについて話しているだけです。私の知る限り、for-eachループは反復子を内部でのみ作成します。
私は混乱しています。
ConcurrentModificationException
はIterator.remove()
によってスローされません。これは、反復中にコレクションを変更する許可された方法だからです。これは、Iterator
の- javadoc が言うことです。
このイテレータによって返された最後の要素を基になるコレクションから削除します(オプションの操作)。このメソッドは、next()の呼び出しごとに1回だけ呼び出すことができます。 反復がこのメソッドを呼び出す以外の方法で進行中に基本となるコレクションが変更された場合のイテレータの動作は指定されていません。
他の方法で反復されているコレクションを変更すると、反復子の実装と、反復しているコレクション(または何でも)に応じて、例外が発生する可能性があります。 (一部のコレクションクラスはConcurrentModificationException
を提供しません。それぞれのjavadocsをチェックして、theirイテレータの動作を指定する方法を確認してください)
同じコレクションに2つのイテレータがあり、そのうちの1つを介して削除した場合も、例外が発生する可能性があります。
List.removeがスローしている間、イテレータが例外をスローしないという点で、list.removeとの違いは何ですか?
理由#1。同じ呼び出しスタックの2つの場所から同時に非並行コレクションが更新されている場合、動作は反復の設計不変を壊します1。非並行コレクションの反復では、コレクション内のすべての要素が1回だけ表示されることが保証されています。 (対照的に、並行コレクションでは、これらの保証は緩和されます。)
理由#2。非並行コレクション型は、スレッドセーフになるように実装されていません。したがって、コレクションとイテレータを使用して異なるスレッドでコレクションを更新すると、競合状態とメモリ異常が発生する可能性があります。とにかくこれらの問題が発生するため、これはstrongの理由ではありません。ただし、2つの異なる方法で更新を行うと、問題がさらに悪化します。
私はfor-eachループとイテレータループについて話しているだけです。私が知る限り、for-eachループは内部的にイテレータのみを作成します。
それは正しいです。 for-eachループは、実際には、イテレータを使用したwhile
ループの単なる構文上の砂糖です。
一方、次のようなループを使用するとします。
for (int i = 0; i < list.size(); i++) {
if (...) {
list.remove(i);
}
}
ConcurrentModificationException
は取得できませんが、削除する要素のインデックス変数を調整する必要があります。別のスレッドによる更新では、要素をスキップしたり、要素に複数回アクセスしたりする可能性があります2。
1-「1回だけ」の反復動作を実現するには、コレクションオブジェクトを介して要素を削除するときに、コレクションに起こったことに合わせて反復子データ構造を更新する必要があります。現在の実装では、優れたイテレータへのリンクが維持されないため、これは不可能です。また、そうした場合、Reference
オブジェクトを使用する必要があります。そうしないと、メモリリークのリスクがあります。
2-またはIndexOutOfBoundsException
を取得します。また、コレクションが並行していない/適切に同期されていない場合、問題がさらに悪化する可能性があります。
つまり、リストを反復している場合、list.remove()
がConcurrentModificationException
をスローするのに、iterator.remove()
はスローしないのはなぜですか?
この例を考えてみましょう:
_ List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (Iterator<String> iter = list.iterator(); iter.hasNext(); ) {
if (iter.next().equals("b")) {
// iter.remove(); // #1
// list.remove("b"); // #2
}
}
_
1行目をコメント解除すると、正常に動作します。行#2のコメントを外す(ただし、#1はコメント化したままにする)と、その後のiter.next()
の呼び出しでConcurrentModificationException
がスローされます。
その理由は、イテレータは、基になるリストの内部状態へのいくつかの参照を持つ独立したオブジェクトであるためです。イテレータの動作中にリストを変更すると、イテレータの動作が低下する可能性があります。要素のスキップ、要素の繰り返し、配列の末尾からのインデックス付けなどにより、このような変更を検出しようとするため、そうした場合はConcurrentModificationException
をスローします。
イテレータによる要素の削除は機能し、例外は発生しません。これは、基になるリストを更新するためですandリストの内部を参照するイテレータの状態。
ただし、すべてのケースで機能するiterator.remove()
について特別なことは何もありません。同じリストを反復するmultipleイテレータがある場合、1つによって行われた変更は他の問題を引き起こします。検討してください:
_ Iterator<String> i1 = list.iterator();
Iterator<String> i2 = list.iterator();
i1.remove();
i2.remove();
_
これで、同じリストを指す2つのイテレータができました。それらの1つを使用してリストを変更すると、2番目の操作が中断されるため、i2.remove()
を呼び出すとConcurrentModificationException
になります。
例外をスローするのはイテレータだからです。 List.remove()
を呼び出すと、削除については認識されず、何かが変更されたことがわかります。 Iterator.remove()
を呼び出すと、現在の要素が削除されたことと、それに対して何をすべきかがわかります。
コレクションイテレータが基になるコレクションの変更をチェックしなかった場合に問題が発生する可能性がある例を次に示します。これがArrayLists
のイテレータの実装方法です:
_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
public E next() {
checkForComodification();
int i = cursor;
if (i >= size) throw new NoSuchElementException();
// ...
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
// ...
ArrayList.this.remove(lastRet);
// ...
cursor = lastRet;
lastRet = -1;
}
_
例を見てみましょう:
_List list = new ArrayList(Arrays.asList(1, 2, 3, 4));
Iterator it = list.iterator();
Integer item = it.next();
_
最初の要素を削除します
_list.remove(0);
_
ここでit.remove()
を呼び出す場合、イテレータはnumber 2を削除します。これはlastRet
が指すフィールドだからです。
_if (item == 1) {
it.remove(); // list contains 3, 4
}
_
これは不正な動作です。イテレータのコントラクトは、remove()
がnext()
によって返された最後の要素を削除することを示していますが、同時変更が存在する場合、コントラクトを保持できませんでした。したがって、それは安全側にいることを選択して例外をスローします。
他のコレクションの場合、状況はさらに複雑になる可能性があります。 HashMap
を変更すると、必要に応じて拡大または縮小される場合があります。そのとき、要素は別のバケットに分類され、リハッシュが完全に失われる前に、バケットへのポインタを保持するイテレータがありました。
iterator.remove()
はそれ自体で例外をスローしないことに注意してください。これは、自身とコレクションの内部状態bothを更新できるためです。ただし、同じインスタンスコレクションの2つのイテレータでremove()
を呼び出すと、イテレータの1つが矛盾した状態になるため、スローされます。
public class ArrayListExceptionTest {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
list1.add("c");
Iterator<String> it1 = list1.iterator();
ArrayList<String> list2 = new ArrayList<String>();
list2.add("a");
try {
while (it1.hasNext()) {
list1.add(it1.next());
}
} catch (ConcurrentModificationException e) {
e.printStackTrace();
}
it1 = list1.iterator();
while (it1.hasNext()) {
System.out.println(it1.next());
}
it1 = list1.iterator();
try {
while (it1.hasNext()) {
if (it1.next().equals("a"))
list1.retainAll(list2);
}
} catch (ConcurrentModificationException e) {
e.printStackTrace();
}
it1 = list1.iterator();
while (it1.hasNext()) {
System.out.println(it1.next());
}
it1 = list1.iterator();
Iterator<String> it2 = list1.iterator();
it1.remove();
it2.remove();
}
}
上記の3つのケースを見ることができます
ケース1:要素を追加することによって変更が行われたため、next()関数を使用すると、ConcurrentModificationExceptionが発生しました。
ケース2:retain()を使用して変更が行われたため、next()関数を使用すると、ConcurrentModificationExceptionが発生しました。
ケース3:ConcurrentModificationExceptionではなく、Java.lang.IllegalStateExceptionをスローします。
出力:
a
b
c
a
a
a
Java.util.ConcurrentModificationException
at Java.util.ArrayList$Itr.checkForComodification(ArrayList.Java:909)
at Java.util.ArrayList$Itr.next(ArrayList.Java:859)
at com.rms.iteratortest.ArrayListExceptionTest.main(ArrayListExceptionTest.Java:21)
Java.util.ConcurrentModificationException
at Java.util.ArrayList$Itr.checkForComodification(ArrayList.Java:909)
at Java.util.ArrayList$Itr.next(ArrayList.Java:859)
at com.rms.iteratortest.ArrayListExceptionTest.main(ArrayListExceptionTest.Java:37)
Exception in thread "main" Java.lang.IllegalStateException
at Java.util.ArrayList$Itr.remove(ArrayList.Java:872)
at com.rms.iteratortest.ArrayListExceptionTest.main(ArrayListExceptionTest.Java:55)