简体   繁体   中英

Should 2 Java objects of different classes ever be equal?

I'm trying to write some generic code to define class equality and hashcodes based on a list of fields. When writing my equals method, I was wondering if, based on Java convention, it should ever be possible for two object of different to be equal. Let me give some examples;

class A {
  int foo;
}
class B {
  int foo;
}
class C extends A {
  int bar;
}
class D extends A {
  void doStuff() { }
}
...
A a = new A(); a.foo = 1;
B b = new B(); b.foo = 1;
C c = new C(); c.foo = 1; c.bar = 2;
D d = new D(); d.foo = 1;

a.equals(b); //Should return false, obviously
a.equals(c);
c.equals(a); //These two must be the same result, so I'd assume it must be false, since c cant possible equal a
a.equals(d); //Now this one is where I'm stuck. 

I see no reason that in the last example the two shouldn't be equal, but they do have different classes. Anyone know what convention dictates? And if they would be equal, how should an equals method handle that?

Edit: if anyone's interested in the code behind this question, see: https://gist.github.com/thomaswp/5816085 It's a little dirty but I'd welcome comments on the gist.

They could be, but it's typically very difficult to maintain the symmetric and transitive properties of equality in that case. At least while having a useful/intuitive definition of equality.

If you allow a subclass to consider itself equal to an instance of the superclass, then the superclass needs to consider itself equal to an instance of the subclass. Which means that you'll be encoding specific knowledge about the subclass (all possible subclasses?) in the superclass, and downcasting as needed, which isn't very clean.

Or, you do the comparison purely with fields contained in A , and don't override equals() at all. This fixes the above, but has the problem that two instances of C with different values of bar would be considered equal, which is probably not what you want.

Or, you override in C , and compare bar if the other object is an instance of C , but otherwise don't for an instance of A , you have another problem. c1.equals(c2) would be false, but c1.equals(a) would be true, as would c2.equals(a) and so a.equals(c2) . This breaks transitivity (since c1 == a and a == c2 implies c1 == c2 ).


In summary, it's theoretically possible but you would have to cripple your equals implementation to do so. And besides, the runtime class is a property of an object just as much as bar is, so I'd expect objects with different concrete classes to be not equal to each other anyway .

First note: when you override .equals() , you absolutely MUST override .hashCode() as well, and obey the defined contract . This is no joke. If you do not obey THAT, you are doomed to encounter problems.

As to handling equality between different classes inheriting one another, if all these classes have a common member, this can be done as such:

@Override
public int hashCode()
{
    return commonMember.hashCode();
}

@Override
public boolean equals(final Object o)
{
    if (o == null)
        return false;
    if (this == o)
        return true;
    if (!(o instanceof BaseClass))
        return false;
    final BaseClass other = (BaseClass) o;
    return commonMember.equals(other.commonMember); // etc -- to be completed
}

Object.equals() is required to be reflexive, symmetric, transitive, consistent across multiple invocations, and x.equals(null) must be false. There are no further requirements beyond that.

If equals() for a class you define does all of those things, then it's an acceptable equals() method. There is no answer to the question of how fine-grained it should be other than the one you provide yourself. You need to ask yourself: Which objects to I want to be equal?

Note, however, that you should have a good reason for making a.equals(b) true when a and b are instances of different classes, as that can make it tricky to implement a correct equals() in both classes.

Also remember that you need to follow these rules in order to correctly implement the equals method.

  1. Reflexive : Object must be equal to itself.
  2. Symmetric : if a.equals(b) is true then b.equals(a) must be true.
  3. Transitive : if a.equals(b) is true and b.equals(c) is true then c.equals(a) must be true.
  4. Consistent : multiple invocation of equals() method must result same value until any of properties are modified. So if two objects are equals in Java they will remain equals until any of there property is modified.
  5. Null comparison : comparing any object to null must be false and should not result in NullPointerException. For example a.equals(null) must be false, passing unknown object, which could be null, to equals in Java is is actually a Java coding best practice to avoid NullPointerException in Java.

As Andrzej Doyle rightly said, it becomes difficult to implement the Symetric and Transitive property when it's spread across multiple classes.

This question seems to me to indicate a muddy architecture. In theory, if you want to implement .equals such that you compare only specific members of the two instances you can do this, but whether this is a good idea really depends on just what purpose these classes are intended to serve (and even then I think there are better approaches).

