1. Introduction
In real life, one way to compare objects is rarely enough. Imagine you have a list of users: sometimes you want to sort them by first name, sometimes — by age, and sometimes — by the length of the last name. Or you have a class you didn’t author at all, and you can’t add compareTo to it. For such cases, Java has the Comparator interface.
When Comparable isn’t enough
- You can’t modify the class (for example, it’s from a third-party library).
- You need several ways to sort (by different fields).
- You want to separate comparison logic from the class itself (for example, sort differently in different parts of the program).
Analogy
If Comparable is the built-in “natural order” of an object, then Comparator is an external judge that can evaluate your objects by any criteria: today by name, tomorrow — by age, and the day after — by name length.
2. The Comparator interface: syntax and contract
Interface declaration
public interface Comparator<T> {
int compare(T o1, T o2);
}
The compare method should return:
- A negative number if the first object is “less than” the second.
- 0 if they are equal.
- A positive number if the first is “greater than” the second.
The contract is the same as Comparable’s, except now you compare two objects directly rather than the “current” vs. “other” via compareTo.
Example: a comparator for sorting by last name
Suppose we have a Person class:
public class Person {
private String firstName;
private String lastName;
private int age;
// Constructor and getters
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
}
Create a comparator that will sort by last name:
import java.util.Comparator;
public class LastNameComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return a.getLastName().compareTo(b.getLastName());
}
}
Note: the compareTo method on strings (String) compares them in lexicographic order.
3. Using Comparator: sorting collections
Sorting with a comparator
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Anna", "Kostetskaya", 25));
people.add(new Person("Boris", "Novak", 20));
people.add(new Person("Victoria", "Bell", 22));
// Sort by last name
Collections.sort(people, new LastNameComparator());
for (Person p : people) {
System.out.println(p.getLastName() + " " + p.getFirstName());
}
}
}
Result:
Novak Boris
Kostetskaya Anna
Bell Victoria
Sorting by age with a comparator
Even if the class already implements Comparable by name, you can create a separate comparator — by age:
public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return Integer.compare(a.getAge(), b.getAge());
}
}
And use it similarly:
Collections.sort(people, new AgeComparator());
Result:
Boris Novak (20)
Victoria Bell (22)
Anna Kostetskaya (25)
Example: choosing a comparator on the fly
Collections.sort(people, new LastNameComparator()); // By last name
Collections.sort(people, new AgeComparator()); // By age
4. Anonymous classes and lambda expressions
You can create comparators “on the fly” without declaring separate classes.
Anonymous class
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return a.getFirstName().compareTo(b.getFirstName());
}
});
Lambda expression
Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));
Or even shorter with the list method List.sort:
people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
- Anonymous classes — the old way; verbose.
- Lambdas — modern and compact.
5. Examples: sorting by different criteria
Sorting by last-name length
Comparator<Person> byLastNameLength = (a, b) ->
Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);
Sorting by age, then by first name (multi-level)
Comparator<Person> byAgeThenName = (a, b) -> {
int cmp = Integer.compare(a.getAge(), b.getAge());
if (cmp != 0) return cmp;
return a.getFirstName().compareTo(b.getFirstName());
};
people.sort(byAgeThenName);
Using a comparator for searching (example)
A comparator is useful not only for sorting but also for searching in sorted collections:
// people must be sorted by age!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
System.out.println("Found a person aged 22: " + people.get(idx));
}
6. Best practices and working with Comparator
Do not violate the contract
- If compare(a, b) returns 0, then compare(b, a) must also return 0.
- If compare(a, b) > 0, then compare(b, a) < 0.
- Account for possible null values (see below).
Don’t forget about equals and hashCode
Although comparators compare objects “in their own way”, for structures like TreeSet or when looking up keys in TreeMap it’s important that the comparator’s comparison logic is consistent with equals. Otherwise, you may get unexpected results: two different objects are considered equal by the comparator but are not equal according to equals.
Sorting with nulls in mind
If fields can be null, use the built-in helpers:
Comparator<Person> byLastNameNullSafe = Comparator.comparing(
Person::getLastName,
Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);
7. Useful tips
Table: Comparable vs. Comparator
| Comparable | Comparator | |
|---|---|---|
| Where is it implemented? | In the class itself | In a separate class/lambda |
| Method | |
|
| How many variants? | Only one “natural” one | As many as you need |
| Usage | |
|
| For third-party classes? | No | Yes |
Example: sorting in descending order
You can invert the order manually:
Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);
Or using reversed():
Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());
8. Common mistakes when working with Comparator
Error #1: Violating the comparison contract. If you forget that compare(a, b) and compare(b, a) must have opposite signs, or you return arbitrary values (for example, just the difference — a.getAge() - b.getAge(), which can overflow), the result will be unpredictable. Use Integer.compare rather than subtraction — it’s safer.
Error #2: Ignoring null values. If the fields you compare can be null, be sure to handle this case (for example, via Comparator.nullsFirst/Comparator.nullsLast), otherwise it’s easy to get a NullPointerException at the most unexpected moment.
Error #3: Unstable sorting criteria. If a comparator returns different values for the same objects (for example, it uses a random number or a highly mutable field), sorting may behave chaotically.
Error #4: Inconsistency with equals. If compare(a, b) == 0 but a.equals(b) is false, collections like TreeSet and TreeMap may not work as you expect. Ideally, equality by the comparator and by equals should match.
Error #5: Sorting without a comparator for third-party classes. If you try to sort objects of a “foreign” class without Comparable and without providing a Comparator, you’ll get a compilation error. Pass an explicit comparator.
GO TO FULL VERSION