关于重写:在重写Java中的等值和哈希代码时应该考虑哪些问题?

What issues should be considered when overriding equals and hashCode in Java?

当覆盖equalshashCode时,必须考虑哪些问题/陷阱?


理论(对于语言律师和数学倾向者而言):

equals()(javadoc)必须定义等价关系(它必须是自反的、对称的和可传递的)。此外,它必须是一致的(如果对象没有被修改,那么它必须保持返回相同的值)。此外,o.equals(null)必须始终返回false。

hashCode()(javadoc)也必须是一致的(如果对象没有根据equals()进行修改,它必须保持返回相同的值)。

这两种方法之间的关系是:

Whenever a.equals(b), then a.hashCode() must be same as b.hashCode().

在实践中:

如果您覆盖其中一个,那么您应该覆盖另一个。

使用与计算equals()相同的一组字段来计算hashCode()

使用来自ApacheCommonsLang库的优秀助手类EqualsBuilder和HashCodeBuilder。一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Person {
    private String name;
    private int age;
    // ...

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
            // if deriving: appendSuper(super.hashCode()).
            append(name).
            append(age).
            toHashCode();
    }

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

        Person rhs = (Person) obj;
        return new EqualsBuilder().
            // if deriving: appendSuper(super.equals(obj)).
            append(name, rhs.name).
            append(age, rhs.age).
            isEquals();
    }
}

还记得:

使用基于哈希的集合或映射(如哈希集、LinkedHashSet、HashMap、HashTable或WeakHashMap)时,请确保在对象位于集合中时,放入集合中的关键对象的HashCode()不会更改。确保这一点的防弹方法是使您的密钥不变,这也有其他好处。


如果您正在处理使用对象关系映射器(ORM)持久化的类(如Hibernate),那么有一些问题值得注意,如果您不认为这已经非常复杂了!

延迟加载的对象是子类

如果使用ORM持久化对象,在许多情况下,您将处理动态代理,以避免过早地从数据存储加载对象。这些代理作为您自己的类的子类实现。这意味着this.getClass() == o.getClass()将返回false。例如:

1
2
3
4
5
Person saved = new Person("John Doe");
Long key = dao.save(saved);
dao.flush();
Person retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass()); // Will return false if Person is loaded lazy

如果您正在处理一个ORM,那么使用o instanceof Person是唯一可以正常工作的方法。

延迟加载的对象具有空字段

窗体通常使用getter强制加载延迟加载的对象。这意味着,如果person是惰性加载,即使person.getName()强制加载并返回"john doe",person.name将是null。根据我的经验,这在hashCode()equals()中更常见。

如果您处理的是ORM,请确保始终使用getter,并且不要在hashCode()equals()中使用字段引用。

保存对象将更改其状态

持久对象通常使用id字段来保存对象的键。首次保存对象时,此字段将自动更新。不要在hashCode()中使用id字段。但是你可以在equals()中使用它。

我经常使用的模式是

1
2
3
4
5
6
if (this.getId() == null) {
    return this == other;
}
else {
    return this.getId().equals(other.getId());
}

但是:你不能把getId()包括在hashCode()中。如果这样做,当一个对象被持久化时,它的hashCode会发生变化。如果物体在HashSet中,你将"永远"再也找不到它。

在我的person例子中,我可能会用getName()来表示hashCodegetId()加上getName()来表示equals()。如果hashCode()存在"碰撞"的风险,那是可以的,但是对于equals()来说永远都不好。

hashCode()应使用equals()中属性的不变子集。


关于obj.getClass() != getClass()的澄清。

这一说法是equals()不友好继承的结果。JLS(Java语言规范)指定如果EDCOX1 OR 8"EDCOX1,9"也必须返回EDCOX1 OR 10。如果省略了继承重写equals()的类(并更改其行为)的语句,将破坏此规范。

