Reputation: 67
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
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 A
s and B
s 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 A
s (and B
s) 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 theSet
interface. (SeeComparable
orComparator
for a precise definition of consistent withequals
.) This is so because theSet
interface is defined in terms of theequals
operation, but aTreeSet
instance performs all element comparisons using itscompareTo
(orcompare
) 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