Comparator
A java.util.Comparator<T> is an object that defines an ordering for values of type T from outside the class. Where Comparable gives a class one built-in natural order, comparators let you sort the same objects in any number of ways (by name, by price, by date, descending, with tie-breakers) and sort classes you cannot modify. Since Java 8, the Comparator interface provides factory and combinator methods that build almost any ordering in a single readable expression.
The Interface
@FunctionalInterface
public interface Comparator<T> {
int compare(T a, T b);
// plus many default and static helper methods
}
compare(a, b) follows the same sign convention as compareTo: negative if a comes first, zero if they are equal in order, positive if a comes after b.
Because Comparator has a single abstract method, it is a functional interface, so it can be written as a lambda:
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
List<String> words = new ArrayList<>(List.of("pear", "Fig", "apple", "kiwi"));
words.sort(byLength);
System.out.println(words); // [Fig, pear, kiwi, apple]
The sort is stable, so pear and kiwi, both length 4, keep their original relative order.
Where Comparators Are Used
| API | Example |
|---|---|
| Sorting lists | list.sort(cmp), Collections.sort(list, cmp) |
| Sorting arrays of objects | Arrays.sort(array, cmp) |
| Sorted collections | new TreeSet<>(cmp), new TreeMap<>(cmp) |
| Priority queues | new PriorityQueue<>(cmp) |
| Minimum / maximum | Collections.min(coll, cmp), Collections.max(coll, cmp) |
| Streams | stream.sorted(cmp), stream.max(cmp), stream.min(cmp) |
| Binary search | Collections.binarySearch(list, key, cmp) |
Building Comparators with Factory Methods
Writing compare by hand is rarely necessary. Static factory methods create a comparator from a key extractor, a function that pulls out the value to compare:
record Product(String name, double price, int stock) { }
Comparator<Product> byName = Comparator.comparing(Product::name);
Comparator<Product> byPrice = Comparator.comparingDouble(Product::price);
Comparator<Product> byStock = Comparator.comparingInt(Product::stock);
| Factory | Key type |
|---|---|
Comparator.comparing(keyExtractor) | Any Comparable key (String, LocalDate, BigDecimal, ...) |
Comparator.comparing(keyExtractor, keyComparator) | Any key, compared with the given comparator |
comparingInt, comparingLong, comparingDouble | Primitive keys, without boxing |
Comparator.naturalOrder() / reverseOrder() | Natural ordering of Comparable elements, or its reverse |
Comparator.nullsFirst(cmp) / nullsLast(cmp) | Wraps a comparator so null elements sort first or last |
Product::name is a method reference meaning "call name() on each product." The comparator calls it on both objects and compares the results.
Combining Comparators
Default methods transform and chain comparators:
| Method | Effect |
|---|---|
reversed() | Reverse the entire ordering so far |
thenComparing(other) | Tie-breaker: used only when the current comparator returns 0 |
thenComparing(keyExtractor) | Tie-breaker by another key |
thenComparing(keyExtractor, keyComparator) | Tie-breaker by another key with its own ordering |
thenComparingInt / Long / Double(...) | Primitive tie-breakers |
Comparator<Product> listing = Comparator
.comparingInt(Product::stock).reversed() // most stock first
.thenComparing(Product::price) // then cheapest
.thenComparing(Product::name); // then alphabetical
reversed() applies to everything before it
reversed() reverses the whole comparator built up to that point, not just the last key:
// Price descending, then name ascending:
Comparator.comparingDouble(Product::price).reversed()
.thenComparing(Product::name);
// Name ascending, then price DESCENDING (reverse only the tie-breaker):
Comparator.comparing(Product::name)
.thenComparing(Product::price, Comparator.reverseOrder());
// Careful: this reverses BOTH keys:
Comparator.comparing(Product::name)
.thenComparing(Product::price)
.reversed();
To reverse a single key in a chain, pass Comparator.reverseOrder() as that key's comparator.
Type inference with lambdas
When a chain starts with a lambda rather than a method reference, the compiler sometimes cannot infer the element type before reversed() or thenComparing is applied. Declaring the lambda parameter's type fixes it:
Comparator.comparingInt((String s) -> s.length()).reversed(); // compiles
// Comparator.comparingInt(s -> s.length()).reversed(); // s is inferred as Object: compile error
Method references such as String::length usually avoid the problem.
Common Orderings
List<String> names = new ArrayList<>(List.of("ravi", "Arjun", "kavya", "Divya"));
names.sort(Comparator.naturalOrder()); // [Arjun, Divya, kavya, ravi] uppercase first
names.sort(String.CASE_INSENSITIVE_ORDER); // [Arjun, Divya, kavya, ravi]
names.sort(Comparator.reverseOrder()); // [ravi, kavya, Divya, Arjun]
names.sort(Comparator.comparing(String::length)
.thenComparing(String.CASE_INSENSITIVE_ORDER)); // [ravi, Arjun, Divya, kavya]
Sorting with nulls
Natural-order comparators throw NullPointerException when they meet null. Wrap them to decide where nulls go, either for elements or for keys:
List<String> cities = new ArrayList<>(Arrays.asList("Pune", null, "Agra"));
cities.sort(Comparator.nullsLast(Comparator.naturalOrder())); // [Agra, Pune, null]
// null KEYS: employees without a manager listed last
Comparator.comparing(Employee::manager, Comparator.nullsLast(Comparator.naturalOrder()));
Sorting map entries
Map<String, Integer> votes = Map.of("java", 42, "python", 57, "go", 42);
List<Map.Entry<String, Integer>> ranking = new ArrayList<>(votes.entrySet());
ranking.sort(Map.Entry.<String, Integer>comparingByValue().reversed()
.thenComparing(Map.Entry.comparingByKey()));
// [python=57, go=42, java=42]
The explicit type witness Map.Entry.<String, Integer>comparingByValue() supplies the generic types that inference cannot determine through .reversed().
Writing compare by Hand
For orderings that do not depend on a simple key, implement compare directly:
// Custom rank: CRITICAL, then HIGH, then everything else alphabetically
Comparator<String> severity = (a, b) -> {
List<String> rank = List.of("CRITICAL", "HIGH");
int ra = rank.indexOf(a), rb = rank.indexOf(b);
ra = ra < 0 ? rank.size() : ra;
rb = rb < 0 ? rank.size() : rb;
int c = Integer.compare(ra, rb);
return c != 0 ? c : a.compareTo(b);
};
The same contract as compareTo applies: antisymmetric, transitive, and consistent. Use Integer.compare and friends rather than subtraction to avoid overflow.
Comparators and Sorted Collections
A comparator given to TreeSet, TreeMap, or PriorityQueue also decides what counts as a duplicate: two elements comparing as 0 are treated as the same. A comparator that compares only one field can therefore make a TreeSet silently drop distinct objects. Add tie-breakers that make the ordering distinguish every non-equal pair:
new TreeSet<>(Comparator.comparingDouble(Product::price)
.thenComparing(Product::name));
Reusable Comparators as Constants
Comparators are stateless and immutable, so they are ideal constants. Defining them once gives orderings meaningful names:
public record Product(String name, double price, int stock) {
public static final Comparator<Product> CHEAPEST_FIRST =
Comparator.comparingDouble(Product::price).thenComparing(Product::name);
public static final Comparator<Product> BEST_STOCKED =
Comparator.comparingInt(Product::stock).reversed();
}
products.sort(Product.CHEAPEST_FIRST);
Practical Example: Sortable Staff Directory
An HR dashboard lets the user choose how to sort the staff list. Each option is a named comparator in a map.
import java.time.LocalDate;
import java.util.*;
public class Directory {
record Employee(String name, String dept, double salary, LocalDate joined, String manager) { }
static final Map<String, Comparator<Employee>> SORTS = Map.of(
"name", Comparator.comparing(Employee::name, String.CASE_INSENSITIVE_ORDER),
"salary", Comparator.comparingDouble(Employee::salary).reversed(),
"seniority", Comparator.comparing(Employee::joined),
"dept", Comparator.comparing(Employee::dept)
.thenComparing(Employee::salary, Comparator.reverseOrder()),
"manager", Comparator.comparing(Employee::manager,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(Employee::name)
);
public static void main(String[] args) {
List<Employee> staff = new ArrayList<>(List.of(
new Employee("meena", "Engineering", 145000, LocalDate.of(2019, 4, 1), null),
new Employee("Ravi", "Engineering", 98000, LocalDate.of(2022, 7, 15), "meena"),
new Employee("arjun", "Sales", 76000, LocalDate.of(2021, 1, 10), "Kavya"),
new Employee("Kavya", "Sales", 120000, LocalDate.of(2018, 9, 3), null),
new Employee("Divya", "Engineering", 112000, LocalDate.of(2020, 2, 20), "meena")
));
for (String key : List.of("name", "salary", "dept", "manager")) {
staff.sort(SORTS.get(key));
StringJoiner line = new StringJoiner(", ", key + ": ", "");
for (Employee e : staff) line.add(e.name());
System.out.println(line);
}
Employee longest = Collections.min(staff, SORTS.get("seniority"));
System.out.println("Longest serving: " + longest.name() + " since " + longest.joined());
}
}
Output:
name: arjun, Divya, Kavya, meena, Ravi
salary: meena, Kavya, Divya, Ravi, arjun
dept: meena, Divya, Ravi, Kavya, arjun
manager: arjun, Divya, Ravi, Kavya, meena
Longest serving: Kavya since 2018-09-03
Each sort key is a comparator built from factory methods:
- name compares case-insensitively, so
arjunandmeenasort among the capitalized names instead of after them. - salary reverses a primitive comparator for highest-first.
- dept sorts departments ascending and, within each department, salary descending, by giving the tie-breaker its own
reverseOrder(). - manager uses
nullsLaston the key, so employees without a manager (meena,Kavya) appear at the end instead of causing aNullPointerException. Its natural string order putsKavyabeforemeenabecause uppercase letters sort first.
Collections.min accepts a comparator too, finding the earliest joining date without sorting. The Employee record has no natural ordering at all; every ordering lives outside the class, which is exactly what comparators are for.