HashSet.remove()和Iterator.remove()不起作用

时间:2008-10-31 18:25:45

标签: java collections

我遇到了在HashSet上调用Iterator.remove()的问题。

我有一组带时间戳的物品。在向Set添加新项目之前,我遍历该集合,识别该数据对象的旧版本并将其删除(在添加新对象之前)。时间戳包含在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)。

  • 我已经在JDK 5和JDK 6中进行了测试。

我认为我必须遗漏一些基本的东西,但是在我的同事花了一些重要的时间后,我感到难过。有什么建议要检查吗?

编辑:

有一些问题 - 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哈希码已更改。

非常微妙 - 我多次看了这个,并没有注意到缺乏不变性。

10 个答案:

答案 0 :(得分:44)

我对这个问题非常好奇,并写下了以下测试:

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

如果对象的hashCode()值已添加到HashSet后发生了更改,则它似乎会使对象无法移除。

我不确定这是否是您遇到的问题,但如果您决定重新访问此问题,则需要考虑这一点。

答案 1 :(得分:6)

在底层,HashSet使用HashMap,当调用HashSet.remove(Object)或Iterator.remove()时,HashMap调用HashMap.removeEntryForKey(Object)。此方法使用hashCode()和equals()来验证它是否从集合中删除了正确的对象。

如果Iterator.remove()和HashSet.remove(Object)都不起作用,那么你的equals()或hashCode()方法肯定是错误的。发布这些代码有助于诊断您的问题。

答案 2 :(得分:2)

您是否绝对确定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());

会打印两次不同的时间。

如果您可以发布一些源代码,也会有所帮助。

答案 3 :(得分:2)

感谢所有帮助。我怀疑问题必须是spencerk建议的equals()和hashCode()。我确实在我的调试器和单元测试中检查了那些,但我必须遗漏一些东西。

我最终做了一个解决方法 - 将除了一个之外的所有项目复制到一个新的Set。对于踢,我使用了Apache Commons CollectionUtils。

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

我要停在这里 - 太多工作要简化为一个简单的测试用例。但是,这种帮助很受欢迎。

答案 4 :(得分:2)

在子类型的哈希码取决于其可变状态的情况下,您应该小心任何通过哈希码获取其子代的Java Collection。一个例子:

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是动态的,并且依赖于它自身的可变属性。

答案 5 :(得分:1)

你有没有试过像

这样的东西
boolean removed = allResults.remove(oldData)
if (!removed) // COMPLAIN BITTERLY!

换句话说,从Set中删除对象并中断循环。这不会导致Iterator抱怨。我不认为这是一个长期解决方案,但可能会为您提供有关hashCodeequalsequalsData方法的一些信息

答案 6 :(得分:1)

几乎可以肯定的是,哈希码与“equals()”的旧数据和新数据不匹配。我之前遇到过这种情况,你最终会为每个对象和字符串表示法喷出哈希码,并试图找出不匹配的原因。

如果您要比较数据库前/后的项目,有时会丢失可能导致哈希码更改的纳秒(取决于您的数据库列类型)。

答案 7 :(得分:0)

Java HashSet在“ remove()”方法中存在问题。检查下面的链接。我切换到TreeSet,它工作正常。但是我需要O(1)时间复杂度。

https://bugs.openjdk.java.net/browse/JDK-8154740

答案 8 :(得分:-2)

如果有两个条目具有相同的数据,则只替换其中一个...您是否考虑到了这一点?为了以防万一,你有没有尝试过另一个不使用哈希码的集合数据结构,比如List?

答案 9 :(得分:-4)

我不能快速掌握我的Java,但是我知道当你在.NET中迭代那个集合时你无法从集合中删除一个项目,尽管如果它捕获了这个,它将抛出异常。这可能是问题吗?