Gabriel
Gabriel

Reputation: 67

Java equals super and sub class

I have the following two very simple classes:

   public class A {

    private int a;

    public A(int a)
    {
    this.a=a;
    }
    public int getA(){
        return a;
    }
    public boolean equals(Object o)
    {
        if (!(o instanceof A))
            return false;
        A other = (A) o;
        return a == other.a;
    }
}

And its subclass:

public class B extends A{
    private int b;
    public B(int a, int b)
    {
        super(a);
        this.b = b;
    }
    public boolean equals(Object o)
    {
        if (!(o instanceof B))
            return false;
        B other = (B) o;
        return super.getA() == other.getA() && b == other.b;
    }
}

This might seem right initially, but in the following case it violates the symmetry principle of the general contract of the specification for Object, which states: "It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true." http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#equals(java.lang.Object) The failing case is the following:

public class EqualsTester {

    public static void main(String[] args) {
        A a = new A(1);
        B b = new B(1,2);
        System.out.println(a.equals(b));
        System.out.println(b.equals(a));
    }

}

The first returns true, while the second returns false. One solution that might seem right would be to use getClass() instead of instanceof. However, this would not be acceptable in cases where we look for B in collections. For example:

Set<A> set = new HashSet<A>();
set.add(new A(1));

The method set.contains(new B(1,2)); would return false. This example as is, might not be ideal to be logically visualized but imagine if A was a Vehicle class and B was a Car class, with field a being the number of wheels and field b being the number of doors. When we call the contains method we essentially ask: "Does our set contain a 4-wheel vehicle?" The answer should be yes since it does contain it, regardless of whether it's a car and the number of doors it has. The solution Joshua Block suggests in Effective Java 2nd Ed pg 40 is not to have B inherit from A and have an instance of A as a field in B instead:

public class B {
    private A a;
    private int b;
    public B(int a, int b)
    {
        this.a = new A(a);
        this.b = b;
    }

    public A getAsA()
    {
        return A;
    }  
    public boolean equals(Object o)
    {
        if (!(o instanceof B))
            return false;
        B other = (B) o;
        return a.getA() == other.getA() && b == other.b;
    }
}

However, does this mean that we lose the power to use inheritance in a great number of cases where it is needed? That is, when we have classes with extra properties that need to extend the more general ones to reuse the code and just add their extra more specific properties.

Upvotes: 3

Views: 596

Answers (1)

Dragan Bozanovic
Dragan Bozanovic

Reputation: 23562

Regardless of the collection issues, I would find it highly unintuitive and confusing for b.equals(a) to return true.

If you want to consider a and b equal in some context, then implement explicitly the equality logic for that context.

1) For example, to use As and Bs in a HashSet by relying on equality logic of A, implement an adapter that will contain A and implement the equals method:

class MyAdapter {
   private final A a;

   MyAdapter(A a) {
      this.a = a;
   }

   public boolean equals(Object o) {
        if (!(o instanceof MyAdapter)) {
            return false;
        }
        MyAdapter other = (MyAdapter) o;
        return a.getA() == other.a.getA();
    }
}

Then simply add adapter objects to the set:

Set<MyAdapter> set = new HashSet<>();
set.add(new MyAdapter(new A(1)));

Then set.contains(new MyAdapter(new B(1,2))); returns true.

Of course, you can write a wrapper class and pass it As (and Bs) directly, hiding MyAdapter from the client code (it can be private static class in the wrapper class) for better readability.

2) An option from the standard jdk libraries is to use TreeSet:

Note that the ordering maintained by a set (whether or not an explicit comparator is provided) must be consistent with equals if it is to correctly implement the Set interface. (See Comparable or Comparator for a precise definition of consistent with equals.) This is so because the Set interface is defined in terms of the equals operation, but a TreeSet instance performs all element comparisons using its compareTo (or compare) method, so two elements that are deemed equal by this method are, from the standpoint of the set, equal. The behavior of a set is well-defined even if its ordering is inconsistent with equals; it just fails to obey the general contract of the Set interface.

Because TreeSet does not rely on equals, just implement the proper comparator in the same fashion you implemented MyAdapter.equals (to return 0 if a1.getA() == a2.getA());

Upvotes: 1

Related Questions