web-dev-qa-db-ja.com

反復中にHashSetから要素を削除する

そのため、反復中にJava HashSetから要素を削除しようとすると、ConcurrentModificationExceptionが返されます。次の例のようにHashSetから要素のサブセットを削除する最良の方法は何ですか?

Set<Integer> set = new HashSet<Integer>();

for(int i = 0; i < 10; i++)
    set.add(i);

// Throws ConcurrentModificationException
for(Integer element : set)
    if(element % 2 == 0)
        set.remove(element);

ここに解決策がありますが、私はそれが非常にエレガントだとは思わない:

Set<Integer> set = new HashSet<Integer>();
Collection<Integer> removeCandidates = new LinkedList<Integer>();

for(int i = 0; i < 10; i++)
    set.add(i);

for(Integer element : set)
    if(element % 2 == 0)
        removeCandidates.add(element);

set.removeAll(removeCandidates);

ありがとう!

111
Scott

セットの要素を手動で反復できます。

Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()) {
    Integer element = iterator.next();
    if (element % 2 == 0) {
        iterator.remove();
    }
}

forループではなく、whileループを使用してこのパターンをよく目にします。

for (Iterator<Integer> i = set.iterator(); i.hasNext();) {
    Integer element = i.next();
    if (element % 2 == 0) {
        i.remove();
    }
}

人々が指摘したように、forループの使用は、反復子変数(この場合はi)をより小さなスコープに限定するため、推奨されます。

177
Adam Paynter

ConcurrentModificationExceptionを取得する理由は、Iterator.remove()ではなくSet.remove()でエントリが削除されるためです。反復の実行中にSet.remove()を使用してエントリが削除されると、ConcurrentModificationExceptionが発生します。一方、Iterator.remove()によるエントリの削除は、この場合は反復がサポートされています。

新しいforループはNiceですが、残念ながらIterator参照を使用できないため、この場合は機能しません。

反復中にエントリを削除する必要がある場合は、Iteratorを直接使用する長い形式を使用する必要があります。

for (Iterator<Integer> it = set.iterator(); it.hasNext();) {
    Integer element = it.next();
    if (element % 2 == 0) {
        it.remove();
    }
}
19
sjlee

Java 8 Collectionには、物事をより簡単で安全にするremoveIfという素敵なメソッドがあります。 APIドキュメントから:

default boolean removeIf(Predicate<? super E> filter)
Removes all of the elements of this collection that satisfy the given predicate. 
Errors or runtime exceptions thrown during iteration or by the predicate 
are relayed to the caller.

興味深いメモ:

The default implementation traverses all elements of the collection using its iterator(). 
Each matching element is removed using Iterator.remove().

From: https://docs.Oracle.com/javase/8/docs/api/Java/util/Collection.html#removeIf-Java.util.function.Predicate-

10
risoldi

最初のループを削除してソリューションをリファクタリングすることもできます。

Set<Integer> set = new HashSet<Integer>();
Collection<Integer> removeCandidates = new LinkedList<Integer>(set);

for(Integer element : set)
   if(element % 2 == 0)
       removeCandidates.add(element);

set.removeAll(removeCandidates);
10
dfa

木材が言ったように-「Java 8 CollectionにはremoveIfという素敵なメソッドがあり、それによって物事がより簡単で安全になります」

問題を解決するコードは次のとおりです。

set.removeIf((Integer element) -> {
    return (element % 2 == 0);
});

これで、セットには奇数値のみが含まれます。

8
Getriax

繰り返しながらする必要がありますか?フィルタリングまたは選択だけをしている場合は、Apache Commons CollectionUtils を使用することをお勧めします。そこには強力なツールがいくつかあり、それによってコードが「クール」になります。

必要なものを提供する実装は次のとおりです。

Set<Integer> myIntegerSet = new HashSet<Integer>();
// Integers loaded here
CollectionUtils.filter( myIntegerSet, new Predicate() {
                              public boolean evaluate(Object input) {
                                  return (((Integer) input) % 2 == 0);
                              }});

同じ種類の述語を頻繁に使用していることに気付いた場合は、それを静的変数に引き出して再利用できます。EVEN_NUMBER_PREDICATEのような名前を付けます。一部の人はそのコードを見て「読みにくい」と宣言するかもしれませんが、Predicateを静的に引き出すときれいに見えます。そうすれば、CollectionUtils.filter(...)を実行していることがわかりやすく、作成中のループの束よりも(私にとっては)読みやすいように見えます。

4
dustmachine

他の可能な解決策:

for(Object it : set.toArray()) { /* Create a copy */
    Integer element = (Integer)it;
    if(element % 2 == 0)
        set.remove(element);
}

または:

Integer[] copy = new Integer[set.size()];
set.toArray(copy);

for(Integer element : copy) {
    if(element % 2 == 0)
        set.remove(element);
}
2
alex2k8