Comparator
Comparator is a functional interface in java.util that defines an ordering for objects that is separate from the objects themselves. Unlike Comparable, which bakes the natural ordering into the class itself, Comparator is an external ordering strategy that can be defined anywhere, by anyone, for any purpose. Java 8 enriched Comparator with a comprehensive set of static and default factory methods that compose, reverse, and chain comparators into complex orderings with minimal code. Understanding Comparator means understanding the compare() contract, the full library of factory methods, how to compose comparators for multi-key sorting, null handling, and the powerful integration with Stream.sorted(), TreeSet, TreeMap, and PriorityQueue. This entry covers every aspect of modern Comparator usage.
The compare() Contract and Basic Comparators
// ── Basic comparator implementations ─────────────────────────────────
// Lambda:
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
// Method reference (for types with existing compare methods):
Comparator<Integer> intComp = Integer::compare;
Comparator<String> strComp = String::compareTo;
// ── Comparator.comparing — extract key, use natural ordering ──────────
record Person(String name, int age, String city) {}
Comparator<Person> byName = Comparator.comparing(Person::name);
Comparator<Person> byAge = Comparator.comparingInt(Person::age);
Comparator<Person> byCity = Comparator.comparing(Person::city);
// ── thenComparing — multi-key sorting ────────────────────────────────
Comparator<Person> byAgeThenName =
Comparator.comparingInt(Person::age)
.thenComparing(Person::name);
Comparator<Person> byAgeThenNameThenCity =
Comparator.comparingInt(Person::age)
.thenComparing(Person::name)
.thenComparing(Person::city);
List<Person> people = new ArrayList<>(List.of(
new Person("Bob", 30, "Paris"),
new Person("Alice", 25, "London"),
new Person("Carol", 30, "Berlin"),
new Person("Dave", 25, "London")));
people.sort(byAgeThenName);
people.forEach(p -> System.out.printf("%s %d%n", p.name(), p.age()));
// Alice 25, Dave 25, Bob 30, Carol 30
// ── reversed — descending order ───────────────────────────────────────
Comparator<Person> oldestFirst = Comparator.comparingInt(Person::age).reversed();
people.sort(oldestFirst);
// Bob 30, Carol 30, Alice 25, Dave 25
// ── Reversed with multi-key: oldest first, then alphabetical name ──────
Comparator<Person> complex =
Comparator.comparingInt(Person::age).reversed()
.thenComparing(Person::name);
people.sort(complex);
// Bob 30, Carol 30, Alice 25, Dave 25 (30s before 25s, alphabetical within)Factory Methods — The Complete Comparator Toolkit
// ── Primitive-specialised comparators — no boxing ────────────────────
record Product(String name, int stock, double price, long id) {}
// comparingInt — ToIntFunction, no Integer boxing:
Comparator<Product> byStock = Comparator.comparingInt(Product::stock);
// comparingDouble — ToDoubleFunction:
Comparator<Product> byPrice = Comparator.comparingDouble(Product::price);
// comparingLong — ToLongFunction:
Comparator<Product> byId = Comparator.comparingLong(Product::id);
// ── comparing with key comparator ─────────────────────────────────────
// Compare by name using case-insensitive ordering:
Comparator<Product> byCaseInsensitiveName =
Comparator.comparing(Product::name, String.CASE_INSENSITIVE_ORDER);
// Compare by name length, then alphabetically:
Comparator<Product> byLengthThenAlpha =
Comparator.comparing(Product::name,
Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder()));
// ── naturalOrder and reverseOrder ─────────────────────────────────────
Comparator<String> natural = Comparator.naturalOrder();
Comparator<String> reversed = Comparator.reverseOrder();
// Max-heap priority queue using reverseOrder:
PriorityQueue<Integer> maxHeap =
new PriorityQueue<>(Comparator.reverseOrder());
maxHeap.addAll(List.of(3, 1, 4, 1, 5, 9, 2, 6));
System.out.println(maxHeap.poll()); // 9 — maximum at head
// ── nullsFirst and nullsLast ───────────────────────────────────────────
List<String> withNulls = new ArrayList<>(
Arrays.asList("banana", null, "apple", null, "cherry"));
withNulls.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(withNulls);
// [null, null, apple, banana, cherry]
withNulls.sort(Comparator.nullsLast(Comparator.naturalOrder()));
System.out.println(withNulls);
// [apple, banana, cherry, null, null]
// nullsFirst with reversed inner comparator:
withNulls.sort(Comparator.nullsFirst(Comparator.reverseOrder()));
System.out.println(withNulls);
// [null, null, cherry, banana, apple]
// ── Complete example: sort employees ──────────────────────────────────
record Employee(String dept, String name, Integer salary) {}
List<Employee> employees = new ArrayList<>(List.of(
new Employee("Eng", "Alice", 90_000),
new Employee("Eng", "Bob", null), // null salary
new Employee("HR", "Carol", 75_000),
new Employee("Eng", "Dave", 85_000),
new Employee(null, "Eve", 80_000) // null department
));
employees.sort(
Comparator.comparing(Employee::dept,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(Employee::name)
.thenComparing(Employee::salary,
Comparator.nullsLast(Comparator.naturalOrder())));Comparator in Streams, Collections, and Maps
// ── Stream operations with Comparator ────────────────────────────────
record Student(String name, double gpa, int year) {}
List<Student> students = List.of(
new Student("Alice", 3.9, 3),
new Student("Bob", 3.5, 2),
new Student("Carol", 3.9, 2),
new Student("Dave", 3.7, 3));
// Sort stream by GPA descending, then name:
students.stream()
.sorted(Comparator.comparingDouble(Student::gpa).reversed()
.thenComparing(Student::name))
.forEach(s -> System.out.printf("%s %.1f%n", s.name(), s.gpa()));
// Alice 3.9, Carol 3.9, Dave 3.7, Bob 3.5
// Max GPA:
Optional<Student> topStudent = students.stream()
.max(Comparator.comparingDouble(Student::gpa));
System.out.println(topStudent.map(Student::name).orElse("none")); // Alice
// Min by year then name (youngest year first):
Optional<Student> first = students.stream()
.min(Comparator.comparingInt(Student::year)
.thenComparing(Student::name));
System.out.println(first.map(Student::name).orElse("none")); // Bob (year 2)
// ── TreeMap with custom key ordering ──────────────────────────────────
// String keys sorted by length then alphabetically (not default lexicographic)
TreeMap<String, Integer> wordCount = new TreeMap<>(
Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder()));
wordCount.put("banana", 5);
wordCount.put("apple", 3);
wordCount.put("fig", 1);
wordCount.put("cherry", 7);
wordCount.forEach((word, count) ->
System.out.println(word + " = " + count));
// fig=1, apple=3, banana=5, cherry=7 — sorted by length then alpha
// ── TreeSet with comparator — equality by comparator ──────────────────
// Persons considered same if same name (regardless of other fields)
TreeSet<Student> byName = new TreeSet<>(
Comparator.comparing(Student::name));
byName.add(new Student("Alice", 3.9, 3));
byName.add(new Student("Alice", 3.5, 2)); // same name → not added (duplicate by comparator)
System.out.println(byName.size()); // 1 — Alice considered duplicate
// ── Combining Comparators for complex queries ─────────────────────────
// Sort by multiple criteria, some ascending some descending:
Comparator<Student> examOrder =
Comparator.comparingInt(Student::year) // ascending year
.thenComparingDouble(Student::gpa) // ascending GPA within year
.reversed() // flip BOTH keys: desc year, desc GPA
.thenComparing(Student::name); // then ascending name (after reversal)
// Note: reversed() flips ALL previously chained comparisons
// If you want mixed asc/desc, chain them separately:
Comparator<Student> mixed =
Comparator.comparingInt(Student::year) // ascending year
.thenComparing(
Comparator.comparingDouble(Student::gpa).reversed()) // desc GPA
.thenComparing(Student::name); // ascending name