- Queue Implementations in Java: A Comparative Analysis
- PriorityQueue: Internal Structure, Performance, and Use Cases
- HashSet in Java: Internal Structure, Performance, and Use Cases
- HashMap in Java: Internal Structure, Performance, and Use Cases
- Map Implementations in Java: A Comparative Guide
- CopyOnWriteArrayList in Java: Performance, and Use Cases
- LinkedList in Java: Internal Structure, Performance, and Use Cases
- ArrayList in Java: Internal Structure, Performance, and Use Cases
- Collections Framework in Java: What You Need to Know
- The Comparator Interface in Java
Introduction
The Comparator interface is central to Java’s ordering and sorting capabilities. Unlike the Comparable interface, which defines natural ordering directly inside the class, a Comparator externalizes comparison logic. This separation allows developers to design flexible and reusable sorting strategies, especially in systems requiring multiple ways to order the same data. Because ordering influences the performance and reliability of collections, understanding Comparator is essential for writing high‑quality Java applications.
1. Purpose of the Comparator interface
The primary purpose of the Comparator interface is to define custom ordering rules outside the model being compared. This is especially useful when:
- A class must support multiple sorting strategies.
- You cannot modify the original class.
- Sorting behavior must be configurable at runtime.
By externalizing behavior, Comparator supports the broader software engineering principle that “behavior should remain independent from state whenever possible.” In other words, the domain object (Person) does not need to know all possible ways it might be sorted.
2. The compare Contract
A Comparator<T> must implement the method:
int compare(T o1, T o2);
The contract mirrors that of Comparable.compareTo:
- Returns a negative integer if
o1 < o2 - Returns zero if
o1 == o2 - Returns a positive integer if
o1 > o2
In addition, a well-behaved comparator should be:
- Antisymmetric: if compare(a, b) < 0 then compare(b, a) > 0
- Transitive: if a < b and b < c, then a < c
- Consistent: repeated comparisons yield the same result if objects are unchanged
Java 8 introduced factory methods that simplify comparator creation:
Comparator.comparing(...)Comparator.thenComparing(...)Comparator.nullsFirst(...)Comparator.nullsLast(...)Comparator.reverseOrder()andComparator.reversed()
Example:
Comparator<Person> byAge = Comparator.comparing(Person::getAge);
These utilities encourage readable, composable comparison logic and reduce boilerplate.
3. Creating a Custom Comparator Class
Inline comparators (lambdas or method references) are convenient, but in many real applications, you want named, reusable comparator classes. They document intent more clearly and can be injected, tested, and maintained like any other component.
Assume the following Person class, which already defines its natural ordering using Comparable<Person> (by lastName, then firstName):
class Person implements Comparable<Person> {
private String firstName;
private String lastName;
private int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
@Override
public int compareTo(Person other) {
int lastNameComparison = this.lastName.compareTo(other.lastName);
if (lastNameComparison != 0) {
return lastNameComparison;
}
return this.firstName.compareTo(other.firstName);
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
@Override
public String toString() {
return String.format("%s %s (%d)", firstName, lastName, age);
}
}
Here, the natural order is alphabetical: last name → first name. However, suppose your business requirement is to sort people primarily by age descending, then by last name, then by first name. Modifying the natural ordering may not be desirable, so you create a dedicated comparator:
/**
* Custom Comparator implementation for Person objects.
* Ordering: age (descending), then lastName, then firstName.
*/
public class PersonAgeDescendingComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
if (p1 == p2) {
return 0; // same reference or both null (if you allow nulls here)
}
if (p1 == null) {
return -1; // convention: null is "less"
}
if (p2 == null) {
return 1;
}
// 1) Compare by age (descending)
int ageComparison = Integer.compare(p2.getAge(), p1.getAge());
if (ageComparison != 0) {
return ageComparison;
}
// 2) If age is equal, compare by last name (ascending)
int lastNameComparison =
Comparator.nullsFirst(String::compareTo)
.compare(p1.getLastName(), p2.getLastName());
if (lastNameComparison != 0) return lastNameComparison;
// 3) If last name is also equal, compare by first name (ascending)
return Comparator.nullsFirst(String::compareTo)
.compare(p1.getFirstName(), p2.getFirstName());
}
}
Why this custom comparator is valuable?
- It does not change the natural ordering defined in
Person.compareTo. - It encapsulates a distinct business rule: “older first, then lexicographic name”.
- It can be reused wherever that ordering is required (e.g., reports, dashboards, exports).
- It is easily testable and can be injected into services or repository layers.
In short, natural ordering via Comparable answers the question:
“How is a Person ordered by default?”
Whereas a custom Comparator answers:
“How is a Person ordered for this specific business use case?”
4. Integration with Java Collections
Comparator integrates deeply with Java’s collections:
4.1. List.sort and Collections.sort
// Using List.sort (preferred)
people.sort(new PersonAgeDescendingComparator());
// Using Collections.sort
Collections.sort(people, new PersonAgeDescendingComparator());
4.2. TreeSet / TreeMap
When you provide a Comparator, sorted collections use it instead of the natural ordering:
SortedSet<Person> byAgeSet = new TreeSet<>(new PersonAgeDescendingComparator());
byAgeSet.addAll(people); // ordered by custom comparator
If you omit the comparator, TreeSet falls back to Person.compareTo, i.e., natural ordering.
4.3. PriorityQueue
PriorityQueue uses comparators to define dequeue priority:
PriorityQueue<Person> queue =
new PriorityQueue<>(new PersonAgeDescendingComparator());
// Oldest person will be polled first
These integrations make Comparator a core piece of Java’s data structure ecosystem.
Using Comparator in Real Applications
In real systems, you often need both inline comparators (for quick one-off sorts) and named comparator classes (like PersonAgeDescendingComparator) for core business rules.
Below is a fully documented example, showing different ways of using a Comparator:
public class ComparatorInterfaceDemo {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", "Dupont", 30));
people.add(new Person("Bruno", "Martin", 45));
people.add(new Person("Chloé", "Dupont", 25));
people.add(new Person("David", "Albert", 45));
// ---------------------------------------------
// 1) Natural ordering (Comparable)
// ---------------------------------------------
Collections.sort(people); // or: people.sort(null)
System.out.println("Natural order (lastName, firstName):");
people.forEach(System.out::println);
// ---------------------------------------------
// 2) Custom comparator class: age desc, then name
// ---------------------------------------------
people.sort(new PersonAgeDescendingComparator());
System.out.println("\nCustom order (age desc, lastName, firstName):");
people.forEach(System.out::println);
// ---------------------------------------------
// 3) Inline comparator: age ascending
// ---------------------------------------------
people.sort(Comparator.comparingInt(Person::getAge));
System.out.println("\nInline comparator (age asc):");
people.forEach(System.out::println);
// ---------------------------------------------
// 4) TreeSet: Natural ordering (Comparable)
// ---------------------------------------------
Set<Person> naturalSet = new TreeSet<>();
naturalSet.addAll(people);
System.out.println("\nTreeSet (natural order):");
naturalSet.forEach(System.out::println);
// ---------------------------------------------
// 5) TreeSet with custom comparator
// ---------------------------------------------
Set<Person> ageDescSet =
new TreeSet<>(new PersonAgeDescendingComparator());
ageDescSet.addAll(people);
System.out.println("\nTreeSet (custom comparator: age desc):");
ageDescSet.forEach(System.out::println);
// ---------------------------------------------
// 6) TreeMap with natural ordering (keys sorted)
// ---------------------------------------------
Map<Person, String> naturalMap = new TreeMap<>();
naturalMap.put(new Person("Zoe", "Durand", 22), "Z");
naturalMap.put(new Person("Alex", "Martin", 33), "A");
naturalMap.put(new Person("Bob", "Albert", 40), "B");
System.out.println("\nTreeMap (natural ordering of keys):");
naturalMap.forEach((k, v) ->
System.out.println(k + " -> " + v));
// ---------------------------------------------
// 7) TreeMap with custom comparator
// ---------------------------------------------
Map<Person, String> customMap =
new TreeMap<>(new PersonAgeDescendingComparator());
customMap.put(new Person("Zoe", "Durand", 22), "Z");
customMap.put(new Person("Alex", "Martin", 33), "A");
customMap.put(new Person("Bob", "Albert", 40), "B");
System.out.println("\nTreeMap (age desc ordering of keys):");
customMap.forEach((k, v) ->
System.out.println(k + " -> " + v));
// ---------------------------------------------
// 8) PriorityQueue using custom comparator
// ---------------------------------------------
PriorityQueue<Person> queue =
new PriorityQueue<>(new PersonAgeDescendingComparator());
queue.addAll(people);
System.out.println("\nPriorityQueue polling (highest age first):");
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
}
}
Running the code will produce the following output:
Natural order (lastName, firstName):
David Albert (45)
Alice Dupont (30)
Chloé Dupont (25)
Bruno Martin (45)
Custom order (age desc, lastName, firstName):
David Albert (45)
Bruno Martin (45)
Alice Dupont (30)
Chloé Dupont (25)
Inline comparator (age asc):
Chloé Dupont (25)
Alice Dupont (30)
David Albert (45)
Bruno Martin (45)
TreeSet (natural order):
David Albert (45)
Alice Dupont (30)
Chloé Dupont (25)
Bruno Martin (45)
TreeSet (custom comparator: age desc):
David Albert (45)
Bruno Martin (45)
Alice Dupont (30)
Chloé Dupont (25)
TreeMap (natural ordering of keys):
Bob Albert (40) -> B
Zoe Durand (22) -> Z
Alex Martin (33) -> A
TreeMap (age desc ordering of keys):
Bob Albert (40) -> B
Alex Martin (33) -> A
Zoe Durand (22) -> Z
PriorityQueue polling (highest age first):
David Albert (45)
Bruno Martin (45)
Alice Dupont (30)
Chloé Dupont (25)
6. Best Practices for Implementing Comparator
When implementing custom comparators (either inline or as classes), several good practices apply:
- Be consistent with equals when possible
If two objects are considered equal by equals, it is usually less surprising if the comparator returns zero as well. - Handle null values explicitly
Decide and document how nulls should be treated. Use helpers like:
Comparator<Person> bySafeLastName =
Comparator.comparing(Person::getLastName,
Comparator.nullsFirst(String::compareTo));
- Prefer factory methods and method references
For many cases,Comparator.comparingandthenComparingproduce concise and readable comparators. - Use chaining instead of nested if statements where appropriate
For instance, the logic of PersonAgeDescendingComparator could also be expressed functionally:
Comparator<Person> byBusinessRule =
Comparator.comparingInt(Person::getAge).reversed()
.thenComparing(Person::getLastName)
.thenComparing(Person::getFirstName);
- Keep comparators stateless and pure
Avoid side effects or stateful behavior inside comparators; they must be predictable for sorting algorithms to behave correctly. - Document the business rule
The name and Javadoc of the comparator class should clearly state the intended ordering, such as “age descending, then lastName, then firstName”.
Conclusion
While it is convenient to create inline comparators with lambdas and method references, robust Java applications often benefit from dedicated custom comparator classes. By understanding the compare contract, designing clear custom comparators, and leveraging tight integration with the collections framework, you can build sorting and ordering logic that is both expressive and maintainable—well aligned with professional Java development standards
You can find the complete code for this article on GitHub.
