When working with custom objects, we often want to compare them based on some pre-defined criteria. That’s why we use the Comparable
interface to implement a natural ordering for our custom objects.
Also, we often need to specify a total ordering if there is no natural ordering implemented by our custom object or override the natural ordering defined by the object, so we use a Comparator
interface to do such a thing.
2. The Example Demo
In the example demo, we’ll show how to compare Person
objects using natural ordering implemented by the Comparable
interface and also to override that natural ordering by using a new ordering implemented by the Comparator
interface.
Assume you have a Person
class as demonstrated below:
class Person {
private int age;
private String firstName;
private String lastName;
public Person(int age, String firstName, String lastName) {
this.age = age;
this.firstName = firstName;
this.lastName = lastName;
}
// getters and setters
@Override
public String toString() {
return "Person{" +
"age=" + age +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
'}';
}
}
3. Comparable Interface
3.1 Definition
You define the natural ordering for your objects by implementing the Comparable’s compareTo(T o)
method in your custom objects. For example, if we want to compare the Person
object based on the first name we’ll implement the Comparable
interface like so:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class Person implements Comparable<Person> {
private int age;
private String firstName;
private String lastName;
public Person(int age, String firstName, String lastName) {
this.age = age;
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
'}';
}
@Override
public int compareTo(Person person) {
return this.firstName.compareTo(person.firstName);
}
public static void main(String[] args) {
// people to sort
List<Person> people = new ArrayList<>(List.of(
new Person(20, "Hamza", "Belmellouki"),
new Person(60, "Allan", "Truck"),
new Person(40, "Zidan", "Kemero"),
new Person(30, "Cindy", "Mahbd")
));
people.forEach(System.out::println); //unsorted
System.out.println("----------");
Collections.sort(people);// sorted
people.forEach(System.out::println);
}
}
If you run the program the output will be:
Person{age=20, firstName=’Hamza’, lastName=’Belmellouki’}
Person{age=60, firstName=’Allan’, lastName=’Truck’}
Person{age=40, firstName=’Zidan’, lastName=’Kemero’}
Person{age=30, firstName=’Cindy’, lastName=’Mahbd’}
— — — — —
Person{age=60, firstName=’Allan’, lastName=’Truck’}
Person{age=30, firstName=’Cindy’, lastName=’Mahbd’}
Person{age=20, firstName=’Hamza’, lastName=’Belmellouki’}
Person{age=40, firstName=’Zidan’, lastName=’Kemero’}
As you can see the compareTo
method implements the sorting strategy based on the lexicographic order. It calls the compareTo
implementation that is defined in the String
class which compares two strings lexicographically.
Our compareTo
implementation returns the value 0 if the person.firstName
is equal to this.fisrtName
, a value less than 0 if this.firstName
is lexicographically less than the person.firstName
; and a value greater than 0 if this.fisrtName
is lexicographically greater than the string argument.
Notice Collections.sort
method accepts a list of Person, it sorts the list of Person based on the natural ordering.
If the Person class does not implement Comparable
then a compile-time error occurs because the Collections.sort
method defines its generic type parameter to extend the Comparable
interface (you know generic type safety is here to rescue us from ClassCastException
! That’s another subject, maybe for another blog).
3.2 Best practices when using Comparable:
These are some best practices when using a Comparable
:
Sometimes you may see compareTo
method that relies on the fact that the difference between two values is negative if the first is less than the second, zero if the values are equal, and positive if the first value is greater. For example, this code violates compareTo
method contract(transitivity):
@Override
public int compareTo(Person p) {
return this.age - p.age;
}
Don’t use this technique. It is filled with danger from integer overflow and IEEE 754 floating arithmetic artifacts.
Instead, Always either use the static Integer, Double, Float’s compare
method. For example:
@Override
public int compareTo(Person p) {
return Integer.compare(this.age, p.age);
}
4. Comparator Interface
4.1 Definition
If we want to define a total ordering on an object that has no natural ordering or if we want to override the natural ordering then we leverage the Comparator
interface to do so. For example, if we want to override the natural ordering of Person
objects and compare the Person
object based on the age we’ll implement the Comparator
interface like so:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
class Person implements Comparable<Person> {
private int age;
private String firstName;
private String lastName;
public Person(int age, String firstName, String lastName) {
this.age = age;
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
'}';
}
@Override
public int compareTo(Person person) {
return this.firstName.compareTo(person.firstName);
}
public static void main(String[] args) {
// people to sort
List<Person> people = new ArrayList<>(List.of(
new Person(20, "Hamza", "Belmellouki"),
new Person(60, "Allan", "Truck"),
new Person(40, "Zidan", "Kemero"),
new Person(30, "Cindy", "Mahbd")
));
people.forEach(System.out::println); //unsorted
System.out.println("----------");
Comparator<Person> cmp = (o1, o2) -> Integer.compare(o1.age, o2.age);
people.sort(cmp); // sorted
people.forEach(System.out::println);
}
}
If you run this code the output will be:
Person{age=20, firstName=’Hamza’, lastName=’Belmellouki’}
Person{age=60, firstName=’Allan’, lastName=’Truck’}
Person{age=40, firstName=’Zidan’, lastName=’Kemero’}
Person{age=30, firstName=’Cindy’, lastName=’Mahbd’}
— — — — —
Person{age=20, firstName=’Hamza’, lastName=’Belmellouki’}
Person{age=30, firstName=’Cindy’, lastName=’Mahbd’}
Person{age=40, firstName=’Zidan’, lastName=’Kemero’}
Person{age=60, firstName=’Allan’, lastName=’Truck’}
Note that the list is sorted by age. In line 43 you can see we have implemented the single abstract compare
method on the Comparator
interface using a lambda. Our implementation of the Comparator#compare method takes two person objects and returns a negative value if o1.age
is less than the o2.age
; 0 if they are equal; positive integer in the o1.age
is greater than the o2.age
.
In line 45 I’ve called List#sort method which sorts the list based on the Comparator
implementation that we’ve passed to it.
4.2 Best practices when using Comparator:
These are some best practices when using a Comparator
:
The practice from Comparable
interface applies here: Don’t use the difference to compare objects, instead always rely on the static Integer, Double, Float’s compare
method to compare numeric values:
Comparator<Person> comparator = (p1, p2) -> Integer.compare(p1.getAge(), p2.getAge());
Failing to do so could result in an integer overflow and IEEE 754 floating arithmetic artifacts.
The second best practice that is recommended is to avoid chaining if-else statements that result from additional comparison if objects were equal on the previous comparison(s). To illustrate, in this code snippet we implemented a Comparator
that compares the person objects based first on their first name if they were equal then we move to the last name then to age:
public static void main(String[] args) {
// people to sort
List<Person> people = new ArrayList<>(List.of(
new Person(20, "Hamza", "Belmellouki"),
new Person(50, "Hamza", "Belmellouki"),
new Person(44, "Hamza", "Belmellouki"),
new Person(60, "Allan", "Truck"),
new Person(40, "Zidan", "Kemero"),
new Person(30, "Cindy", "Mahbd")
));
people.forEach(System.out::println); //unsorted
System.out.println("----------");
// Don't do this!
Comparator<Person> cmp = (p1, p2) -> {
int result = p1.firstName.compareTo(p2.firstName);
if (result == 0) {
result = p1.lastName.compareTo(p2.lastName);
if (result == 0) {
return Integer.compare(p1.age, p2.age);
} else {
return result;
}
} else {
return result;
}
};
people.sort(cmp); // sorted
people.forEach(System.out::println);
}
}
Output:
Person{age=20, firstName='Hamza', lastName='Belmellouki'}
Person{age=50, firstName='Hamza', lastName='Belmellouki'}
Person{age=44, firstName='Hamza', lastName='Belmellouki'}
Person{age=60, firstName='Allan', lastName='Truck'}
Person{age=40, firstName='Zidan', lastName='Kemero'}
Person{age=30, firstName='Cindy', lastName='Mahbd'}
----------
Person{age=60, firstName='Allan', lastName='Truck'}
Person{age=30, firstName='Cindy', lastName='Mahbd'}
Person{age=20, firstName='Hamza', lastName='Belmellouki'}
Person{age=44, firstName='Hamza', lastName='Belmellouki'}
Person{age=50, firstName='Hamza', lastName='Belmellouki'}
Person{age=40, firstName='Zidan', lastName='Kemero'}
You don’t have to use this old pattern when you want to do such a comparison because it is hard to read, error-prone code and lacks robustness. Instead, you would use Java 8 Comparator’s comparing
and thenComparingXXX
methods:
public static void main(String[] args) {
// people to sort
List<Person> people = new ArrayList<>(List.of(
new Person(20, "Hamza", "Belmellouki"),
new Person(50, "Hamza", "Belmellouki"),
new Person(44, "Hamza", "Belmellouki"),
new Person(60, "Allan", "Truck"),
new Person(40, "Zidan", "Kemero"),
new Person(30, "Cindy", "Mahbd")
));
people.forEach(System.out::println); //unsorted
System.out.println("----------");
Comparator<Person> cmp = Comparator.comparing(Person::getFirstName)
.thenComparing(Person::getLastName)
.thenComparingInt(Person::getAge);
people.sort(cmp); // sorted
people.forEach(System.out::println);
}
This code does that same thing but it is much more readable and less error-prone. Here on line 16, the static Comparator’s comparing
method takes a key extractor which is a Function
object implementation used to extract the Comparable sort key.
Notice on line 18 I used thenComparingInt
instead of thenComparing
because the former takes a ToIntFunction
object, thus there will be no boxing involved because its abstract method returns an int primitive, not a boxed int.
5. Comparable vs. Comparator
You'll want to use the Comparable interface when we want a default order. When using a Comparable interface we don’t need to make any code changes at the client-side. For example, Collections#sort
method automatically uses the compareTo()
method of the class. For Comparator, the client needs to provide the Comparator class to use in compare()
method.
Both of these interfaces are part of the Java Collection framework.
There are several reasons to use a Comparator even if we already have a Comparable implemented:
Specify different comparison strategies which aren’t possible when using Comparable
To avoid changing/adding code to our custom classes
Often, it is impossible to modify the source code of the class whose objects we want to sort, thus it is impossible to use Comparable in such circumstances.
7. Conclusion
In this article, we saw how to use Comparable to implement the natural ordering and how to override it using a Comparator. We saw well-known best practices to use when implementing them. In the next article, there will be more Core Java. Stay tuned!
Let us know what you think in the comments below and don’t forget to share!
Top comments (0)