Java HashSet contains 方法返回 false,即使已经重写了 equals() 和 hashCode() 方法。

6

我这样初始化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。我做错了什么?
我在StackOverflow上找到了类似的问题,但大多数都涉及到equals()hashCode()方法未被正确覆盖。我认为我已经正确实现了这两个方法。

2
rT.equals(r) 是成立的吗? - JP Moresmau
1
你的测试使用相同的 Pool 实例,你确定 equals 在 Pool 上能正常工作吗? - JP Moresmau
2
你在equals方法中多加了一个条件this.getSrcTcp() == r.getSrcTcp(),但这个条件并不属于哈希码计算的一部分——也许这就是问题所在,哈希码相同但equals返回false。 - 6ton
2
如果您能发布一个简短但是完整的示例来演示问题,那将非常有帮助... - Jon Skeet
1
顺便说一句,一旦你解决了问题,你可能想要修改你实现的 hashCode() 方法。将字符串连接起来计算哈希码比必要的代价更高。在这里阅读其他想法:https://dev59.com/oHVD5IYBdhLWcg3wBm1g。 - sstan
显示剩余7条评论
3个回答

1
你的问题在于hashCode()equals()不一致。
你的hashCode()实现基于池的toString(),但是你的equals()使用了池类的.equals()
将你的.equals()更改为比较用于生成哈希码的字符串。

0

有一些可能性:

  • Rule 是可变的,在将规则添加到集合后,某些键(相对于哈希或等于)字段已更改;
  • 如果两个对象相等,则它们应该具有相同的 hashCode;
  • 错误,例如在 equals 中使用 == 而不是 equals

在这里,我猜测您有两个没有相等的池名称或池名称哈希码的Pool实例。


-1

您在equals中有一项额外的条件:this.getSrcTcp() == r.getSrcTcp(),而这不是哈希码的一部分 - 可能就是问题所在,哈希码相同,但是equals为false。请检查您正在比较的值中是否存在此字段的差异。

尽管有注释,我认为这不起作用的原因是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


2
你没有证明你认为你已经证明的东西。你的“equals”方法总是返回“false”,所以显然它永远不会打印“true”。一个更好的测试是取消注释“equals”中的所有代码,但使“hashCode()”返回0-因此对于所有实例都是相同的值。 - Jon Skeet
2
还应该阅读 Object.hashCode 的文档:“根据 equals(java.lang.Object) 方法,如果两个对象不相等,则不要求在每个对象上调用 hashCode 方法会产生不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。” - Jon Skeet
1
我试图表达的是两者需要一致。HashMap.getEntry会检查相等性,如下所示:if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) - 6ton
1
我觉得这很有趣,因为你既对又错。你认为equalshashCode都必须基于完全相同的字段计算是错误的(像Jon Skeet一样总是正确的)。但也许你的确是对的,不同的字段可能导致OP难以置信。但这并不是因为HashSet做错了什么,而是因为OP可能错误地假设具有相等的hashCodes会自动返回true的HashSet.Contains()检查。 - sstan
1
假设你有一个领域,在这个领域计算一个合理的哈希码可能非常昂贵,但相等性检查通常很便宜 - 并且实例很少仅因该字段而不同。在这种情况下,不将其包含在哈希码中是有意义的。 - Jon Skeet
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接