web-dev-qa-db-ja.com

HashSet.remove()およびIterator.remove()が機能しない

HashSetで呼び出されるIterator.remove()に問題があります。

タイムスタンプ付きのオブジェクトのセットがあります。セットに新しいアイテムを追加する前に、セットをループし、そのデータオブジェクトの古いバージョンを識別して削除します(新しいオブジェクトを追加する前に)。タイムスタンプはhashCodeとequals()に含まれていますが、equalsData()には含まれていません。

for (Iterator<DataResult> i = allResults.iterator(); i.hasNext();)
{
    DataResult oldData = i.next();
    if (data.equalsData(oldData))
    {   
        i.remove();
        break;
    }
}
allResults.add(data)

奇妙なことに、i.remove()は、セット内の一部のアイテムに対してサイレントに失敗します(例外はありません)。確認しました

  • 行i.remove()が実際に呼び出されます。 Eclipseのブレークポイントでデバッガーから直接呼び出すことができますが、それでもSetの状態を変更できません。

  • DataResultは不変オブジェクトであるため、最初にセットに追加された後で変更することはできません。

  • EqualsメソッドとhashCode()メソッドは、@ Overrideを使用して、それらが正しいメソッドであることを確認します。ユニットテストはこれらの作業を検証します。

  • ForステートメントとSet.removeを代わりに使用した場合も、これは失敗します。 (たとえば、アイテムをループし、リストでアイテムを見つけて、ループの後にSet.remove(oldData)を呼び出します)。

  • JDK5とJDK6でテストしました。

何か基本的なことが欠けているに違いないと思いましたが、これにかなりの時間を費やした後、同僚と私は困惑しています。チェックすることについて何か提案はありますか?

編集:

質問がありました-DataResultは本当に不変ですか。はい。セッターはありません。また、Dateオブジェクト(可変オブジェクト)が取得されると、コピーが作成されます。

public Date getEntryTime()
{
    return DateUtil.copyDate(entryTime);
}

public static Date copyDate(Date date)
{
    return (date == null) ? null : new Date(date.getTime());
}

さらなる編集(しばらくしてから):記録のために-DataResultは不変ではありませんでした!これは、データベースに永続化されると変更されるハッシュコードを持つオブジェクトを参照していました(悪い習慣ですが、私は知っています)。 DataResultが一時的なサブオブジェクトで作成され、サブオブジェクトが永続化されている場合、DataResultハッシュコードが変更されていることが判明しました。

非常に微妙です-私はこれを何度も見て、不変性の欠如に気づきませんでした。

35
Will Glass

私はまだこれに非常に興味があり、次のテストを書きました:

import Java.util.HashSet;
import Java.util.Iterator;
import Java.util.Random;
import Java.util.Set;

public class HashCodeTest {
    private int hashCode = 0;

    @Override public int hashCode() {
        return hashCode ++;
    }

    public static void main(String[] args) {
        Set<HashCodeTest> set = new HashSet<HashCodeTest>();

        set.add(new HashCodeTest());
        System.out.println(set.size());
        for (Iterator<HashCodeTest> iter = set.iterator();
                iter.hasNext();) {
            iter.next();
            iter.remove();
        }
        System.out.println(set.size());
    }
}

その結果:

1
1

HashSetに追加されてからオブジェクトのhashCode()値が変更された場合、オブジェクトが削除できなくなっているように見えます。

それがあなたが直面している問題であるかどうかはわかりませんが、これを再検討することにした場合は、調査する必要があります。

49
Jack Leow

裏では、HashSetはHashMapを使用します。これは、HashSet.remove(Object)またはIterator.remove()のいずれかが呼び出されたときにHashMap.removeEntryForKey(Object)を呼び出します。このメソッドは、hashCode()とequals()の両方を使用して、コレクションから適切なオブジェクトを削除していることを検証します。

Iterator.remove()とHashSet.remove(Object)の両方が機能していない場合は、equals()メソッドまたはhashCode()メソッドに間違いなく問題があります。これらのコードを投稿すると、問題の診断に役立ちます。

6
Spencer Kormos

DataResultが不変であると絶対に確信していますか?タイムスタンプの種類は何ですか? Java.util.Dateの場合、DataResultを初期化するときにコピーを作成していますか? Java.util.Dateは変更可能であることに注意してください。

例えば:

Date timestamp = new Date();
DataResult d = new DataResult(timestamp);
System.out.println(d.getTimestamp());
timestamp.setTime(System.currentTimeMillis());
System.out.println(d.getTimestamp());

2つの異なる時間を印刷します。

ソースコードを投稿できれば助かります。

2
Jack Leow

子タイプのハッシュコードが可変状態に依存する場合、ハッシュコードによって子をフェッチするJavaコレクションには注意する必要があります。例:

HashSet<HashSet<?>> or HashSet<AbstaractSet<?>> or HashMap variant:

HashSetはhashCodeによってアイテムを取得しますが、そのアイテムタイプはHashSetであり、hashSet.hashCodeはそのアイテムの状態によって異なります。

その問題のコード:

HashSet<HashSet<String>> coll = new HashSet<HashSet<String>>();
HashSet<String> set1 = new HashSet<String>();
set1.add("1");
coll.add(set1);
print(set1.hashCode()); //---> will output X
set1.add("2");
print(set1.hashCode()); //---> will output Y
coll.remove(set1) // WILL FAIL TO REMOVE (SILENTLY)

理由は、HashSetのremoveメソッドがHashMapを使用し、hashCodeによってキーを識別するのに対し、AbstractSetのhashCodeは動的であり、それ自体の可変プロパティに依存するためです。

2
Tomer Shalev

すべての助けをありがとう。 spencerkが示唆しているように、問題はequals()とhashCode()にあるに違いないと思います。デバッガーと単体テストでそれらをチェックしましたが、何かが足りないはずです。

最終的に回避策を実行しました。1つを除くすべてのアイテムを新しいセットにコピーしました。キックには、Apache CommonsCollectionUtilsを使用しました。

    Set<DataResult> tempResults = new HashSet<DataResult>();
    CollectionUtils.select(allResults, 
            new Predicate()
            {
                public boolean evaluate(Object oldData)
                {
                    return !data.equalsData((DataResult) oldData);
                }
            }
            , tempResults);
    allResults = tempResults;

ここでやめます。単純なテストケースに単純化するには作業が多すぎます。しかし、助けは大歓迎です。

2
Will Glass

あなたは次のようなことを試しましたか

boolean removed = allResults.remove(oldData)
if (!removed) // COMPLAIN BITTERLY!

つまり、オブジェクトをセットから削除し、ループを解除します。それによってIteratorが文句を言うことはありません。これは長期的な解決策ではないと思いますが、おそらくhashCodeequals、およびequalsDataメソッドに関する情報が得られるでしょう。

1
Ken Gentle

ハッシュコードが「equals()」である新旧のデータと一致しないのはほぼ確実です。私は以前にこの種のことに遭遇しましたが、基本的にすべてのオブジェクトと文字列表現のハッシュコードを吐き出し、不一致が発生している理由を理解しようとしています。

データベースの前後のアイテムを比較している場合、(DB列のタイプに応じて)ナノ秒が失われることがあり、ハッシュコードが変更される可能性があります。

1
Chris Kessel