我遇到了在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哈希码已更改。
非常微妙 - 我多次看了这个,并没有注意到缺乏不变性。
答案 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
抱怨。我不认为这是一个长期解决方案,但可能会为您提供有关hashCode
,equals
和equalsData
方法的一些信息
答案 6 :(得分:1)
几乎可以肯定的是,哈希码与“equals()”的旧数据和新数据不匹配。我之前遇到过这种情况,你最终会为每个对象和字符串表示法喷出哈希码,并试图找出不匹配的原因。
如果您要比较数据库前/后的项目,有时会丢失可能导致哈希码更改的纳秒(取决于您的数据库列类型)。
答案 7 :(得分:0)
Java HashSet在“ remove()”方法中存在问题。检查下面的链接。我切换到TreeSet,它工作正常。但是我需要O(1)时间复杂度。
答案 8 :(得分:-2)
如果有两个条目具有相同的数据,则只替换其中一个...您是否考虑到了这一点?为了以防万一,你有没有尝试过另一个不使用哈希码的集合数据结构,比如List?
答案 9 :(得分:-4)
我不能快速掌握我的Java,但是我知道当你在.NET中迭代那个集合时你无法从集合中删除一个项目,尽管如果它捕获了这个,它将抛出异常。这可能是问题吗?