简体   繁体   中英

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

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 Impl s 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 AbstractBase s, 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 Impl s at compile time (ie, an Impl object or Collection<Impl> ).

If you really want to apply a different comparison whenever your two objects are Impl s, 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 (ie, 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.

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