Why is it considered bad practice to define a covariant compareTo method? - java

Here's an example from my code:
Baseclass:
abstract class AbstractBase implements Comparable<AbstractBase> {
private int a;
private int b;
public int compareTo(AbstractBase other) {
// compare using a and b
}
}
Implementation:
class Impl extends AbstractBase {
private int c;
public int compareTo(Impl other) {
// compare using a, b and c with c having higher impact than b in AbstractBase
}
FindBugs reports this as an issue. But why is that? What could happen?
And how would I correctly implement a solution?

Impl#compareTo(Impl) is not overriding AbstractBase#compareTo(AbstractBase) since they don't have the same signature. In other words, it won't be called when using Collections#sort for example.

EDIT: Added solution without casting
If you don't want to cast you could try the following.
Alter your baseclass to:
abstract class AbstractBase<T extends AbstractBase<?>> implements Comparable<T> {
//...
public int compareTo(T other) {
//...
}
}
And you Impl class to:
class Impl extends AbstractBase<Impl> {
//...
#Override
public int compareTo(Impl other) {
//...
}
}
Solution with casting:
A possible solution would be to override the compareTo(AbstractBase) method in the Impl class and explicitly check if an instance of Impl is passed in:
class Impl extends AbstractBase {
//...
#Override
public int compareTo(AbstractBase other) {
if (other instanceof Impl) {
int compC = Integer.compare(c, ((Impl) other).c);
if (compC == 0) {
return super.compareTo(other);
}
return compC;
}
return super.compareTo(other);
}
}

The following is something that I tried. Not exactly sure this is the reason why findbugs gives the error.
See the following code with a hypothetical implementation of the compareTo method.
Comparing the same objects results in different outputs.
public class Main
{
public static void main(String[] args)
{
Impl implAssignedToImpl = new Impl(1, 2, 3);
Impl otherImpl = new Impl(3, 2, 1);
System.out.println(implAssignedToImpl.compareTo(otherImpl)); // prints -2
AbstractBase implAssignedToAbstract = implAssignedToImpl;
System.out.println(implAssignedToAbstract.compareTo(otherImpl)); //prints 0
}
}
class AbstractBase implements Comparable<AbstractBase>
{
private int a;
private int b;
public AbstractBase(int a, int b)
{
super();
this.a = a;
this.b = b;
}
public int compareTo(AbstractBase other)
{
return (a + b) - (other.a + other.b);
}
}
class Impl extends AbstractBase
{
private int c;
public Impl(int a, int b, int c)
{
super(a, b);
this.c = c;
}
public int compareTo(Impl other)
{
return super.compareTo(other) + (c - other.c);
}
}
Building on my hypothetical compareTo, following seems to be a good solution. You can try to have a method similar to getSum which gives the object instance a value.
public class Main
{
public static void main(String[] args)
{
Impl implAssignedToImpl = new Impl(1, 2, 3);
Impl otherImpl = new Impl(3, 2, 1);
System.out.println(implAssignedToImpl.compareTo(otherImpl)); // prints 0
AbstractBase implAssignedToAbstract = implAssignedToImpl;
System.out.println(implAssignedToAbstract.compareTo(otherImpl)); //prints 0
}
}
class AbstractBase implements Comparable<AbstractBase>
{
private int a;
private int b;
public AbstractBase(int a, int b)
{
super();
this.a = a;
this.b = b;
}
public int compareTo(AbstractBase other)
{
return getSum() - other.getSum();
}
public int getSum()
{
return a + b;
}
}
class Impl extends AbstractBase
{
private int c;
public Impl(int a, int b, int c)
{
super(a, b);
this.c = c;
}
#Override
public int getSum()
{
return super.getSum() + c;
}
}

As sp00m said, your Impl#compareTo(Impl) has a different signature than AbstractBase#compareTo(AbstractBase), so it's not overloading it.
The key point is in understanding why it doesn't work, even when you try to sort() comparing with another Impl, where the more specific signature do matches.
As you defined Comparable<AbstractBase>, you need to define how your
instances compareTo AbstractBase instances. And so you need to implement compareTo(AbstractBase).
You can think that, being Impl a subtype of AbstractBase, the more specific method would be used when a comparison between two Impls takes place. The problem is Java has static binding, and so the compiler defines at compile time which method would use for solving each method call. If what you were doing was sorting AbstractBases, then the compiler would use the compareTo(AbstractBase), that is the one AbstractBase's interface define when it implements the Comparable(AbstractBase) interface.
You can make Impl implement the Comparable<Impl> interface for using the compareTo(Impl) method, but that would only work if you explicitly sort things that are known to be Impls at compile time (ie, an Impl object or Collection<Impl>).
If you really want to apply a different comparison whenever your two objects are Impls, you should fall to some kind of double-dispatch in your Impl#compareTo(AbstractBase) like:
Impl >>>
int compareTo(AbstractBase other) {
return other.compareToImpl(this);
}
int compareToImpl(Impl other) {
// perform custom comparison between Impl's
}
AbstractBase >>>
int compareTo(AbstractBase other) {
// generic comparison
}
int compareToImpl(Impl other) {
// comparison between an AbstractBase and an Impl.
//Probably could just "return this.compareTo(other);", but check for loops :)
}
This requires you add some Impl information in your AbstractBase, which is not pretty, though, but solves the problem the more elegant way it could - using reflection for this is not elegant at all.

The Liskov substitution principle (http://en.wikipedia.org/wiki/Liskov_substitution_principle) states: if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.)
In your case, you are overriding the compareTo method from the Base class in a way that breaks the behaviour of the original method. This is probably why FindBugs has an issue with it.
If you want to be proper about it:
abstract class AbstractBase {
}
class Impl1 extends AbstractBase implements Comparable<Impl1> ...
class Impl2 extends AbstractBase implements Comparable<Impl2> ...
OR
even better, do not use the Comparable interface at all - use a Comparator at sort time instead.
However, in real life there are situations where you need to get around it (maybe you don't have access to the source of AbstractBase, or maybe your new class is just a POC). In these special cases, I would go with the "ugly" cast solution proposed by John.

Related

Using Comparable interface to prohibit comparison between inherited types

I am going through the "Java Generics and Collections" book by Naftalin. I want to check my understanding regarding a point that they make in Chapter 3.
They explain about how the Comparable interface allows us to control what types we can compare with one another. In the below code example that they provide, they say that it is not possible to compare Apple with Orange. To be precise, this is what they say:
Since Apple implements Comparable<Apple>, it is clear that you can compare apples with apples, but not with oranges.
But as you can see, on the last line of the main method, we are able to do so, because the compareTo from the base class ends up being called. So I presume the solution to this would be to duplicate the implementation of the compareTo method by moving it to each of the Apple and Orange classes? And removing it from the base Fruit class? Or is there a better way to do this?
abstract class Fruit {
protected String name;
protected int size;
protected Fruit(String name, int size) {
this.name = checkNotNull(name);
this.size = size;
}
#Override
public boolean equals(Object o) {
if (o instanceof Fruit) {
Fruit that = (Fruit) o;
return this.name.equals(that.name) && this.size == that.size;
}
return false;
}
#Override
public int hashCode() {
return name.hashCode() * 29 + size;
}
protected int compareTo(#Nullable Fruit that) {
if (this.size > that.size) {
return 1;
} else if (this.size < that.size) {
return -1;
} else return 0;
}
}
}
class Apple extends Fruit implements Comparable<Apple> {
public Apple(int size) {
super("Apple", size);
}
#Override
public int compareTo(Apple apple) {
return super.compareTo(apple);
}
}
class Orange extends Fruit implements Comparable<Orange> {
protected Orange(int size) {
super("Orange", size);
}
#Override
public int compareTo(Orange orange) {
return super.compareTo(orange);
}
}
class TestFruit {
public static void main(String[] args) {
Apple apple1 = new Apple(1);
Apple apple2 = new Apple(2);
Orange orange1 = new Orange(1);
Orange orange2 = new Orange(2);
List<Apple> apples = List.of(apple1, apple2);
List<Orange> oranges = List.of(orange1, orange2);
// Both of these work as expected
System.out.println(Collections.max(apples)); // Apple{name=Apple, size=2}
System.out.println(Collections.max(oranges)); // Orange{name=Orange, size=2}
// But now, as expected, when you try to create a mixed list, it will no longer work
List<Fruit> mixed = List.of(apple1, orange2);
// Compile error on the below line
System.out.println(Collections.max(mixed));
// But this also works now, because compareTo from the Fruit class
// is being used here.
System.out.println(apple1.compareTo(orange1)); // -1
}
}
A class has method compareTo(Fruit) doesn't mean it implements Comparable<Fruit>. Only the opposite is correct.
In java API, method requiring comparison is based on Comparable interface, instead of checking if the class has compareTo method.
Since Fruit does not implements Comparable<Fruit>(it is a design problem if it implements), what the book say is correct. You can see Collections.max can't be called with List<Fruit>.
IMO, what make us confuse is the method name compareTo in Fruit.
The name is ambiguous to Comparable method,
So by convention, we should name a method compareTo only when we implements Comparable.
Wrong method may be called due to static binding
// Apple#compareTo is called
System.out.println(apple1.compareTo(apple2));
Fruit fruitWhichIsApple2 = apple2;
// Fruit#compareTo is called
System.out.println(apple1.compareTo(fruitWhichIsApple2));
Although it is marked as protected(which avoid usage from other package class), it is much clear if we rename it to something else.

Interfaces as parameters (Restrictions)

I came across an AP CSA question which had me puzzled for a while. It was basically an incomplete method that looked like this:
public static void methodMan(Comparable c) {.....}
The question first asked if it was valid to use the comparable interface in the parameter listing, then it asked if there were any restrictions on the comparable object. I was stuck between the choices that said either the object c that is being passed needs to be casted or initialized as a comparable or the object c could be any object that implements the comparable interface. Which one is it, and if it isn't either, what would be a restriction on the object c?
Yes it is valid to use interfaces as a parameter in methods and yes object c can be any object that implements the interface. The only caveat to the second portion is if there is a special method that needs to be invoked that the interface does not implement then you will need to cast it to the class first to get the method For Example:
public class MyComparable implements Comparable<String> {
private String item;
public MyComparable(String item) {
this.item = item;
}
#Override
public int compareTo(String o) {
return this.item.compareTo(o);
}
public Integer doThis() {
return 100;
}
public Integer compareSample(Comparable<String> c) {
if (c instanceof MyComparable) {
return ((MyComparable)c).doThis();
}
return c.compareTo(this.item);
}
}
compiles like a charm:
public static void methodMan (Comparable c) {
out.println ("we ignore c");
}
public static void main (String args[])
{
Comparable c1 = new String ();
methodMan (c1);
methodMan ((Comparable) c1);
String s2 = new String ();
methodMan (s2);
methodMan ((Comparable) s2);
}
and runs like a charm.

Is it ugly design that I overload a method depending on whether the argument is subtype or supertype?

Is it ugly design that I overload a method depending on whether the argument is subtype or supertype?
I intend to implent a super class A and a subclass B, and the objects can compare with each other.
compareTo is overloaded twice both in class A and class B, and the code seems a little messy.
It feels like ugly design. I'm not sure if there is a more elegant approach.
class A implements Comparable<A> {
private Integer x;
public A(Integer i) {
x = i;
}
public Integer getX() {
return x;
}
public int compareTo(A other) {
return x.compareTo(other.getX());
}
public int compareTo(B other) {
return x.compareTo(other.getX() + other.getY());
}
}
class B extends A {
private Integer y;
public B(Integer a, Integer b) {
super(a);
y = b;
}
public Integer getY() {
return y;
}
#Override
public int compareTo(A other) {
return Integer.compare(this.getX() + y, other.getX());
}
#Override
public int compareTo(B other) {
return Integer.compare(this.getX() + y, other.getX() + other.getY());
}
}
Yes it is a bad design. Something like this would be better.
class A implements Comparable<A> {
private Integer x;
public A(Integer i) {
x = i;
}
public Integer getX() {
return getX();
}
protected Integer compareValue() {
return getX();
}
#Override
public int compareTo(A other) {
return compareValue().compareTo(other.compareValue());
}
}
class B extends A {
Integer y;
public B(Integer a, Integer b) {
super(a);
y = b;
}
public Integer getY() {
return y;
}
#Override
protected Integer compareValue() {
return getX() + getY();
}
}
It's bad design, yes. Specifically, it violates transitivity. From the Comparable Javadocs:
The implementor must also ensure that the relation is transitive: (x.compareTo(y)>0 && y.compareTo(z)>0) implies x.compareTo(z)>0.
When you introduce inheritance, you lose transitivity. That's because your A.compareTo(B) method doesn't actually override anything, and so if you passed a List<A> to Collections.sort() which contained both A and B, it would end up using the A.compareTo(A) method for the A instances and the B.compareTo(A) method for the B instances. As you've written them, they are not transitive, and therefore would result in unpredictable ordering since reversing the comparison can change the sort order.
I wrote a similar response w.r.t. Object.equals and why you can't overload it a while back, which you can find here.

Java: Interface

I have been reading about interface in java. Overall I understood the concept except one problem. In http://goo.gl/l5r69 (docs.oracle), in the note it is written that we can type cast an interface and a class implementing it. That is
public interface Relatable {
public int isLargerThan (Relatable other) ;
}
public class RectanglePlus implements Relatable {
public int width = 0;
public int height = 0;
public Point origin;
// four constructors
// ...
// a method for computing the area of the rectangle
public int getArea() {
return width * height;
}
// a method required to implement the Relatable interface
public int isLargerThan(Relatable other) {
RectanglePlus otherRect = (RectanglePlus) other;
if (this.getArea() < otherRect.getArea()) {
return -1;
} else if (this.getArea () > otherRect.getArea()) {
return 1;
} else {
return 0;
}
}
}
How can otherRect (which is a interface) be casted to a RectanglePlus. The confusion is, RectanglePlus is a class having variables, which are not present in the otherRect which is an interface
I have to admit that the example in the java doc you showed is simply bad and confusing.
It's bad because it contains an unsafe cast down the hierarchy.
It is always safe to cast up (from implementing class to interface/superclass), but casting down should be avoided when possible.
Ideally, the Relatable interface should also contain getArea() method:
public interface Relatable {
public int isLargerThan(Relatable other);
public int getArea();
}
Now you don't need the ugly cast, simply:
public int isLargerThan(Relatable other) {
if (this.getArea() < other.getArea()) {
return -1;
} else if (this.getArea () > other.getArea()) {
return 1;
} else {
return 0;
}
}
is enough. I also think that isLargerThan(Relatable other) is a bad name (larger in terms of what?). It should probably be something like hasBiggerArea(Relatable other) so that it explains what we are actually comparing (only "larger" is rather vogue).
Your interface is pretty similar to Comparable, (Are you sure Comparable isn't what your looking for?) so maybe you should add a generic to it:
public interface Relatable<T extends Relatable> {
public int isLargerThan(T t);
}
And then your class will start as:
public class RectanglePlus implements Relatable<RectanglePlus> {...
So your RectanglePlus instance will be compared with other RectanglesPlus elements only.
If this does not suit what you need, then you have to choose what will happen when you are comparing two different classes:
public class RectanglePlus implements Relatable {
public int width = 0;
public int height = 0;
public Point origin;
public int getArea() {
return width * height;
}
public int isLargerThan(Relatable other) {
if (!(other instanceof RectanglePlus)) {
return 1; // I'm bigger than any other class!
}
RectanglePlus otherRect =(RectanglePlus)other;
return this.getArea() - otherRect.getArea();
}
}
Or, a third option, you can add another method to your interface to obtain the measureable, realatable value of an object. Then you could add a default implementation to isLargerThan, if you are using Java 8:
public interface Relatable<T extends Relatable> {
public default int isLargerThan(T t) {
return this.getRelatableValue - t.getRelatableValue();
}
public int getRelatableValue();
}
In the method declaration public int isLargerThan(Relatable other){...} the parameter other is declared to be a reference to an object whose class implements the interface Relatable.
In the method body, the expression (RectanglePlus)other means to check that the object is of class RectanglePlus or a subclass of that (and throw a ClassCastException if it isn't). Now, a RectanglePlus is Relatable, but the inverse isn't necessarily true; the cast here ensures that other will either be RectanglePlus; if it's not further instructions will not be executed because an exception will have been thrown.
We can type cast any object(of class C) stored in a variable of type T1 (interface or class) into a variable of type T2 if T2 is a super class or super interface of class C or T2==C otherwise a ClassCastException will be thrown.
So in your case if an object obj of class Foo implements Relatable is passed to isLargerThan method then it will throw ClassCastException, because obj's class Foo is not a super class of RectanglePlus.
One aspect that hasn't been touched on in other answers is the fact that the example in the Oracle docs has a clear problem: if Relatable is only meant to be analogous to Comparable, then there needs to be a specialization for shapes in order to avoid the cast in the isLargerThan method. Perhaps, for example, an interface called RelatableShape, which itself extends Relatable, and provides for the getArea() method. Then, you could have Circle, Hexagon, Rectangle, etc. classes that implement RelatableShape (the interface with isLargerThan and getArea), and the isLargerThan() method would not need to cast its argument to a particular concrete class (since the parameter could be guaranteed to implement RelatableShape, and getArea() would always be available).
Thus, though the Oracle documentation is showing something that is valid Java, it's also showing something that's necessary because of a bad design. Keep that in mind. Casting is almost never necessary in real code.
It's rather simple your mehtod
public int isLargerThan(Relatable other)
just asks for an argument that implements Relatable. It could be an object of any class that implements Relatable. As long as there is something like
public class SomeName implements Relatable { /* Implementation */ }
in the class, you can treat objects of that class as Relatable.
But that does not mean that these objects are not of their own type. If you have the following classes
public class Square implements Relatable {
public int isLargerThan(Relatable other) {
// Implementation
}
// Square specific implementation
}
and
public class Rectangle implements Relatable {
public int isLargerThan(Relatable other) {
// implmentation
}
// Rectangle specific implemenation
}
you can call the interface methods like this:
/* ... */
public static int check(Relatable a, Relatable b) {
return a.isLargerThan(b);
}
/* ... */
Square s = new Square();
Rectangle r = new Rectangle();
System.out.println("Check: " + check(s, r));
ATTENTION:
Because several different classes can implement Relatable you have to check the type of the argument to isLargerThan, otherwise you run into type cast exceptions.
Maybe you can specify something like this in Relatable
public int getSize();
Than you could write your isLargeThan method like this:
public int isLargerThan(Relatable other) {
if (this.getSize() < other.getSize())
return -1;
else if (this.getSize() > other.getSize())
return 1;
else
return 0;
}
Then there would be no need for a type cast.

Enforcing same generics type in a class using generics

The following example is taken from GenericsFAQ:
class Pair<X,Y> {
private X first;
private Y second;
public Pair(X a1, Y a2) {
first = a1;
second = a2;
}
public X getFirst() { return first; }
public Y getSecond() { return second; }
public void setFirst(X arg) { first = arg; }
public void setSecond(Y arg) { second = arg; }
}
Question: I wanted to enforce X and Y should be of same type. Example Pair<Integer,Integer> is correct but Pair<Integer, String> should not be accepted. Is it possible to achieve this through Generics?
Use
class Pair<X> {
And change all Y to X.
Did you consider something like this?
class LikePair<Z> extends Pair<Z,Z> {
public LikePair(Z a, Z b) {
super(a, b);
}
}
That way you get to keep the (potentially very useful) Pair class whilst also enforcing your 'likeness' constraint were needed.
As a matter of programming style I'd make Pair (and LikePair) immutable (final fields, no setters). Would also be good to implement equals(), hashCode(), toString(), etc.

Categories