Are these objects, more or less, just intended to be nothing more than bags of data? If so, perhaps you should create a separate comparison class that determines whether the two objects are "equivalent enough" for the purposes you need, rather than force the objects themselves to care about some alien, unrelated class. I'd be concerned if my code were concerning itself with potentially unrelated objects just because I thought it might be a good idea for them to know about each other due to temporary convenience. Also, as Andrzej mentioned, it's very problematic for a parent class to know or care about specific implementation details of derived classes. I've seen first-hand how this causes problems both subtle and egregious.

Are the objects "doers" rather than data storage? Since your subclass D implements a method then this indicates that it's more than just a bag of data... and in this case, philosophically, I can't see how it would be a good idea to consider A and D equal based merely on a set of value fields. Compare the fields, yes. Consider them equal or equivalent? No. This sounds like a maintainability nightmare in the long haul.

Here's an example of what I think would be a better idea:

class A implements IFoo{

    private int foo;
    public int getFoo(){ return foo; }

}

class B implements IFoo{

    private int foo;
    public int getFoo(){ return foo; }

}

class CompareFoos{
    public static boolean isEquivalent(IFoo a, IFoo b){
        // compare here as needed and return result.
    }
}

IFoo a = new A();
IFoo b = new B();
boolean result = CompareFoos.isEquivalent(a, b);

To some extent it depends on what you want the code to do. As long as you are clear what the equals method will compare then I don't see a problem necessarily. I think the problem really comes when you start making lots of sub-classes (eg Class E). There's a danger than that one of the sub-classes won't follow the contract so you could end up with

a.equals(e) --> true
e.equals(a) --> false

which would result in strange behaviour.

Personally I try to avoid equals comparing two different classes and returning true but I have done it a couple of times where the whole class hierarchy was under my control and final.

Consider this example -

abstract class Quadrilateral{
    Quadrilateral(int l, int w){
        length=l;
        width=w;
    }

    private int length, width;
}

class Rectangle extends Quadrilateral{
    Rectangle(int l, int w){super(l,w);}
}

class Square extends Quadrilateral{
    Square(int l){super(l, l);}
}

Square s = new Square(3);
Rectangle r = new Rectangle(3,3);

r.equals(s);//This should be true because the rectangle and square are logically the same.

So yes, there are cases where two different classes can be equal. Though clearly this is not common.

No.

Set<Parent> p = new HashSet<>();
p.insert(new Parent(1));
p.insert(new Child(1));

Supposing those two instances are equals , does p contain a Child ? Very unclear. More fun:

class Parent {
    public int foo = 0;
    void foo() {
        foo = 1;
    }
}

class Child extends Parent {
    @Override
    void foo() {
        foo = 2;
    }
}

Set<Parent> set = new HashSet<>();

for(Parent parent : set) {
    parent.foo();
    System.out.println(parent.foo); // 1 or 2?
}

I challenge you to know what p 's element contains without spending more than 1 minute on the Javadoc pages for Set , HashSet , or equals .

The short answer is that in very limited cases (eg Integer and Long ) it is OK for two objects of different classes to be equal, but it is so hard to pull off correctly that it is generally discouraged. It's hard enough to ensure that you create an equals() method that is Symmetric , Reflexive , and Transitive , but on top of that you should also:

  • create a hashCode() method that is consistent with equals()
  • create a compareTo() method that is consistent with equals()
  • further ensure that the compareTo() method is well behaved

Failure to do that can cause problems with using the objects in Sets, Trees, Hashes, and Sorted Lists.

There's a good article on the the topic of equals() based on part of Josh Bloch's book Effective Java but even that only covers equals() . The book goes into much greater detail, especially about how the problem becomes serious quickly once you start using the objects in collections.

It depends what you need, as @andrzeg said.

One additional thing. Do you want a.equals(d) to be true, but d.equals(a) to be false? when coding you will probably be using instanceof which may or may not be what you want.

I think that this question can be reduced to whether to use instanceof or getClass() in equals() implementation.

If D derives A and your inheritance hierarchy is right then indeed D IS A. It is logical to treat D as any other A and therefore you should be able to check D for equality as you would any other A.

Here is a link where Josh Bloch explains why he favors the instanceof approach.

http://www.artima.com/intv/bloch17.html

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM