Java HashSet 包含返回 false,即使重写了 equals() 和 hashCode()

Posted

技术标签:

【中文标题】Java HashSet 包含返回 false,即使重写了 equals() 和 hashCode()【英文标题】:Java HashSet contains returns false, even with overridden equals() and hashCode() 【发布时间】:2015-09-18 17:10:43 【问题描述】:

我这样初始化 HashSet:

private HashSet<Rule> ruleTable = new HashSet<Rule>();

我的TcpRule 对象(抽象类Rule 的子类)的equals()hashCode() 方法如下所示:

@Override
public int hashCode() 
    // Ignore source Port for now
    return (this.getSrcPool() + ":" + this.getDstPool() + ":" + this.getProtocol() + ":" + this.dstTcp).hashCode();


@Override
public boolean equals(Object obj) 
    if (!(obj instanceof TcpRule))
        return false;
    if (obj == this)
        return true;

    TcpRule r = (TcpRule) obj;
    return (this.getSrcPool().equals(r.getSrcPool()) && this.getDstPool().equals(r.getDstPool()) && this.getProtocol().equals(r.getProtocol()) && this.getSrcTcp() == r.getSrcTcp() && this.getDstTcp() == r.getDstTcp());

我什至写了一个简单的单元测试,没有任何错误:

@Test
public void equalsTest() 
    Pool srcPool = new Pool("PROXY");
    Pool dstPool = new Pool("WEB");
    int srcTcp = 54321;
    int dstTcp = 80;

    TcpRule r1 = new TcpRule(srcPool, dstPool, srcTcp, dstTcp);
    TcpRule r2 = r1;
    assert r1.equals(r2);

    TcpRule r3 = new TcpRule(srcPool, dstPool, srcTcp, dstTcp);
    TcpRule r4 = new TcpRule(srcPool, dstPool, srcTcp, dstTcp);
    assert r3.equals(r4);


@Test
public void hashCodeTest() 
    Pool srcPool = new Pool("PROXY");
    Pool dstPool = new Pool("WEB");
    int srcTcp = 54321;
    int dstTcp = 80;

    TcpRule r1 = new TcpRule(srcPool, dstPool, srcTcp, dstTcp);
    TcpRule r2 = new TcpRule(srcPool, dstPool, srcTcp, dstTcp);
    assert r1.hashCode() == r2.hashCode();

    HashSet<Rule> rules = new HashSet<Rule>();
    rules.add(r1);
    assert rules.contains(r1);

    assert rules.contains(r2);

在我的应用程序中,我有一个add() 方法,我只需将Rule 对象添加到HashSet

@Override
public void add(Rule rule) 
    ruleTable.add(rule);

在另一种方法中,我检查HashSet中是否存在规则:

    @Override
public boolean isPermittedTcp(IpAddress sourceAddress, IpAddress destinationAddress, short srcTcp, short dstTcp) 
    Pool sourcePool = poolService.getPool(new Host(sourceAddress));
    Pool destinationPool = poolService.getPool(new Host(destinationAddress));
    Rule r = new TcpRule(sourcePool, destinationPool, srcTcp, dstTcp);
    log.info("Checking: " + r.toString());
    log.info("Hash-Code: " + r.hashCode());
    log.info("Hashes in ruleTable:");
    for(Rule rT : ruleTable) 
        log.info("" + rT.hashCode());
    
    if(ruleTable.contains(r)) 
        log.info("Hash found!");
     else 
        log.info("Hash not found!");
    
    return ruleTable.contains(r);

日志消息表明Rule 对象(r.hashCode())的哈希是-1313430269,而HashSet(循环中的rT.hashCode())中的一个哈希也是-1313430269。 但是ruleTable.contains(r) 总是返回false。我做错了什么?

我在 *** 上发现了类似的问题,但这些问题主要涉及 equals()hashCode() 方法未被(正确)覆盖。我想我已经正确地实现了这两种方法。

【问题讨论】:

