首页 文章

在Java中重写equals和hashCode时应该考虑哪些问题?

提问于
浏览
617

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

11 回答

  • 1383

    仍然惊讶于没有人推荐 Guava 库 .

    //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());
        }
    
  • 7

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

    如果在hashCode或equals测试中包含关系的两端,则可能会进入以StackOverflowException结尾的递归循环 .
    最简单的解决方案是不在方法中包含getChildren集合 .

  • 11

    对于equals,请查看 Secrets of Equals by Angelika Langer . 我非常爱它 . 关于 Generics in Java ,她也是一个很棒的常见问题解答 . 查看她的其他文章here(向下滚动到"Core Java"),她还继续使用Part-2和"mixed type comparison" . 玩得开心吧!

  • 6

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

    Lazy loaded objects are subclasses

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

    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 是唯一能正常运行的东西 .

    Lazy loaded objects have null-fields

    ORM通常使用getter来强制加载延迟加载的对象 . 这意味着如果 person 延迟加载, person.name 将为 null ,即使 person.getName() 强制加载并返回"John Doe" . 根据我的经验,这种情况在 hashCode()equals() 中更频繁地出现 .

    如果您正在处理ORM,请确保始终使用getter,并且永远不要在 hashCode()equals() 中进行字段引用 .

    Saving an object will change its state

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

    我经常使用的模式是

    if (this.getId() == null) {
        return this == other;
    }
    else {
        return this.getId().equals(other.getId());
    }
    

    但是:你不能在 hashCode() 中包含 getId() . 如果这样做,当对象被持久化时,它的 hashCode 会发生变化 . 如果对象在 HashSet 中,您将再次找到它 .

    在我的 Person 示例中,我可能会将 getName() 用于 hashCodegetId() 加上 getName() (仅用于偏执)用于 equals() . 如果对于 hashCode() 存在"collisions"的风险是可以的,但对于 equals() 永远没有问题 .

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

  • 31

    在检查成员相等性之前,有两种方法可以检查类的相等性,我认为两种方法在适当的情况下都很有用 .

    • 使用 instanceof 运算符 .

    • 使用 this.getClass().equals(that.getClass()) .

    我在 final equals实现中使用#1,或者在实现一个为equals规定算法的接口时(比如 java.util 集合接口 - 使用 (obj instanceof Set) 或任何接口检查的正确方法,当等于时,你通常是一个不好的选择)被覆盖,因为它破坏了对称属性 .

    选项#2允许安全地扩展类,而不会覆盖等于或破坏对称性 .

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

    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;
      }
    
    }
    
  • 26

    有关继承友好的实现,请查看Tal Cohen的解决方案,How Do I Correctly Implement the equals() Method?

    摘要:

    约书亚布洛赫在他的着作Effective Java Programming Language Guide(Addison-Wesley,2001)中声称"There is simply no way to extend an instantiable class and add an aspect while preserving the equals contract." Tal不同意 .

    他的解决方案是通过两种方式调用另一个非对称的blindlyEquals()来实现equals() . blindlyEquals()被子类覆盖,equals()被继承,并且永远不会被覆盖 .

    例:

    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 Substitution Principle,则equals()必须在继承层次结构中起作用 .

  • 82

    从逻辑上讲,我们有:

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

    但是 not 反之亦然!

  • 15

    理论(语言律师和数学倾向):

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

    hashCode()javadoc)也必须一致(如果对象未按 equals() 进行修改,则必须保持返回相同的值) .

    两种方法之间的 relation 是:

    每当a.equals(b),则a.hashCode()必须与b.hashCode()相同 .

    在实践中:

    如果你覆盖一个,那么你应该覆盖另一个 .

    使用相同的一组用于计算 equals() 以计算 hashCode() 的字段 .

    使用Apache Commons Lang库中的优秀辅助类EqualsBuilderHashCodeBuilder . 一个例子:

    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();
        }
    }
    

    还要记住:

    使用基于散列的CollectionMap(例如HashSetLinkedHashSetHashMapHashtableWeakHashMap)时,请确保在对象位于集合中时,放入集合的键对象的hashCode()永远不会更改 . 确保这一点的防弹方法是使您的密钥不可变,which has also other benefits .

  • 18

    超类中有两种方法,如java.lang.Object . 我们需要将它们覆盖到自定义对象 .

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

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

    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

    玩得开心! @ . @

  • 282

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

    这句话是 equals() 继承不友好的结果 . JLS(Java语言规范)指定如果 A.equals(B) == trueB.equals(A) 也必须返回 true . 如果省略该语句,继承覆盖 equals() (并更改其行为)的类将破坏此规范 .

    请考虑以下示例,省略语句时会发生什么:

    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)) 结果给出了真实,应该如此 .

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

    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 实例后检查:

    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;
    

    哪个会输出:

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

    其中,如果 a 不是 B 的引用,那么它可能是类 A 的引用(因为你扩展它),在这种情况下你调用 super.equals() too .

  • 43

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

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

    每当我们覆盖equals()方法时,我们应该总是覆盖hashCode()方法 . 如果没有,会发生什么?如果我们在应用程序中使用哈希表,它将不会按预期运行 . 由于hashCode用于确定存储的值的相等性,因此它不会为键返回正确的对应值 .

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

    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;
    
      }
    }
    

    示例代码输出:

    bengalTiger1 and bengalTiger2: true 
    bengalTiger1 and siberianTiger: false 
    bengalTiger1 hashCode: 1398212510 
    bengalTiger2 hashCode: 1398212510 
    siberianTiger hashCode: –1227465966
    

相关问题