Java HashSet 包含返回 true 的方法,当它应该是 false

问题描述 投票:0回答:3

我用

Triple
equals
方法定义了一个
hashcode
类。根据文档,Java HashSet 的
HashSet.contains()
方法必须返回与
Objects.equals()
相同的值。但是
contains()
Java HashSet 方法返回 true,当它假设为 false.

class Triple{

    private List<Integer> triple;

    public Triple(int one, int two, int three){
        this.triple = new ArrayList<>();
        this.triple.add(one);
        this.triple.add(two);
        this.triple.add(three);
    }

    public int hashCode(){
        return triple.get(0).hashCode() + triple.get(1).hashCode() + triple.get(2).hashCode();
    }

    public boolean equals(Object t){
        Triple triple = (Triple)t;
        return triple.triple.contains(this.triple.get(0))
                && triple.triple.contains(this.triple.get(1))
                && triple.triple.contains(this.triple.get(2));
    }
}
public class Main {
    public static void main(String[] args) {
        HashSet<Triple> answersSet = new HashSet<>();
        Triple triple1 = new Triple(-4,0,4);
        Triple triple2 = new Triple(0,0,0);
        answersSet.add(triple1);
        boolean b = answersSet.contains(triple2); //returns true
        b = Objects.equals(triple1,triple2); //but it returns false, 
    }
}
java hash hashmap
3个回答
3
投票

boolean b = answersSet.contains(triple2); //返回真

确实如此,因为您的

equals
方法以及您的
hashCode()
方法在此处产生相同的值。乍一看,它不像。但是,实际上,这就是您的代码所做的。让我们通过它:

哈希码

返回 triple.get(0).hashCode() + triple.get(1).hashCode() + triple.get(2).hashCode();

Integer
值的 hashCode 就是..本身。因此,对于
-4,0,4
,您最终会得到一个 0 的 hashCode。毕竟,-4 + 0 + +4 是 0.

0, 0, 0
的哈希码也是0.

这称为散列冲突。这并不意味着您对

hashCode()
的实施肯定是坏的;它发生了。通过简单的鸽巢原理:有2^32种不同的可能的哈希码,并且有2^96种不同的
Triple
实例;因此,必须至少有 2^64 个不同的
Triple
实例仍然具有相同的 hashCode。

这很好——规范中没有任何内容要求不同的对象具有不同的哈希码。规范只规定了两件事:

  • 如果 2 个对象相等,则它们 must 具有相同的 hashCode(您的代码是正确的)。
  • 如果许多不同的对象仍然哈希到相同的哈希码,那么 hashset 和 hashmap 和朋友的性能将非常糟糕。不过,它会起作用的。只是,不是很快。这称为“哈希分布”。

你的散列分布大部分都很好。通过考虑数字的位置,您可以做得更好。像这样的东西:

@Override public int hashCode() {
  return triple.get(0) + triple.get(1) * 31 + triple.get(2) * 97;

31 和 97 是质数。那是故意的。

等于

您的 equals 方法已损坏。它不关心秩序!它说相等意味着提供的

Triple
对象包含
this
三元组的每个值。

鉴于

this
三元组是 0/0/0,您检查传入的
triple
是否包含 0(确实如此)。然后它检查它是否包含 0(它..仍然包含),然后第三次检查它,它..它仍然包含..

这真的,真的坏了。它与 hashCode 冲突(0/0/0 和 5/0/4 是相等的,但没有相同的 hashCode,这是违规的),并且是一个奇怪的相等定义。它也是不可逆的,并且 equals 方法必须是:如果

a.equals(b)
为真,则
b.equals(a)
也必须为真,但是对于 0/0/0 和 -4/0/4 这也不是真的.

另外,如果您向它传递一个非三元组,您的 equals 方法将抛出一个

ClassCastException
,这也被破坏了。 equals 的文档描述了你必须做什么。你违反了它的 3 条规则。

让我们解决所有问题:

修复是检查每个位置

@Override public  boolean equals(Object other) {
  if (other == null || other.getClass() != Triple.class) return false;
  Triple t = (Triple) other;
  return t.triple.get(0).equals(this.triple.get(0)) &&
    t.triple.get(1).equals(this.triple.get(1)) &&
    t.triple.get(2).equals(this.triple.get(2));
}

或..

使用记录,或者,lombok 的

@Value
为您处理这一切:

public record Triple(int a, b, c) {}

@Value
public class Triple {
  List<Integer> triple;

  public Triple(int a, b, c) {
    this.triple = List.of(a, b, c);
  }
}

更一般地说,感觉这个类应该有 3 个

int
类型的字段,而不是一个
List<Integer>
类型的字段。


2
投票

你的

equals
坏了。您只测试该值是否包含在
List
中,而不是三个值是否相等。我想你想要

@Override
public boolean equals(Object o) {
    Triple t = (Triple) o;
    return triple.get(0).equals(t.triple.get(0))
            && triple.get(1).equals(t.triple.get(1))
            && triple.get(2).equals(t.triple.get(2));
}

哪个(显然)返回

false
与您的示例数据。此外,您可以简化构造函数

public Triple(int one, int two, int three) {
    this.triple = List.of(one, two, three);
}

如评论中所讨论,并在其他答案中指出;您最初的问题是由基于简单加法的哈希码引起的,其中所有三个值的总和相同。你的 equals 被打破了,因为

a
可以等于
b
b
等于
a
(因为
a
可能包含一个存在于
b
中的值,而
b
可能包含三个值只有一个他们出现在
a
)。最好将这三个值存储为
int
字段。您还应该使
Triple
不可变,因为更改任何字段都会改变实例的哈希码。您也可以覆盖
toString
并在您使用它时将其设为
Comparable
。所以最终版本可能看起来像,

class Triple implements Comparable<Triple> {
    private final int a, b, c;

    public Triple(int one, int two, int three) {
        this.a = one;
        this.b = two;
        this.c = three;
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(this.a, this.b, this.c);
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof Triple) {
            Triple that = (Triple) o;
            return this.a == that.a 
                    && this.b == that.b 
                    && this.c == that.c;
        }
        return false;
    }

    @Override
    public String toString() {
        return String.format("(%d, %d, %d)", a, b, c);
    }

    @Override
    public int compareTo(Triple that) {
        int comp = Integer.compare(this.a, that.a);
        if (comp != 0) {
            return comp;
        }
        comp = Integer.compare(this.b, that.b);
        if (comp != 0) {
            return comp;
        }
        return Integer.compare(this.c, that.c);
    }
}

1
投票

要使两个列表进行相等比较,每个列表必须具有

same size
并且
list1.get(i).equals(list2.get(i))
必须是
true
对于
0 <= i < List.size()

这很容易通过以下方式实现:

  • 使用模式匹配
    instanceof
    (Java 16+)如果为真,它将
    obj
    转换并分配给
    t
  • 只需使用等号比较列表,因为列表使用上述一对一比较覆盖等号。
public boolean equals(Object obj) {
    if (obj instanceof Triple t) {
        return t.triple.equals(this.triple);
    }
    return false;
}

此外,您可以按如下方式创建

hashCode

public int hashCode() {
        return Objects.hash(triple.get(0), triple.get(1), triple.get(2));
}
© www.soinside.com 2019 - 2024. All rights reserved.