请考虑以下省略语句时发生的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    class A {
      int field1;

      A(int field1) {
        this.field1 = field1;
      }

      public boolean equals(Object other) {
        return (other != null && other instanceof A && ((A) other).field1 == field1);
      }
    }

    class B extends A {
        int field2;

        B(int field1, int field2) {
            super(field1);
            this.field2 = field2;
        }

        public boolean equals(Object other) {
            return (other != null && other instanceof B && ((B)other).field2 == field2 && super.equals(other));
        }
    }

在执行new A(1).equals(new A(1))的同时,new B(1,1).equals(new B(1,1))的结果也应该是真实的。

这看起来很好,但是如果我们尝试使用这两个类,会发生什么:

1
2
3
4
A a = new A(1);
B b = new B(1,1);
a.equals(b) == true;
b.equals(a) == false;

显然,这是错误的。

如果要确保对称条件。a=b如果b=a和liskov替换原则不仅在B实例中调用super.equals(other),而且在A实例之后检查:

1
2
3
4
if (other instanceof B )
   return (other != null && ((B)other).field2 == field2 && super.equals(other));
if (other instanceof A) return super.equals(other);
   else return false;

将输出:

1
2
a.equals(b) == true;
b.equals(a) == true;

如果A不是B的引用,那么它可能是A类的引用(因为您扩展了它),在这种情况下,您也可以称为super.equals()


对于易于继承的实现,请查看Tal-Cohen的解决方案,如何正确实现equals()方法?

总结:

在他的书《有效Java编程语言指南》(Addison Wesley,2001)中,Joshua Bloch声称:"根本没有办法扩展一个可实例化的类,并且在保存等值契约的同时添加一个方面。"TAL不同意。

他的解决方案是通过双向调用另一个非对称的blindlyequals()来实现equals()。BlindlyEquals()被子类重写,Equals()被继承,并且从不重写。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Point {
    private int x;
    private int y;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return (p.x == this.x && p.y == this.y);
    }
    public boolean equals(Object o) {
        return (this.blindlyEquals(o) && o.blindlyEquals(this));
    }
}

class ColorPoint extends Point {
    private Color c;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint)o;
        return (super.blindlyEquals(cp) &&
        cp.color == this.color);
    }
}

注意,如果要满足Liskov替换原则,equals()必须跨继承层次结构工作。


仍然令人惊讶的是,没有人为此推荐了番石榴图书馆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 //Sample taken from a current working project of mine just to illustrate the idea

    @Override
    public int hashCode(){
        return Objects.hashCode(this.getDate(), this.datePattern);
    }

    @Override
    public boolean equals(Object obj){
        if ( ! obj instanceof DateAndPattern ) {
            return false;
        }
        return Objects.equal(((DateAndPattern)obj).getDate(), this.getDate())
                && Objects.equal(((DateAndPattern)obj).getDate(), this.getDatePattern());
    }


在超级类中有两个方法,即java.lang.object。我们需要将它们重写为自定义对象。

1
2
public boolean equals(Object obj)
public int hashCode()

相等的对象必须产生相同的哈希代码,只要它们相等,但是不相等的对象不需要产生不同的哈希代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Test
{
    private int num;
    private String data;
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if((obj == null) || (obj.getClass() != this.getClass()))
            return false;
        // object must be Test at this point
        Test test = (Test)obj;
        return num == test.num &&
        (data == test.data || (data != null && data.equals(test.data)));
    }

    public int hashCode()
    {
        int hash = 7;
        hash = 31 * hash + num;
        hash = 31 * hash + (null == data ? 0 : data.hashCode());
        return hash;
    }

    // other methods
}

如果您想了解更多信息,请查看以下链接:http://www.javaranch.com/journal/2002/10/equalhash.html

这是另一个例子,http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareto-java-method.html

玩得高兴!@