还有 rT.equals(r) 吗? 您的测试使用了相同的 Pool 实例,您确定 equals 在 Pool 上运行良好吗? 你在 equals this.getSrcTcp() == r.getSrcTcp() 中有一个额外的条件,它不是哈希码的一部分 - 也许这就是问题所在,哈希码是相同的,但 equals 是假的跨度> 如果您发布一个简短但完整的示例来演示问题,那将非常有帮助... 顺便说一句,一旦你弄清楚你的问题,你可能想修改你如何实现你的 hashCode() 方法。连接字符串来计算哈希码比它需要的更昂贵。阅读here 了解其他想法。 【参考方案1】:

有一些可能性:

Rule 是可变的,在向集合添加规则后,某些键(w.r.t. hash 或 equals)字段已更改; 如果两个对象相等,它们应该具有相同的 hashCode; 错误,就像在equals 中使用== i.o 进行比较。 equals

我猜你有两个 Pool 实例,在池名称上没有等于或在池名称上没有 hashCode。

【讨论】:

【参考方案2】:

您的问题是 hashCode()equals() 确实同意。

您的hashCode() 实现基于池的toString(),但您的equals() 使用池类的.equals()

更改您的.equals() 以比较用于生成哈希码的字符串。

【讨论】:

【参考方案3】:

您在 equals this.getSrcTcp() == r.getSrcTcp() 中有一个额外的条件,它不是哈希码的一部分 - 也许这就是问题所在,哈希码是相同的,但 equals 是错误的。检查此字段在您比较的值中是否不同。

尽管有 cmets,但我认为这不起作用的原因是 equals 和 hashCode 实现不使用相同的字段。

模拟问题的代码:

import java.util.HashSet;

/**
 * @author u332046
 *
 */
public class HashCodeCollisionTest 
    public static class KeyDemo 
        String id;

        @Override
        public int hashCode() 
            final int prime = 31;
            int result = 1;
            result = prime * result + ((id == null) ? 0 : id.hashCode());
            return result;
        

        @Override
        public boolean equals(Object obj) 
            /*if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            KeyDemo other = (KeyDemo) obj;
            if (id == null) 
                if (other.id != null)
                    return false;
             else if (!id.equals(other.id))
                return false;
            return true;*/
            return false;
        

        public KeyDemo(String id) 
            super();
            this.id = id;
        
    

    static HashSet<KeyDemo> set = new HashSet<>();

    public static void main(String[] args) 
        set.add(new KeyDemo("hi"));
        set.add(new KeyDemo("hello"));

        System.out.println(set.contains(new KeyDemo("hi")));
    

这会打印出false。取消注释等于代码并打印true

【讨论】:

你还没有证明你认为你已经证明了。您的equals 方法总是 返回false,所以显然它永远不会打印“true”。更好的测试是取消注释 equals 中的所有代码,但让 hashCode() 返回 0 - 所以 all 实例的值相同。 另请阅读Object.hashCode 的文档:“根据equals(java.lang.Object) 方法,如果两个对象不相等,则要求,然后调用两个对象中每一个的 hashCode 方法必须产生不同的整数结果。但是,程序员应该意识到,为不相等的对象产生不同的整数结果可能会提高哈希表的性能。" 我试图说明两者必须一致。 HashMap.getEntry 确实检查相等 if (e.hash == hash &amp;&amp; ((k = e.key) == key || (key != null &amp;&amp; key.equals(k)))) 我觉得这很有趣,因为你是对的,也是错的。你错了,equalshashCode 都需要根据完全相同的字段列表进行计算(@Jon Skeet 像往常一样 100% 正确)。然而,很可能你是对的,不同的领域是导致 OP 难以置信地挠头的原因。但这不会是因为HashSet 做错了什么。这可能是因为 OP 做出了错误的假设,即具有相等的 hashCodes 自动意味着 HashSet.Contains() 检查将返回 true。 @6ton:假设您有一个领域,其中计算一个像样的哈希码可能非常昂贵,但相等检查通常很便宜 - 并且实例很少不同场地。在这种情况下,不将其包含在哈希码中是有意义的。

以上是关于Java HashSet 包含返回 false,即使重写了 equals() 和 hashCode()的主要内容,如果未能解决你的问题,请参考以下文章

java学习第17天(TreeSet HashSet)

java中hashset和hashmap有啥特点?

如何在 java hashset 中查找并返回对象

元素存在但 `Set.contains(element)` 返回 false

java中hashset和hashmap 有啥特点。

HashSet,TreeSet和LinkedHashSet的区别