【问题标题】:HashSet.remove() and Iterator.remove() not workingHashSet.remove() 和 Iterator.remove() 不起作用
【发布时间】:2008-10-31 18:25:44
【问题描述】:

我在 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))。

  • 我在 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 哈希码已更改。

非常微妙——我看了很多遍,并没有注意到缺乏不变性。

【问题讨论】:

  • 两种可能。 1.你说DataResult是不可变的。假设值由构造函数设置并且没有设置方法是否安全? 2. 你的 equals 和 hashcode 没有像你预期的那样工作。你能把这两个的代码贴出来吗?

标签: java collections


【解决方案1】:

我还是对这个很好奇,写了如下测试:

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 后发生了变化,则似乎使该对象不可移除。

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

【讨论】:

  • 谢谢——非常感谢。我怀疑你可能是对的。将重新访问我的单元测试,以获取基础对象上的 equals/hashCode。
  • 六年后,这个答案为我节省了更多时间与前台互动。 +1。 +allthe1s。
  • +1;这个解决方案只是帮助我解决了构建一组子图时的一个令人沮丧的错误。我想补充一点,发生这种情况的原因似乎是 HashSet 包装了 HashMap。散列中的值被视为键而不是值。当然键不能是可变的,但令人沮丧的是,集合由迭代组成的直观概念被违反了......
  • “如果一个对象的 hashCode() 值在添加到 HashSet 后发生了变化,它似乎会导致该对象不可移除。”那是金子。
  • 好的,所以不可能删除这样的元素,或者每次发生这种情况时我们应该用new HashSet&lt;&gt;(set) 重新实例化set
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

    您确定 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());
    

    将打印两次不同的时间。

    如果你能发布一些源代码也会有所帮助。

    【讨论】:

    • @Jack Leow:这是一个很棒的评论。但是,我在通过 getter 公开之前复制日期对象。 (最近正在阅读 Bloch 的 Effectiva Java 第 2 版)。所以 DataResult 是真正不可变的。
    • 复制值或引用?
    • @Blade:创建一个新的 Date 实例。请参阅对原始问题的编辑。
    【解决方案4】:

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

    我最终做了一个解决方法——将除一个之外的所有项目复制到一个新集合中。为了好玩,我使用了 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;
    

    我将在此停下来——要简化为一个简单的测试用例,工作量太大了。但是非常感谢您的帮助。

    【讨论】:

      【解决方案5】:

      您都应该小心任何通过哈希码获取其子代的 Java 集合,以防其子类型的哈希码取决于其可变状态。一个例子:

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

      HashSet 通过它的 hashCode 检索一个项目,但它的项目类型 是一个 HashSet,hashSet.hashCode 依赖于它的 item 的状态。

      相关代码:

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

      【讨论】:

        【解决方案6】:

        你有没有尝试过类似的东西

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

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

        【讨论】:

          【解决方案7】:

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

          如果您在比较数据库前后的项目,有时它会丢失几纳秒(取决于您的数据库列类型),这可能会导致哈希码发生变化。

          【讨论】:

            【解决方案8】:

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

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

            【讨论】:

              【解决方案9】:

              如果有两个条目具有相同的数据,则只替换其中一个...您对此进行了说明吗?为了以防万一,您是否尝试过另一种不使用哈希码的集合数据结构,例如 List?

              【讨论】:

              • 没错,但是骗子是由equals()方法定义的,在这种情况下使用数据+时间戳。
              【解决方案10】:

              我的 Java 跟不上速度,但我知道当您在 .NET 中迭代该集合时,您无法从该集合中删除一个项目,尽管 .NET 会在捕获此异常时抛出异常.会不会是这个问题?

              【讨论】:

              • 虽然有时确实不能从支持迭代器的集合中删除元素(可能会抛出 ConcurrentModificationException),但如果它支持该操作,则可以直接通过迭代器本身删除元素,它为 HashSet 所做的。
              • 并且不支持删除的迭代器在任何 remove() 尝试时都会抛出 UnsupportedOperationException。
              猜你喜欢
              • 1970-01-01
              • 2020-10-24
              • 1970-01-01
              • 2016-09-21
              • 2014-04-17
              • 2012-11-21
              • 2014-09-11
              • 2021-08-18
              相关资源
              最近更新 更多