Why should I not use equals with inheritance?

2019-02-03 11:26发布

When I read a Java book, author has said that, when designing a class, it's typically unsafe to use equals() with inheritance. For example:

public final class Date {
    public boolean equals(Object o) {
         // some code here
    }
}

In the class above, we should put final, so other class cannot inherit from this. And my question is, why it is unsafe when allow another class inherit from this?

标签: java oop equals
2条回答
可以哭但决不认输i
2楼-- · 2019-02-03 11:32

Martin Odersky (the guy behind generics in Java and the original codebase for the current javac) has a nice chapter in his book Programming in Scala addressing this problem. He suggests that adding a canEqual method can fix the equality/inheritance problem. You can read the discussion in the first edition of his book, which is available online:

Chapter 28 of Programming in Scala, First Edition: Object Equality

The book is of course referring to Scala, but the same ideas apply to classic Java. The sample source code shouldn't be too difficult for someone coming from a Java background to understand.

Edit:

It looks like Odersky published an article on the same concept in Java back in 2009, and it's available on the same website:

How to Write an Equality Method in Java

I really don't think trying to summarize the article in this answer would do it justice. It covers the topic of object equality in depth, from common mistakes made in equality implementations to a full discussion of Java equals as an equivalence relation. You should really just read it.

查看更多
倾城 Initia
3楼-- · 2019-02-03 11:51

Because it's hard (impossible?) to make it right, especially the symmetric property.

Say you have class Vehicle and class Car extends Vehicle. Vehicle.equals() yields true if the argument is also a Vehicle and has the same weight. If you want to implement Car.equals() it should yield true only if the argument is also a car, and except weight, it should also compare make, engine, etc.

Now imagine the following code:

Vehicle tank = new Vehicle();
Vehicle bus = new Car();
tank.equals(bus);  //can be true
bus.equals(tank);  //false

The first comparison might yield true if by coincidence tank and bus have the same weight. But since tank is not a car, comparing it to a car will always yield false.

You have few work-arounds:

  • strict: two objects are equal if and only if they have exactly the same type (and all properties are equal). This is bad, e.g. when you subclass barely to add some behaviour or decorate the original class. Some frameworks are subclassing your classes as well without you noticing (Hibernate, Spring AOP with CGLIB proxies...)

  • loose: two objects are equal if their types are "compatible" and they have same contents (semantically). E.g. two sets are equal if they contain the same elements, it doesn't matter that one is HashSet and the other is TreeSet (thanks @veer for pointing that out).

    This can be misleading. Take two LinkedHashSets (where insertion order matters as part of the contract). However since equals() only takes raw Set contract into account, the comparison yields true even for obviously different objects:

    Set<Integer> s1 = new LinkedHashSet<Integer>(Arrays.asList(1, 2, 3));
    Set<Integer> s2 = new LinkedHashSet<Integer>(Arrays.asList(3, 2, 1));
    System.out.println(s1.equals(s2));
    

See also

查看更多
登录 后发表回答