Bits of Java – Episode 22: Comparator vs Comparable

Last week we discussed a bit about lambdas expressions and functional interfaces, and we saw that one of the built-in Java functional interfaces is the Comparator. It provides as functional method:


int compare(T obj1, T obj2);

which allows to compare two objects and returning an int, whose value depends on the result of such a comparison.

Today we will also see another Java functional interface, which is the Comparable interface. Since the nomenclature here is quite similar, we will try to underline the differences between the two, and see how it is recommended to use them.

First, let’s start with the abstract method of Comparable:


public interface Comparable<T> {
    int compareTo(T obj);
}

So, as you can see, the compareTo method still returns an int, but it takes only one parameter. This is because Comparable is intended to be implemented, and the instance on which the method is then called will be the object which has to be compared with the input parameter. It sounds more complicated than it actually is. Let’s have a look at a practical example.


public class Person implements Comparable<Person> {
    private int age;
    private String name;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Person person) {
        if(age == person.getAge()) {
            return 0;
        }
        else if(age > person.getAge()) {
            return 1;
        }
        return -1;
    }

    public int getAge() {
        return age;
    }
     public String getName() {
         return name;
     }
}

What are we doing here? So, we are creating a class Person which implements our Comparable interface. Person can have in this case two attributes, age and name, which are set in the constructor.

Then we have to override the compareTo method of the interface. In there, we compare the current instance we are in, so the one on which the method will be called, and an instance of Person which is passed as input parameter. We chose to implement the comparison based on the age attribute and following the standard rules when comparing two objects. So, if the two Person instances have the same age we return 0; if the first one, namely the instance on which the method is called, has an age larger than the other, we return a positive integer, otherwise we return a negative one.


public static void main(String[] args) {
    Person p1 = new Person("Ilenia", 30);
    Person p2 = new Person("Francesca", 17);
    System.out.println(p1.compareTo(p2)); //1
    System.out.println(p2.compareTo(p1)); //-1
}

So far so good, right? But why exactly do we need both Comparable and Comparator? Well, remember when we looked at sorting, comparing and searching within an array?


int[] numbers = {3,4,1,7,2};
Arrays.sort(numbers);
//print 1,2,3,4,7
for(int n : numbers) {
    System.out.println(n);
}

How does Java know the criterion for sorting and comparing? Well, Integer, which is the wrapper class for int, implements Comparable. So, what is actually happening under the hood, is that Java performs first an autoboxing operation to convert the int to Integer, and then it uses the Integer implementation of the Comparable method to perform the sorting.

The same thing would have happened if you used an array of String, since also String implements Comparable.

If, instead, you have an array of a type which does not implement Comparable, you cannot sort it in this way.


public class Animal {
    String name;
    int numOfLegs;

    public Animal(String name, int numOfLegs) {
        this.name = name;
        this.numOfLegs = numOfLegs;
    }

    //Getter and setter assumed

    public static void main(String[] args) {
        Animal cat = new Animal("cat", 4);
        Animal spider = new Animal("spider", 8);
        Animal[] animals = {cat, spider};
        Arrays.sort(animals); //throws a ClassCastException!!
    }
}

We created a class Animal and we tried to sort an array of that type. The compiler does not complain in this case, but at runtime, Java tries to cast our Animal objects to Comparable, in order to be able to call the compareTo method. Since Animal does not implement Comparable, however, this cast will fail, resulting in a runtime exception!

Should we then make our classes all implementing Comparable, to account for the possibility of sorting and comparing them? Luckily for us, this is not needed! Both Arrays and Collections, which can be seen as the equivalent helper class for lists, have an overloaded version of the sorting and comparing methods, which takes as second parameter a Comparator (pay attention, a Comparator, not a Comparable!).


public static void main(String[] args) {
    Animal cat = new Animal("cat", 4);
    Animal spider = new Animal("spider", 8);
    Animal[] animals = {cat, spider};
    //This will work!!
    Arrays.sort(animals, (a1,a2)->((Integer)a1.getNumOfLegs()).compareTo(a2.getNumOfLegs()));
}

As you can see, here we are using Comparator with lambdas expressions to specify how the comparison should be done when sorting the array.

So, to conclude, Comparable is an interface which is most intended to be directly implemented by other classes, so its method compareTo can be applied to any instance of the implementing class. On the other hand, Comparator is more intended to be used with lambdas expressions, when you need a comparison only in particular cases, and not, let’s say, “by default” in the implementation of your class.

Next week we will dive a bit deeper into the use of generics in Java!

by Ilenia Salvadori