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