Comparator
When you need to sort objects in more than one way — by name here, by salary there — Comparator is your go-to tool. Unlike Comparable, which bakes a single “natural” order into the class itself, Comparator lives outside the class and lets you define as many different orderings as you like without ever touching the original source.
What Is Comparator?
Comparator<T> is a functional interface in java.util. It defines one abstract method:
int compare(T o1, T o2);
The contract is simple:
- Return a negative number if
o1should come beforeo2. - Return zero if they are considered equal.
- Return a positive number if
o1should come aftero2.
You pass a Comparator to Collections.sort(), List.sort(), Arrays.sort(), or sorted data structures like TreeSet and TreeMap.
A First Example — Sorting by Name
import java.util.*;
class Employee {
String name;
int salary;
Employee(String name, int salary) {
this.name = name;
this.salary = salary;
}
@Override
public String toString() {
return name + "($" + salary + ")";
}
}
public class ComparatorDemo {
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>(List.of(
new Employee("Zara", 90000),
new Employee("Alice", 75000),
new Employee("Bob", 85000)
));
// Sort alphabetically by name
employees.sort(Comparator.comparing(e -> e.name));
System.out.println(employees);
}
}
Output:
[Alice($75000), Bob($85000), Zara($90000)]
Comparator.comparing() (added in Java 8) accepts a key extractor — a function that pulls the field you want to compare — and builds the full Comparator for you.
Writing a Comparator Three Ways
1. Anonymous Class (pre-Java 8)
Comparator<Employee> bySalary = new Comparator<Employee>() {
@Override
public int compare(Employee a, Employee b) {
return Integer.compare(a.salary, b.salary);
}
};
Always use Integer.compare() (or Double.compare(), etc.) instead of plain subtraction — subtraction can overflow for extreme values.
2. Lambda Expression
Comparator<Employee> bySalary = (a, b) -> Integer.compare(a.salary, b.salary);
Because Comparator is a @FunctionalInterface, it works perfectly with lambda expressions.
3. Comparator.comparingInt() Factory
Comparator<Employee> bySalary = Comparator.comparingInt(e -> e.salary);
The primitive-specialised variants (comparingInt, comparingLong, comparingDouble) avoid boxing overhead.
Reversing Order
Comparator<Employee> byHighestSalary = Comparator
.comparingInt((Employee e) -> e.salary)
.reversed();
employees.sort(byHighestSalary);
System.out.println(employees);
Output:
[Zara($90000), Bob($85000), Alice($75000)]
reversed() wraps the comparator and flips the sign of every compare() call.
Chaining Multiple Sort Criteria with thenComparing()
Real-world sorting often needs a secondary (or tertiary) criterion to break ties. thenComparing() makes this elegant:
List<Employee> team = new ArrayList<>(List.of(
new Employee("Alice", 75000),
new Employee("Alice", 90000), // same name, different salary
new Employee("Bob", 75000)
));
Comparator<Employee> byNameThenSalary = Comparator
.comparing((Employee e) -> e.name)
.thenComparingInt(e -> e.salary);
team.sort(byNameThenSalary);
System.out.println(team);
Output:
[Alice($75000), Alice($90000), Bob($75000)]
The second comparator only fires when the first returns zero — exactly like a SQL ORDER BY name, salary.
Handling null Values
Sorting a list that might contain null elements crashes with a NullPointerException by default. Use Comparator.nullsFirst() or Comparator.nullsLast() to handle them gracefully:
List<String> names = Arrays.asList("Charlie", null, "Alice", null, "Bob");
names.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(names);
Output:
[null, null, Alice, Bob, Charlie]
Tip:
Comparator.naturalOrder()returns a comparator that uses the element’s owncompareTo()— it works for any class that implementsComparable.
Using Comparator with TreeSet and TreeMap
Sorted collections accept a Comparator in their constructors to override the default ordering:
TreeSet<Employee> byName = new TreeSet<>(Comparator.comparing(e -> e.name));
byName.add(new Employee("Zara", 90000));
byName.add(new Employee("Alice", 75000));
byName.add(new Employee("Bob", 85000));
byName.forEach(System.out::println);
Output:
Alice($75000)
Bob($85000)
Zara($90000)
Warning: When you provide a custom
Comparatorto aTreeSet, it replaces the natural ordering entirely. Two objects that your comparator considers equal (comparereturns0) are treated as duplicates and only one is stored — even ifequals()says they are different.
Comparator vs Comparable at a Glance
| Feature | Comparable | Comparator |
|---|---|---|
| Package | java.lang | java.util |
| Method | compareTo(T o) | compare(T o1, T o2) |
| Implemented by | The class being sorted | A separate class or lambda |
| Number of orderings | One (natural order) | Unlimited |
| Modifying the class | Required | Not required |
| Typical use | Strings, numbers, dates | Custom / multiple sort criteria |
See Comparable vs Comparator for a deeper side-by-side analysis.
Under the Hood
Comparator has been a @FunctionalInterface since Java 8, which means the JVM can represent it as a single invokedynamic call site rather than allocating a named anonymous-class object every time. At runtime, the JIT can inline small lambdas directly into the sort loop, eliminating the virtual-dispatch overhead that older hand-written Comparator anonymous classes incurred.
Comparator.comparing() returns a Comparators.NaturalOrderComparator or an KeyExtractorComparator internally. When you chain .thenComparing(), it wraps the first comparator in a CompoundComparator that calls the first, checks for zero, and only then delegates to the second. The chain is built lazily as a linked structure, not eagerly evaluated, so building a long chain has O(n) composition cost but adds only a tiny constant overhead per element during the actual sort.
List.sort() uses a TimSort algorithm (O(n log n) worst-case) which is adaptive — nearly-sorted input converges in near O(n) time. The stability of TimSort means equal elements (those where compare() returns 0) keep their original relative order, which matters when you chain comparators.
Note: For primitive arrays (
int[],long[], etc.)Arrays.sort()uses a dual-pivot quicksort, not TimSort, and does not accept aComparator. To use custom comparators you must work with boxed arrays orList.
Practical Pattern — Reusable Comparators as Constants
Centralise your comparators as public static final fields or factory methods so the rest of the codebase stays consistent:
public class Employee {
public static final Comparator<Employee> BY_NAME =
Comparator.comparing(e -> e.name);
public static final Comparator<Employee> BY_SALARY =
Comparator.comparingInt(e -> e.salary);
public static final Comparator<Employee> BY_SALARY_DESC =
BY_SALARY.reversed();
String name;
int salary;
Employee(String name, int salary) {
this.name = name;
this.salary = salary;
}
}
// Elsewhere:
employees.sort(Employee.BY_SALARY_DESC);
This pairs well with Enums if the set of orderings is fixed and known at compile time.
Related Topics
- Comparable — understand natural ordering before adding external comparators
- Comparable vs Comparator — pick the right tool for each sorting job
- Sorting Collections — how
Collections.sort()andList.sort()use comparators under the hood - TreeSet — a
SortedSetthat relies on a comparator (or natural order) to stay ordered - TreeMap — a
SortedMapwhere the key ordering is driven by a comparator - Lambda Expressions — write concise, readable comparators without anonymous classes