Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should Comparable ever compare to another type?

I'm wondering if there's ever a valid use case for the following:

class Base {}

class A implements Comparable<Base> {
    //...
}

It seems to be a common pattern (see Collections for a number of examples) to accept a collection of type T, where T extends Comparable<? super T>.

But it seems technically impossible to fulfill the contract of compareTo() when comparing to a base class, because there's no way to ensure that another class doesn't extend the base with a contradictory comparison. Consider the following example:

class Base {
    final int foo;
    Base(int foo) {
        this.foo = foo;
    }
}

class A extends Base implements Comparable<Base> {
    A(int foo) {
        super(foo);
    }
    public int compareTo(Base that) {
        return Integer.compare(this.foo, that.foo); // sort by foo ascending
    }
}

class B extends Base implements Comparable<Base> {
    B(int foo) {
        super(foo);
    }
    public int compareTo(Base that) {
        return -Integer.compare(this.foo, that.foo); // sort by foo descending
    }
}

We have two classes extending Base using comparisons that don't follow a common rule (if there were a common rule, it would almost certainly be implemented in Base). Yet the following broken sort will compile:

Collections.sort(Arrays.asList(new A(0), new B(1)));

Wouldn't it be safer to only accept T extends Comparable<T>? Or is there some use case that would validate the wildcard?

like image 221
shmosel Avatar asked Jan 05 '16 02:01

shmosel


People also ask

When should I use comparable?

To summarize, if sorting of objects needs to be based on natural order then use Comparable whereas if you sorting needs to be done on attributes of different objects, then use Comparator in Java.

What is the difference between a Comparator and a comparable?

While Comparable is used on objects that are naturally ordered, the Comparator interface implements sorting by taking the attributes of an object into consideration. Further, Comparator sorting takes into account objects of two different classes and Comparable compares objects using the “this” reference.

When would you use a Comparator instead of implementing comparable to sort a collection or an array?

1) Comparable provides a single sorting sequence. In other words, we can sort the collection on the basis of a single element such as id, name, and price. The Comparator provides multiple sorting sequences. In other words, we can sort the collection on the basis of multiple elements such as id, name, and price etc.

Why do we need Comparator and comparable in Java?

Comparable interface can be used to provide single way of sorting whereas Comparator interface is used to provide different ways of sorting. For using Comparable, Class needs to implement it whereas for using Comparator we don't need to make any change in the class. Comparable interface is in java.


1 Answers

This is a very good question. First, let's start with the reason why Collections use methods like

binarySearch(List<? extends Comparable<? super T>> list, T key)

Indeed, why not just

binarySearch(List<? extends Comparable<T>> list, T key)

The reason for this is the PECS principle: Producer Extends, Consumer Super. What does binarySearch do? It reads elements from the list and then compares them by passing their values to the compareTo function. Since it reads elements, the list acts as a producer, and hence the first part—Producer Extends. This one is obvious, so what about the Consumer Super part?

Consumer Super basically means that if you're only going to pass values to some function, you don't really care whether it accepts the exact type of your object or some superclass of it. So what the binarySearch declaration is saying is: I can look for anything as long as that anything can be passed to the compareTo method of the list elements.

In the case of sorting it's not that obvious, because the elements are only compared to each other. But even then, what if Base actually implements Comparable<Base> (and does the whole comparison) and A and B just extend Base without touching comparison in any way? Then you would be unable to sort lists of A and B because they don't implement Comparable<A> and Comparable<B> respectively. You'd have to reimplement the whole interface each time you subclass!

Another example: what if someone wanted to do a binary search on a list containing instances of some class that doesn't even extend your Base?

class BaseComparable implements Comparable<Base> {
    private final Base key;
    // other fields

    BaseComparable(Base key, ...) {
        this.key = key;
        // other fields initialization
    }

    @Override
    public int compareTo(Base otherKey) {
        return this.key.compareTo(otherKey);
    }
};

And now they want to use an instance of A as the key for this binary search. They can only do it because of the ? super T part. Note that this class has no idea about whether the key is an A or a B so it can't possibly implement Comparable<A/B>.

As for your example, I guess it is just an example of poor design. Unfortunately, I see no way of preventing such things without breaking the PECS principle and/or limiting already limited functionality of Java generics.

like image 113
Sergei Tachenov Avatar answered Oct 03 '22 21:10

Sergei Tachenov