Java Comparator
Java Comparator
java.util.Comparator<T> is the interface for defining sort orders externally — outside the class being sorted. Where Comparable bakes one natural ordering into the class itself, Comparator lets you define as many alternate orderings as you need, pass them to sort methods, store them in variables, and compose them into multi-level sort chains. Every place in Java that accepts a sort order — Collections.sort(), List.sort(), Arrays.sort(), TreeSet, TreeMap, Stream.sorted() — accepts a Comparator.
What Is Java Comparator?
Comparator<T> is a functional interface in java.util that declares one abstract method: int compare(T a, T b). It is annotated with @FunctionalInterface, which means it can be written as a lambda expression. Since Java 8, the interface also provides a rich set of default and static factory methods for building, chaining, and composing comparators without writing boilerplate.
java.util.Comparator<T> ← @FunctionalInterface since Java 8
abstract: compare(T a, T b) : int
WHERE Comparator IS ACCEPTED:
Collections.sort(list, comparator)
list.sort(comparator) ← Java 8+, cleaner than Collections.sort
Arrays.sort(array, comparator)
new TreeSet<>(comparator) ← replaces compareTo() for that TreeSet
new TreeMap<>(comparator) ← replaces compareTo() for that TreeMap
stream.sorted(comparator) ← produces sorted stream
stream.min(comparator) ← finds minimum by comparator
stream.max(comparator) ← finds maximum by comparator
Collections.min(collection, cmp)
Collections.max(collection, cmp)
Basic Overview — The Full Comparator API
COMPARATOR CREATION:
1. Lambda (most common — Java 8+):
Comparator<Employee> bySalary =
(a, b) -> Double.compare(a.getSalary(), b.getSalary());
2. Comparator.comparing() — factory method (cleanest):
Comparator<Employee> bySalary =
Comparator.comparingDouble(Employee::getSalary);
3. Anonymous class (pre-Java 8, avoid in new code):
Comparator<Employee> bySalary = new Comparator<Employee>() {
public int compare(Employee a, Employee b) {
return Double.compare(a.getSalary(), b.getSalary());
}
};
COMPARATOR CHAINING (default methods on Comparator):
.thenComparing(keyExtractor) ← secondary sort when primary is equal
.thenComparingInt(keyExtractor) ← secondary sort for int fields
.thenComparingDouble(keyExtractor) ← secondary sort for double fields
.thenComparingLong(keyExtractor) ← secondary sort for long fields
.reversed() ← reverses entire comparison
.thenComparing(cmp) ← chain another full Comparator
COMPARATOR STATIC FACTORIES:
Comparator.comparing(keyExtractor) ← extracts Comparable key
Comparator.comparingInt(keyExtractor) ← extracts int key
Comparator.comparingDouble(keyExtractor) ← extracts double key
Comparator.comparingLong(keyExtractor) ← extracts long key
Comparator.naturalOrder() ← uses compareTo (ascending)
Comparator.reverseOrder() ← uses compareTo (descending)
Comparator.nullsFirst(comparator) ← nulls sort before non-nulls
Comparator.nullsLast(comparator) ← nulls sort after non-nulls
RETURN VALUE — same as compareTo():
Negative → a comes BEFORE b
Zero → a and b are equal in this ordering
Positive → a comes AFTER b
When to Use Comparator
USE Comparator WHEN:
1. The class does not implement Comparable (external class, third-party lib)
— sorting by a field on a class you cannot modify
— sorting DTOs from an API response
2. Multiple sort orders are needed for the same class
— Employee by salary for payroll report
— Employee by name for directory listing
— Employee by department, then by hire date for org chart
3. The sort order is decided at runtime or by the caller
— user selects "sort by price" or "sort by rating" in a UI
— a generic service accepts a Comparator parameter
4. The sort order is the inverse of natural ordering
— Comparator.reverseOrder() for descending String/Integer sort
— .reversed() to flip any existing Comparator
5. Null-safe sorting is required
— Comparator.nullsFirst()/nullsLast() handle nulls without NPE
USE Comparable WHEN:
- The class has one obvious default ordering that always applies
- The ordering is an inherent property of the type (ID, timestamp, SKU)
- You own the class source and can modify it
BOTH CAN COEXIST:
A class can implement Comparable (natural order by ID) AND
have multiple Comparator instances (by salary, by name, by department).
When a Comparator is passed to sort(), it overrides the natural order.
How Comparator Works Internally
COMPARE() SEMANTICS — identical to compareTo():
compare(a, b)
< 0 → a comes BEFORE b (a is "smaller")
= 0 → a and b are EQUAL in this ordering
> 0 → a comes AFTER b (a is "larger")
HOW sort() USES compare():
To sort [Employee(salary=80k), Employee(salary=60k), Employee(salary=95k)]
with Comparator.comparingDouble(Employee::getSalary):
compare(80k, 60k) → positive → swap: 60k comes first
compare(80k, 95k) → negative → no swap
compare(60k, 95k) → negative → no swap
Result: [60k, 80k, 95k]
COMPARATOR.COMPARING() — HOW IT BUILDS A COMPARATOR:
Comparator.comparing(Employee::getDepartment)
internally: (a, b) -> a.getDepartment().compareTo(b.getDepartment())
The key extractor is called once per element per comparison.
Comparator.comparing(Employee::getDepartment)
.thenComparingDouble(Employee::getSalary)
internally:
1. compare depts: if non-zero, return that result
2. if zero (same dept): compare salaries
This chains produce a single Comparator object — not two comparisons.
REVERSED() UNDER THE HOOD:
bySalary.reversed()
internally: (a, b) -> bySalary.compare(b, a) ← arguments swapped
Reverses the entire chain, not just the last field.
Core Operations with Examples
Lambda and Comparator.comparing() — Modern Syntax
1// File: ComparatorBasicsDemo.java
2
3import java.util.ArrayList;
4import java.util.Collections;
5import java.util.Comparator;
6import java.util.List;
7
8public class ComparatorBasicsDemo {
9
10 record Employee(int id, String name, String department, double salary, int experience) {
11 @Override public String toString() {
12 return String.format("[%d] %-16s %-14s Rs.%6.0f %dy",
13 id, name, department, salary, experience);
14 }
15 }
16
17 public static void main(String[] args) {
18
19 List<Employee> employees = new ArrayList<>(List.of(
20 new Employee(1005, "Priya Sharma", "Engineering", 92000, 4),
21 new Employee(1002, "Rohan Gupta", "Design", 78000, 6),
22 new Employee(1008, "Ananya Iyer", "Engineering", 105000, 8),
23 new Employee(1001, "Karan Mehta", "Product", 88000, 5),
24 new Employee(1003, "Divya Nair", "Engineering", 92000, 3),
25 new Employee(1006, "Meera Pillai", "Design", 82000, 4),
26 new Employee(1004, "Arjun Reddy", "Product", 95000, 7)
27 ));
28
29 // Sort by salary ascending — lambda form
30 Comparator<Employee> bySalaryAsc =
31 (a, b) -> Double.compare(a.salary(), b.salary());
32 employees.sort(bySalaryAsc);
33 System.out.println("=== Sorted by salary ascending (lambda) ===");
34 employees.forEach(System.out::println);
35
36 System.out.println();
37
38 // Sort by salary descending — Comparator.comparingDouble + reversed()
39 Comparator<Employee> bySalaryDesc =
40 Comparator.comparingDouble(Employee::salary).reversed();
41 employees.sort(bySalaryDesc);
42 System.out.println("=== Sorted by salary descending (comparing + reversed) ===");
43 employees.forEach(System.out::println);
44
45 System.out.println();
46
47 // Sort by name ascending — Comparator.comparing with String key
48 employees.sort(Comparator.comparing(Employee::name));
49 System.out.println("=== Sorted by name ascending (Comparator.comparing) ===");
50 employees.forEach(System.out::println);
51 }
52}Output:
=== Sorted by salary ascending (lambda) ===
[1002] Rohan Gupta Design Rs. 78000 6y
[1006] Meera Pillai Design Rs. 82000 4y
[1001] Karan Mehta Product Rs. 88000 5y
[1005] Priya Sharma Engineering Rs. 92000 4y
[1003] Divya Nair Engineering Rs. 92000 3y
[1004] Arjun Reddy Product Rs. 95000 7y
[1008] Ananya Iyer Engineering Rs.105000 8y
=== Sorted by salary descending (comparing + reversed) ===
[1008] Ananya Iyer Engineering Rs.105000 8y
[1004] Arjun Reddy Product Rs. 95000 7y
[1005] Priya Sharma Engineering Rs. 92000 4y
[1003] Divya Nair Engineering Rs. 92000 3y
[1001] Karan Mehta Product Rs. 88000 5y
[1006] Meera Pillai Design Rs. 82000 4y
[1002] Rohan Gupta Design Rs. 78000 6y
=== Sorted by name ascending (Comparator.comparing) ===
[1008] Ananya Iyer Engineering Rs.105000 8y
[1004] Arjun Reddy Product Rs. 95000 7y
[1003] Divya Nair Engineering Rs. 92000 3y
[1001] Karan Mehta Product Rs. 88000 5y
[1006] Meera Pillai Design Rs. 82000 4y
[1005] Priya Sharma Engineering Rs. 92000 4y
[1002] Rohan Gupta Design Rs. 78000 6y
Chaining — thenComparing() for Multi-Level Sort
1// File: ComparatorChainingDemo.java
2
3import java.util.ArrayList;
4import java.util.Comparator;
5import java.util.List;
6
7public class ComparatorChainingDemo {
8
9 record Employee(int id, String name, String department, double salary, int experience) {
10 @Override public String toString() {
11 return String.format("[%d] %-16s %-14s Rs.%6.0f %dy",
12 id, name, department, salary, experience);
13 }
14 }
15
16 public static void main(String[] args) {
17
18 List<Employee> employees = new ArrayList<>(List.of(
19 new Employee(1005, "Priya Sharma", "Engineering", 92000, 4),
20 new Employee(1002, "Rohan Gupta", "Design", 78000, 6),
21 new Employee(1008, "Ananya Iyer", "Engineering", 105000, 8),
22 new Employee(1001, "Karan Mehta", "Product", 88000, 5),
23 new Employee(1003, "Divya Nair", "Engineering", 92000, 3),
24 new Employee(1006, "Meera Pillai", "Design", 82000, 4),
25 new Employee(1004, "Arjun Reddy", "Product", 95000, 7),
26 new Employee(1007, "Sneha Kumar", "Design", 82000, 2)
27 ));
28
29 // Department asc → salary desc → name asc
30 Comparator<Employee> orgChart =
31 Comparator.comparing(Employee::department) // primary: dept asc
32 .thenComparingDouble(Employee::salary) // secondary: salary asc
33 .reversed() // flip entire chain (dept desc, salary desc)
34 .thenComparing(Employee::name); // tertiary: name asc (after reversed)
35 // Note: thenComparing AFTER reversed() is NOT reversed
36
37 // Clearer: build each comparison explicitly
38 Comparator<Employee> byDeptThenSalaryDesc =
39 Comparator.comparing(Employee::department) // dept asc
40 .thenComparingDouble(Employee::salary) // salary asc within dept
41 .thenComparing(Employee::name); // name asc as tiebreaker
42
43 employees.sort(byDeptThenSalaryDesc);
44 System.out.println("=== Department asc, then salary asc, then name asc ===");
45 employees.forEach(System.out::println);
46
47 System.out.println();
48
49 // Department asc, then salary DESC within department, then name asc
50 Comparator<Employee> byDeptThenSalaryDescV2 =
51 Comparator.comparing(Employee::department)
52 .thenComparing(
53 Comparator.comparingDouble(Employee::salary).reversed()) // salary desc only
54 .thenComparing(Employee::name);
55
56 employees.sort(byDeptThenSalaryDescV2);
57 System.out.println("=== Department asc, salary DESC within dept, name asc ===");
58 employees.forEach(System.out::println);
59
60 System.out.println();
61
62 // Experience desc (seniors first), salary desc as tiebreaker
63 employees.sort(Comparator.comparingInt(Employee::experience).reversed()
64 .thenComparingDouble(Employee::salary).reversed());
65 System.out.println("=== Most experienced first, then highest salary ===");
66 employees.forEach(System.out::println);
67 }
68}Output:
=== Department asc, then salary asc, then name asc ===
[1006] Meera Pillai Design Rs. 82000 4y
[1007] Sneha Kumar Design Rs. 82000 2y
[1002] Rohan Gupta Design Rs. 78000 6y
[1003] Divya Nair Engineering Rs. 92000 3y
[1005] Priya Sharma Engineering Rs. 92000 4y
[1008] Ananya Iyer Engineering Rs.105000 8y
[1001] Karan Mehta Product Rs. 88000 5y
[1004] Arjun Reddy Product Rs. 95000 7y
=== Department asc, salary DESC within dept, name asc ===
[1008] Ananya Iyer Engineering Rs.105000 8y
[1003] Divya Nair Engineering Rs. 92000 3y
[1005] Priya Sharma Engineering Rs. 92000 4y
[1002] Rohan Gupta Design Rs. 78000 6y
[1006] Meera Pillai Design Rs. 82000 4y
[1007] Sneha Kumar Design Rs. 82000 2y
[1001] Karan Mehta Product Rs. 88000 5y
[1004] Arjun Reddy Product Rs. 95000 7y
=== Most experienced first, then highest salary ===
[1008] Ananya Iyer Engineering Rs.105000 8y
[1004] Arjun Reddy Product Rs. 95000 7y
[1002] Rohan Gupta Design Rs. 78000 6y
[1001] Karan Mehta Product Rs. 88000 5y
[1005] Priya Sharma Engineering Rs. 92000 4y
[1006] Meera Pillai Design Rs. 82000 4y
[1003] Divya Nair Engineering Rs. 92000 3y
[1007] Sneha Kumar Design Rs. 82000 2y
Null Handling, TreeSet, and Streams
1// File: ComparatorAdvancedDemo.java
2
3import java.util.ArrayList;
4import java.util.Comparator;
5import java.util.List;
6import java.util.TreeSet;
7import java.util.stream.Collectors;
8
9public class ComparatorAdvancedDemo {
10
11 record Product(String sku, String name, Double price, Integer stock) {}
12
13 public static void main(String[] args) {
14
15 // Null-safe comparator — nullsLast puts null prices at the end
16 Comparator<Product> byPriceNullSafe =
17 Comparator.comparing(Product::price,
18 Comparator.nullsLast(Comparator.naturalOrder()));
19
20 List<Product> products = new ArrayList<>(List.of(
21 new Product("P003", "USB Hub", 799.0, 42),
22 new Product("P001", "Mouse Pad", null, 100), // null price
23 new Product("P004", "HDMI Cable", 399.0, 65),
24 new Product("P002", "Laptop Stand", null, 30), // null price
25 new Product("P005", "Webcam", 2999.0, 20)
26 ));
27
28 products.sort(byPriceNullSafe);
29 System.out.println("=== Sorted by price, nulls last ===");
30 products.forEach(p -> System.out.printf(
31 " %-14s Rs.%s%n", p.name(), p.price() != null ? p.price() : "N/A"));
32
33 System.out.println();
34
35 // TreeSet with custom Comparator — overrides compareTo (if any)
36 // Sort products by price ascending, then sku as tiebreaker for uniqueness
37 Comparator<Product> priceAndSku =
38 Comparator.comparing(Product::price,
39 Comparator.nullsLast(Comparator.naturalOrder()))
40 .thenComparing(Product::sku);
41
42 TreeSet<Product> priceSet = new TreeSet<>(priceAndSku);
43 products.forEach(priceSet::add);
44
45 System.out.println("=== TreeSet with Comparator (nulls last) ===");
46 priceSet.forEach(p -> System.out.printf(
47 " %-14s Rs.%s%n", p.name(), p.price() != null ? p.price() : "N/A"));
48
49 System.out.println();
50
51 // Stream.sorted() with Comparator — declarative pipeline
52 System.out.println("=== Stream.sorted() — in-stock, cheapest first ===");
53 products.stream()
54 .filter(p -> p.stock() > 0 && p.price() != null)
55 .sorted(Comparator.comparingDouble(Product::price))
56 .forEach(p -> System.out.printf(
57 " %-14s Rs.%6.2f stock=%d%n", p.name(), p.price(), p.stock()));
58
59 System.out.println();
60
61 // Comparator.reverseOrder() for descending natural order on Comparable types
62 System.out.println("=== Comparator.reverseOrder() on String list ===");
63 List<String> cities = new ArrayList<>(
64 List.of("Mumbai", "Delhi", "Bengaluru", "Chennai", "Hyderabad"));
65 cities.sort(Comparator.reverseOrder());
66 System.out.println(" Reverse alphabetical: " + cities);
67 }
68}Output:
=== Sorted by price, nulls last ===
HDMI Cable Rs.399.0
USB Hub Rs.799.0
Webcam Rs.2999.0
Mouse Pad Rs.N/A
Laptop Stand Rs.N/A
=== TreeSet with Comparator (nulls last) ===
HDMI Cable Rs.399.0
USB Hub Rs.799.0
Webcam Rs.2999.0
Mouse Pad Rs.N/A
Laptop Stand Rs.N/A
=== Stream.sorted() — in-stock, cheapest first ===
HDMI Cable Rs.399.00 stock=65
USB Hub Rs.799.00 stock=42
Webcam Rs.2999.00 stock=20
=== Comparator.reverseOrder() on String list ===
Reverse alphabetical: [Mumbai, Hyderabad, Delhi, Chennai, Bengaluru]
Real-World Example — Flipkart Product Ranking Engine
Flipkart's search results apply different ranking strategies depending on the context. A price-conscious search ranks by price ascending. A quality search ranks by rating descending. A default search applies a composite score: sponsored first, then rating descending, then review count descending, then price ascending as a final tiebreaker. Each ranking is a distinct Comparator — built, named, selected at runtime, and passed to a single sort method.
1// File: SearchResult.java
2
3public record SearchResult(
4 String productId,
5 String title,
6 double price,
7 double rating,
8 int reviewCount,
9 boolean sponsored,
10 int stockLeft) {
11
12 @Override
13 public String toString() {
14 return String.format("[%s] %-28s Rs.%6.0f %.1f* (%d reviews)%s",
15 productId, title, price, rating, reviewCount,
16 sponsored ? " [AD]" : "");
17 }
18}1// File: ProductRankingEngine.java
2
3import java.util.Comparator;
4import java.util.List;
5
6public class ProductRankingEngine {
7
8 // Each Comparator is a named, reusable sort strategy
9 private static final Comparator<SearchResult> BY_PRICE_ASC =
10 Comparator.comparingDouble(SearchResult::price);
11
12 private static final Comparator<SearchResult> BY_PRICE_DESC =
13 Comparator.comparingDouble(SearchResult::price).reversed();
14
15 private static final Comparator<SearchResult> BY_RATING_DESC =
16 Comparator.comparingDouble(SearchResult::rating).reversed();
17
18 private static final Comparator<SearchResult> BY_REVIEWS_DESC =
19 Comparator.comparingInt(SearchResult::reviewCount).reversed();
20
21 // Composite ranking: sponsored first, then rating, reviews, price
22 private static final Comparator<SearchResult> DEFAULT_RANKING =
23 Comparator.comparing(SearchResult::sponsored).reversed() // sponsored = true sorts first
24 .thenComparing(BY_RATING_DESC)
25 .thenComparing(BY_REVIEWS_DESC)
26 .thenComparing(BY_PRICE_ASC);
27
28 // Runtime sort selection — Comparator is chosen by caller context
29 public static List<SearchResult> rank(List<SearchResult> results, String strategy) {
30 Comparator<SearchResult> comparator = switch (strategy) {
31 case "PRICE_LOW" -> BY_PRICE_ASC;
32 case "PRICE_HIGH" -> BY_PRICE_DESC;
33 case "TOP_RATED" -> BY_RATING_DESC;
34 case "MOST_REVIEWED" -> BY_REVIEWS_DESC;
35 default -> DEFAULT_RANKING;
36 };
37
38 return results.stream()
39 .sorted(comparator)
40 .toList();
41 }
42
43 public static void printResults(List<SearchResult> results, String label) {
44 System.out.println("=".repeat(68));
45 System.out.println(" " + label);
46 System.out.println("=".repeat(68));
47 for (int i = 0; i < results.size(); i++) {
48 System.out.printf(" %d. %s%n", i + 1, results.get(i));
49 }
50 System.out.println("=".repeat(68));
51 }
52
53 public static void main(String[] args) {
54
55 List<SearchResult> catalogue = List.of(
56 new SearchResult("P001","Samsung Galaxy A54", 28999, 4.3, 18420, false, 150),
57 new SearchResult("P002","Redmi Note 12 Pro", 18999, 4.4, 32100, false, 320),
58 new SearchResult("P003","iQOO Z6 Lite", 12999, 4.1, 9870, true, 200),
59 new SearchResult("P004","OnePlus Nord CE 3", 24999, 4.5, 25300, false, 80),
60 new SearchResult("P005","Realme 11 Pro", 19999, 4.2, 11400, true, 180),
61 new SearchResult("P006","POCO X5 Pro", 17999, 4.6, 8900, false, 90),
62 new SearchResult("P007","Moto G84 5G", 17999, 4.3, 14600, false, 220)
63 );
64
65 // Same data, four different orderings via Comparator
66 printResults(rank(catalogue, "DEFAULT"), "DEFAULT — sponsored, rating, reviews, price");
67 System.out.println();
68 printResults(rank(catalogue, "PRICE_LOW"), "PRICE LOW TO HIGH");
69 System.out.println();
70 printResults(rank(catalogue, "TOP_RATED"), "TOP RATED FIRST");
71 System.out.println();
72 printResults(rank(catalogue, "MOST_REVIEWED"),"MOST REVIEWED FIRST");
73 }
74}Output:
====================================================================
DEFAULT — sponsored, rating, reviews, price
====================================================================
1. [P003] iQOO Z6 Lite Rs. 12999 4.1* (9870 reviews) [AD]
2. [P005] Realme 11 Pro Rs. 19999 4.2* (11400 reviews) [AD]
3. [P006] POCO X5 Pro Rs. 17999 4.6* (8900 reviews)
4. [P004] OnePlus Nord CE 3 Rs. 24999 4.5* (25300 reviews)
5. [P002] Redmi Note 12 Pro Rs. 18999 4.4* (32100 reviews)
6. [P007] Moto G84 5G Rs. 17999 4.3* (14600 reviews)
7. [P001] Samsung Galaxy A54 Rs. 28999 4.3* (18420 reviews)
====================================================================
====================================================================
PRICE LOW TO HIGH
====================================================================
1. [P003] iQOO Z6 Lite Rs. 12999 4.1* (9870 reviews) [AD]
2. [P006] POCO X5 Pro Rs. 17999 4.6* (8900 reviews)
3. [P007] Moto G84 5G Rs. 17999 4.3* (14600 reviews)
4. [P002] Redmi Note 12 Pro Rs. 18999 4.4* (32100 reviews)
5. [P005] Realme 11 Pro Rs. 19999 4.2* (11400 reviews) [AD]
6. [P004] OnePlus Nord CE 3 Rs. 24999 4.5* (25300 reviews)
7. [P001] Samsung Galaxy A54 Rs. 28999 4.3* (18420 reviews)
====================================================================
====================================================================
TOP RATED FIRST
====================================================================
1. [P006] POCO X5 Pro Rs. 17999 4.6* (8900 reviews)
2. [P004] OnePlus Nord CE 3 Rs. 24999 4.5* (25300 reviews)
3. [P002] Redmi Note 12 Pro Rs. 18999 4.4* (32100 reviews)
4. [P001] Samsung Galaxy A54 Rs. 28999 4.3* (18420 reviews)
5. [P007] Moto G84 5G Rs. 17999 4.3* (14600 reviews)
6. [P005] Realme 11 Pro Rs. 19999 4.2* (11400 reviews) [AD]
7. [P003] iQOO Z6 Lite Rs. 12999 4.1* (9870 reviews) [AD]
====================================================================
====================================================================
MOST REVIEWED FIRST
====================================================================
1. [P002] Redmi Note 12 Pro Rs. 18999 4.4* (32100 reviews)
2. [P004] OnePlus Nord CE 3 Rs. 24999 4.5* (25300 reviews)
3. [P001] Samsung Galaxy A54 Rs. 28999 4.3* (18420 reviews)
4. [P007] Moto G84 5G Rs. 17999 4.3* (14600 reviews)
5. [P005] Realme 11 Pro Rs. 19999 4.2* (11400 reviews) [AD]
6. [P003] iQOO Z6 Lite Rs. 12999 4.1* (9870 reviews) [AD]
7. [P006] POCO X5 Pro Rs. 17999 4.6* (8900 reviews)
====================================================================
Performance Considerations
Comparator.compare() is called O(n log n) times during a sort. The method should be O(1) — constant time regardless of input size. A compare() that calls a database, reads a file, or performs an O(n) collection operation converts a sort into a performance disaster.
SORT CALL COUNTS:
Sorting n elements → O(n log n) compare() calls
n = 1,000 → ~10,000 calls
n = 1,000,000 → ~20,000,000 calls
COMPARATOR COST GUIDELINES:
Field comparison → O(1) — String.compareTo, Double.compare, Integer.compare
Method call overhead → negligible (JIT inlines lambdas)
Comparator.comparing() → key extractor called once per comparison — same as manual lambda
.thenComparing() chains → each chained comparison only runs when previous returns 0
.reversed() → swaps arguments — no extra cost beyond method call
SCHWARTZIAN TRANSFORM for expensive key extraction:
If the key extraction is costly (JSON parse, regex, I/O), extract once:
list.stream()
.map(item -> new AbstractMap.SimpleEntry<>(expensiveKey(item), item))
.sorted(Map.Entry.comparingByKey())
.map(Map.Entry::getValue)
.toList();
This calls expensiveKey() exactly once per element, not once per comparison.
Best Practices
Use Comparator.comparing() factory methods instead of raw lambdas for clarity. Comparator.comparingDouble(Employee::salary) reads as a specification — "compare by salary". (a, b) -> Double.compare(a.salary(), b.salary()) is equivalent but reads as an implementation. The factory form also prevents the subtraction-overflow bug: developers familiar with the lambda form sometimes write (a, b) -> a.id() - b.id(), which overflows for extreme values. Comparator.comparingInt(Employee::id) is immune to this.
Chain comparators explicitly when partial reversal is needed. .reversed() on the entire chain reverses every level. Comparator.comparing(X::dept).thenComparing(Comparator.comparingDouble(X::salary).reversed()) reverses only the salary level while keeping department ascending. Failing to understand this leads to subtle bugs where the entire sort order is unexpectedly inverted. When in doubt, write the partial reversal inline and add a comment.
Store reusable Comparators as named constants or static fields. private static final Comparator<Product> BY_PRICE = Comparator.comparingDouble(Product::price) is self-documenting and reusable across the class. Anonymous lambdas inline in sort() calls are harder to test, harder to name, and create minor object allocation overhead on each call.
Use Comparator.nullsFirst() or Comparator.nullsLast() for nullable fields. Calling this.name.compareTo(other.name) inside a comparator throws NullPointerException for null fields. Comparator.comparing(Employee::name, Comparator.nullsLast(Comparator.naturalOrder())) handles null cleanly and declaratively — nulls sort to the end without any manual null checks.
Common Mistakes
Mistake 1 — Subtraction Instead of compare() in Lambda
1// WRONG — integer overflow: Integer.MIN_VALUE - 1 = Integer.MAX_VALUE
2Comparator<Employee> byId = (a, b) -> a.id() - b.id();
3// When a.id() = Integer.MIN_VALUE, b.id() = 1:
4// result = Integer.MIN_VALUE - 1 = Integer.MAX_VALUE → wrong order!
5
6// CORRECT — comparingInt() or Integer.compare(), no overflow risk
7Comparator<Employee> byId2 = Comparator.comparingInt(Employee::id);
8// OR:
9Comparator<Employee> byId3 = (a, b) -> Integer.compare(a.id(), b.id());Mistake 2 — Misunderstanding Where reversed() Applies
1// WRONG — reversed() after thenComparing reverses the ENTIRE chain
2Comparator<Employee> wrong =
3 Comparator.comparing(Employee::department)
4 .thenComparingDouble(Employee::salary)
5 .reversed(); // REVERSES BOTH department AND salary
6
7// Result: department DESC, salary DESC — not what was intended
8
9// CORRECT — reverse only salary while keeping department ascending
10Comparator<Employee> correct =
11 Comparator.comparing(Employee::department) // dept ASC
12 .thenComparing(
13 Comparator.comparingDouble(Employee::salary).reversed()); // salary DESC onlyMistake 3 — Using TreeSet with a Comparator That Returns 0 for Distinct Objects
1// WRONG — Comparator only compares by department; two employees in same
2// department compare as 0 → TreeSet treats them as duplicates, second ignored
3TreeSet<Employee> byDept = new TreeSet<>(
4 Comparator.comparing(Employee::department) // no tiebreaker!
5);
6byDept.add(new Employee(1, "Priya", "Engineering", 92000));
7byDept.add(new Employee(2, "Rohan", "Engineering", 78000)); // SILENTLY DROPPED!
8System.out.println(byDept.size()); // 1, not 2
9
10// CORRECT — always chain to a unique tiebreaker
11TreeSet<Employee> byDeptSafe = new TreeSet<>(
12 Comparator.comparing(Employee::department)
13 .thenComparingInt(Employee::id) // unique — ensures 0 only for same object
14);
15byDeptSafe.add(new Employee(1, "Priya", "Engineering", 92000));
16byDeptSafe.add(new Employee(2, "Rohan", "Engineering", 78000));
17System.out.println(byDeptSafe.size()); // 2Mistake 4 — Performing Expensive Work Inside compare()
1List<String> filePaths = new ArrayList<>();
2// ... thousands of paths
3
4// WRONG — Files.size() makes a system call on EVERY comparison (O(n log n) I/O calls!)
5filePaths.sort((a, b) -> {
6 try {
7 return Long.compare(
8 java.nio.file.Files.size(java.nio.file.Path.of(a)), // I/O per call
9 java.nio.file.Files.size(java.nio.file.Path.of(b))); // I/O per call
10 } catch (Exception e) { return 0; }
11});
12
13// CORRECT — extract the expensive key ONCE per element before sorting
14record FileWithSize(String path, long size) {}
15filePaths.stream()
16 .map(p -> {
17 try {
18 return new FileWithSize(p, java.nio.file.Files.size(java.nio.file.Path.of(p)));
19 } catch (Exception e) { return new FileWithSize(p, -1); }
20 })
21 .sorted(Comparator.comparingLong(FileWithSize::size))
22 .map(FileWithSize::path)
23 .toList();Interview Questions
Q1. What is the Comparator interface in Java and how does it differ from Comparable?
Comparator<T> is a @FunctionalInterface in java.util that defines sort order externally through int compare(T a, T b). Comparable<T> defines natural ordering inside the class through int compareTo(T other). Comparable is one default ordering baked into the type; Comparator is an external, situational ordering that can vary per use case. A class implements Comparable once. Multiple Comparator instances can exist for the same class. When a Comparator is passed to Collections.sort() or TreeSet, it overrides the class's natural ordering entirely for that operation.
Q2. How does Comparator.comparing() work and why is it preferred over a raw lambda?
Comparator.comparing(keyExtractor) takes a Function<T, Comparable> that extracts a sort key and returns a Comparator that sorts by that key using natural ordering. For primitive fields: comparingInt(), comparingDouble(), comparingLong() avoid boxing. The factory form is preferred because it reads as a specification rather than implementation, prevents the subtraction-overflow bug that appears in manual lambdas, and produces chainable Comparator objects through .thenComparing() and .reversed() without extra code.
Q3. How does thenComparing() work and what does reversed() affect?
thenComparing() creates a new Comparator that first applies the original comparison; only when that returns zero does it apply the secondary comparison. Multiple thenComparing() calls chain left to right: primary, secondary, tertiary. .reversed() negates the entire Comparator it is called on — swapping the return sign for every comparison in the chain. If .reversed() is called on a chain of two fields, both fields reverse. To reverse only one field, wrap just that field in its own .reversed() before chaining: .thenComparing(Comparator.comparingDouble(X::price).reversed()).
Q4. How do you sort a TreeSet or TreeMap with a custom Comparator?
Pass the Comparator to the constructor: new TreeSet<>(comparator) or new TreeMap<>(comparator). This overrides the natural ordering (compareTo) for that specific collection. The comparator is then used for all insertions, lookups, and iterations. The critical constraint: the comparator must return 0 only for objects that are logically identical to the collection — if it returns 0 for two distinct objects, one is silently rejected. Always chain to a unique tiebreaker field (ID, UUID) as the final comparison to prevent silent data loss.
Q5. What is the difference between Comparator.naturalOrder() and Comparator.reverseOrder()?
Comparator.naturalOrder() returns a Comparator that delegates to compareTo() — identical in effect to not providing a comparator at all. It is useful when you need to pass an explicit Comparator object but want natural ordering, for example as the argument to nullsFirst(Comparator.naturalOrder()). Comparator.reverseOrder() returns the reverse of natural ordering — equivalent to Comparator.naturalOrder().reversed(). Used for descending sort on String, Integer, LocalDate, and any Comparable type without writing a lambda.
Q6. How do you implement a null-safe Comparator?
Comparator.nullsFirst(Comparator) and Comparator.nullsLast(Comparator) wrap any comparator to handle null elements. Null elements sort before or after all non-null elements respectively, while non-null elements are compared by the wrapped comparator. For a nullable field within an element: Comparator.comparing(Product::price, Comparator.nullsLast(Comparator.naturalOrder())) sorts products by price with null prices appearing last. Without this wrapper, calling a.price().compareTo(b.price()) throws NullPointerException when any price is null.
FAQs
Is Comparator a functional interface?
Yes. Comparator<T> is annotated with @FunctionalInterface since Java 8. It has one abstract method — compare(T a, T b) — and many default and static methods. Being a functional interface means it can be written as a lambda: Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length()). The default and static methods (reversed(), thenComparing(), comparing()) are not abstract and do not affect the functional interface status.
What is the difference between list.sort(comparator) and Collections.sort(list, comparator)?
Both sort the list in-place using the provided comparator. list.sort(comparator) was added in Java 8 as an instance method on List — it is equivalent to Collections.sort(list, comparator) internally and is the preferred modern form. Collections.sort() predates Java 8. Both are stable sorts — equal elements preserve their original relative order. list.sort(null) uses natural ordering, equivalent to Collections.sort(list).
Does Comparator affect how equals() or contains() works on a List?
No. Comparator only affects sort order. list.contains(), list.indexOf(), and list.remove(Object) all use equals() — they are unaware of any Comparator. Sorting with a Comparator changes the iteration order of the list but does not change how elements are found by value. Only TreeSet and TreeMap use Comparator for both ordering AND equality.
How do I sort by a field that implements Comparable without Comparator.comparing()?
Directly: list.sort((a, b) -> a.getName().compareTo(b.getName())). But Comparator.comparing(Employee::getName) is cleaner and equivalent. Both call String.compareTo() under the hood. For primitive fields, use the appropriate wrapper: Integer.compare(a.getId(), b.getId()) in a lambda or Comparator.comparingInt(Employee::getId) with the factory.
Can I use the same Comparator for both ascending and descending sort?
Yes. comparator.reversed() returns a new Comparator that negates the original. Store one and derive the other: Comparator<Product> byPrice = Comparator.comparingDouble(Product::price); Comparator<Product> byPriceDesc = byPrice.reversed(). This is the recommended approach — you define the logic once and derive both directions.
When does Comparator.comparing() throw a NullPointerException?
When the key extractor returns null and the wrapped comparator does not handle null. Comparator.comparing(Product::name) calls name1.compareTo(name2) — NullPointerException if either name is null. Wrap with null handling: Comparator.comparing(Product::name, Comparator.nullsLast(Comparator.naturalOrder())). Alternatively, handle null in the key extractor itself before the sort.
Summary
Comparator<T> is Java's mechanism for defining sort orders externally. Its single abstract method compare(T a, T b) returns negative, zero, or positive to place a before, at, or after b. Since Java 8, Comparator.comparing() and its primitive variants build type-safe comparators from method references. .thenComparing() chains produce multi-level sort specifications. .reversed() flips the entire chain. Comparator.nullsFirst() and Comparator.nullsLast() handle nullable fields without null checks.
Two rules prevent the most common bugs: use Integer.compare() or comparingInt() for numeric fields (never subtraction — overflow silently reverses the sort), and always chain to a unique tiebreaker when using a Comparator with TreeSet or TreeMap (returning zero for distinct objects silently drops the second entry).
Comparator complements Comparable — the natural ordering belongs in the class, alternative and runtime orderings belong in Comparator. For interviews: explain the difference between the two, demonstrate .thenComparing() chaining, clarify where .reversed() applies in a chain, explain null handling, and know the TreeSet data-loss trap when compare returns 0 for distinct objects.
What to Read Next
See when to use Comparable instead of Comparator.