在检查成员相等之前,有几种方法可以检查类相等性,我认为这两种方法在正确的情况下都有用。

  • 使用instanceof运算符。
  • 使用this.getClass().equals(that.getClass())
  • 我在final等价实现中使用1,或者在实现一个为equals指定算法的接口时使用1(如java.util集合接口,这是检查(obj instanceof Set)或您正在实现的任何接口的正确方法)。当equals可以被重写时,这通常是一个错误的选择,因为这会破坏对称性。

    选项2允许安全地扩展类,而不覆盖等号或破坏对称性。

    如果您的类也是Comparable,那么equalscompareTo方法也应该是一致的。这是Comparable类中equals方法的模板:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    final class MyClass implements Comparable<MyClass>
    {

      …

      @Override
      public boolean equals(Object obj)
      {
        /* If compareTo and equals aren't final, we should check with getClass instead. */
        if (!(obj instanceof MyClass))
          return false;
        return compareTo((MyClass) obj) == 0;
      }

    }


    对于平等者,请看安吉丽卡·兰格的《平等的秘密》。我非常喜欢它。她也是Java泛型的一个很好的常见问题。查看她的其他文章(向下滚动到"核心Java"),在那里她还继续进行第二部分和"混合类型比较"。阅读它们很有趣!


    equals()方法用于确定两个对象的相等性。

    因为int值10总是等于10。但这个equals()方法是关于两个对象的相等性。当我们说对象时,它将具有属性。为了确定相等性,需要考虑这些属性。不必考虑所有属性来确定相等性,并且可以根据类定义和上下文来确定相等性。然后可以重写equals()方法。

    每当重写equals()方法时,我们都应该重写hashcode()方法。如果没有,会发生什么?如果我们在应用程序中使用哈希表,它将不会像预期的那样工作。由于哈希代码用于确定存储值的相等性,因此它不会返回键的正确对应值。

    给定的默认实现是object类中的hashcode()方法,它使用对象的内部地址,并将其转换为整数并返回它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    public class Tiger {
      private String color;
      private String stripePattern;
      private int height;

      @Override
      public boolean equals(Object object) {
        boolean result = false;
        if (object == null || object.getClass() != getClass()) {
          result = false;
        } else {
          Tiger tiger = (Tiger) object;
          if (this.color == tiger.getColor()
              && this.stripePattern == tiger.getStripePattern()) {
            result = true;
          }
        }
        return result;
      }

      // just omitted null checks
      @Override
      public int hashCode() {
        int hash = 3;
        hash = 7 * hash + this.color.hashCode();
        hash = 7 * hash + this.stripePattern.hashCode();
        return hash;
      }

      public static void main(String args[]) {
        Tiger bengalTiger1 = new Tiger("Yellow","Dense", 3);
        Tiger bengalTiger2 = new Tiger("Yellow","Dense", 2);
        Tiger siberianTiger = new Tiger("White","Sparse", 4);
        System.out.println("bengalTiger1 and bengalTiger2:"
            + bengalTiger1.equals(bengalTiger2));
        System.out.println("bengalTiger1 and siberianTiger:"
            + bengalTiger1.equals(siberianTiger));

        System.out.println("bengalTiger1 hashCode:" + bengalTiger1.hashCode());
        System.out.println("bengalTiger2 hashCode:" + bengalTiger2.hashCode());
        System.out.println("siberianTiger hashCode:"
            + siberianTiger.hashCode());
      }

      public String getColor() {
        return color;
      }

      public String getStripePattern() {
        return stripePattern;
      }

      public Tiger(String color, String stripePattern, int height) {
        this.color = color;
        this.stripePattern = stripePattern;
        this.height = height;

      }
    }

    代码输出示例:

    1
    2
    3
    4
    5
    bengalTiger1 and bengalTiger2: true
    bengalTiger1 and siberianTiger: false
    bengalTiger1 hashCode: 1398212510
    bengalTiger2 hashCode: 1398212510
    siberianTiger hashCode:1227465966

    逻辑上我们有:

    a.getClass().equals(b.getClass()) && a.equals(b)号?a.hashCode() == b.hashCode()

    但反之亦然!


    我发现的一个问题是,两个对象包含彼此的引用(一个例子是父/子关系,父对象上有一个方便的方法来获取所有子对象)。例如,在进行Hibernate映射时,这类事情相当常见。

    如果将关系的两端都包含在哈希代码或等于测试中,则可以进入以StackOverflowException结尾的递归循环。最简单的解决方案是不要在方法中包含getchildren集合。