As a Java developer, you constantly work with collections of objects. While sorting a list of strings or numbers is trivial, what happens when you need to sort a list of your own custom objects, like Employee or Product? How do you tell Java how to order them? By salary? By name? By age? This is precisely the problem the Comparator interface was designed to solve.
In this guide, we’ll take a deep dive into the java.util.Comparator. We’ll explore everything from the classic implementation methods to the elegant and powerful approaches introduced in Java 8. By the end, you’ll be able to handle any custom sorting challenge with confidence.
What Exactly is a Java Comparator?
At its core, a Comparator is an object that encapsulates a specific sorting logic. It’s a functional interface whose main purpose is to define a custom order for a set of objects that might not have a natural ordering or when you need an ordering different from the natural one.
The interface has one crucial abstract method:
int compare(T o1, T o2);
The compare method’s contract is simple but vital. Given two objects, o1 and o2, it returns:
- A negative integer if o1 should come before o2.
- Zero if o1 and o2 are equal in terms of sorting order.
- A positive integer if o1 should come after o2.
This simple contract is the foundation of all custom sorting in Java.
The Classic Showdown: Comparator vs. Comparable
You can’t discuss Comparator without mentioning its sibling, Comparable. They both deal with sorting, but they have fundamentally different designs and use cases.
Here’s a simple analogy:
- Comparable is like an object having an intrinsic, built-in sense of order. For example, a Person object might implement Comparable to sort by its natural ID. The sorting logic is part of the object’s class.
- Comparator is like an external, expert judge. It can look at two Person objects and decide their order based on a specific criteria for that day—maybe by last name, or by age. The sorting logic is separate from the object’s class.
Here’s a quick comparison table:
| Feature | Comparable | Comparator |
|---|---|---|
| Purpose | Defines the single, natural sorting order for a class. | Defines multiple, external sorting strategies for a class. |
| Implementation | The class itself must implement Comparable. | A separate class implements Comparator. |
| Code Impact | Modifies the source code of the class being sorted. | No changes needed to the class being sorted. |
| Method | int compareTo(T o) | int compare(T o1, T o2) |
| Package | java.lang | java.util |
Rule of thumb: Use Comparable for the most obvious, single natural order. For everything else—multiple sort orders, sorting third-party classes you can’t modify—use Comparator.
Let’s Get Coding: Our Example Employee Class
Throughout this guide, we’ll use a simple Employee POJO (Plain Old Java Object) to demonstrate different sorting techniques.
public class Employee {
private long id;
private String name;
private int age;
private double salary;
public Employee(long id, String name, int age, double salary) {
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
}
// Getters and Setters...
public long getId() { return id; }
public String getName() { return name; }
public int getAge() { return age; }
public double getSalary() { return salary; }
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", salary=" + salary +
'}';
}
}
The “Old School” Way: Implementing Comparator (Pre-Java 8)
Before Java 8, there were two common ways to create a Comparator.
1. Using a Separate Class
You could create a dedicated class for each sorting strategy. This is reusable but verbose.
// A comparator to sort employees by name
class SortByName implements Comparator<Employee> {
@Override
public int compare(Employee e1, Employee e2) {
return e1.getName().compareTo(e2.getName());
}
}
// Usage
List<Employee> employees = ...;
Collections.sort(employees, new SortByName());
2. Using an Anonymous Inner Class
For one-off sorting tasks, an anonymous inner class was more concise. It avoided creating a whole new .java file, but the syntax was still bulky.
Collections.sort(employees, new Comparator<Employee>() {
@Override
public int compare(Employee e1, Employee e2) {
return Integer.compare(e1.getAge(), e2.getAge());
}
});
While these methods still work, they feel clunky in modern Java. Let’s see a better way.
The Modern Approach: Java 8 Lambdas and Method References
Java 8 revolutionized how we work with interfaces like Comparator. The introduction of lambda expressions and helper methods made creating comparators incredibly clean and readable.
1. Using a Lambda Expression
Since Comparator is a functional interface, we can replace the verbose anonymous class with a concise lambda. Let’s sort our employees by age:
// Lambda to sort by age
employees.sort((Employee e1, Employee e2) -> Integer.compare(e1.getAge(), e2.getAge()));
// Java can infer the types, so we can make it even shorter:
employees.sort((e1, e2) -> Integer.compare(e1.getAge(), e2.getAge()));
We use list.sort() which was added in Java 8, but Collections.sort() works with lambdas too. This is a massive improvement!
2. The Best Way: Comparator.comparing()
Java 8 went a step further by adding static helper methods to the Comparator interface itself. The most useful one is Comparator.comparing().
It takes a function that extracts a sort key from an object and returns a Comparator for that key. This is best shown with a method reference.
// Sort by name
employees.sort(Comparator.comparing(Employee::getName));
// Sort by age
employees.sort(Comparator.comparing(Employee::getAge));
// Sort by salary
employees.sort(Comparator.comparing(Employee::getSalary));
This code is declarative, fluent, and incredibly easy to read. It clearly states, “Sort employees by comparing their names.” For primitive types, you can even use specialized versions to avoid boxing overhead:
- comparingInt()
- comparingLong()
- comparingDouble()
// Using comparingInt is more efficient for primitives
employees.sort(Comparator.comparingInt(Employee::getAge));
Advanced Sorting: Chaining and Reversing
Real-world sorting often has multiple levels. For example, “sort by name, and if names are the same, sort by age.” The modern Comparator API makes this trivial.
Chaining with thenComparing()
The thenComparing() method lets you create a chain of comparison criteria. It’s only used if the previous comparator in the chain found the objects to be equal.
// Sort by name, then by age
Comparator<Employee> byNameThenByAge = Comparator.comparing(Employee::getName)
.thenComparing(Employee::getAge);
employees.sort(byNameThenByAge);
Reverse Order with reversed()
Need to sort in descending order? Just call the reversed() method on any comparator.
// Sort by salary in descending order (highest first)
employees.sort(Comparator.comparingDouble(Employee::getSalary).reversed());
Handling Null Values Gracefully
A common source of NullPointerException during sorting is when the collection or the sort key is null. The Comparator interface provides elegant solutions.
You can wrap an existing comparator with nullsFirst() or nullsLast() to define how null elements should be treated.
// Sort by name, but keep null names at the beginning of the list
employees.sort(Comparator.comparing(Employee::getName, Comparator.nullsFirst(String::compareTo)));
Wait, what’s String::compareTo? When using Comparator.comparing with a key that can be null (like the name string), we need to tell it how to compare non-null values. String::compareTo is the natural order for strings.
Let’s make it even simpler. What if an entire Employee object is null in the list?
// Sort employees by name, with null employees pushed to the end
Comparator<Employee> byName = Comparator.comparing(Employee::getName);
employees.sort(Comparator.nullsLast(byName));
Putting It All Together: A Complete Example
Here is a complete, runnable example that demonstrates all the modern techniques we’ve discussed.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class ComparatorDemo {
public static void main(String[] args) {
List employees = new ArrayList<>(List.of(
new Employee(101, "Zoe", 29, 80000),
new Employee(102, "Alex", 45, 120000),
new Employee(103, "Bob", 29, 85000),
new Employee(104, "Alex", 35, 110000)
));
System.out.println("--- Original List ---");
employees.forEach(System.out::println);
// 1. Sort by name (using method reference)
System.out.println("--- Sorted by Name (Ascending) ---");
employees.sort(Comparator.comparing(Employee::getName));
employees.forEach(System.out::println);
// 2. Sort by salary (descending)
System.out.println("--- Sorted by Salary (Descending) ---");
employees.sort(Comparator.comparingDouble(Employee::getSalary).reversed());
employees.forEach(System.out::println);
// 3. Multi-level sorting: by name, then by age
System.out.println("--- Sorted by Name, then by Age ---");
employees.sort(
Comparator.comparing(Employee::getName)
.thenComparingInt(Employee::getAge)
);
employees.forEach(System.out::println);
// 4. Handling nulls
employees.add(new Employee(105, null, 40, 90000));
System.out.println("--- Sorted by Name (Nulls First) ---");
employees.sort(Comparator.comparing(Employee::getName, Comparator.nullsFirst(String.CASE_INSENSITIVE_ORDER)));
employees.forEach(System.out::println);
}
}
Conclusion and Key Takeaways
The Comparator interface is an essential tool in any Java developer’s toolkit. While older methods exist, embracing the Java 8+ features will make your code more readable, maintainable, and expressive.
- Comparator is for defining external, custom sorting logic.
- Prefer Comparator over Comparable when you need multiple sort orders or cannot modify the class source code.
- For modern Java, always favor Comparator.comparing() with method references for clean, fluent, and type-safe sorting.
- Chain comparators with thenComparing() for multi-level sorting.
- Easily handle descending order with .reversed() and nulls with nullsFirst()/nullsLast().
Next time you’re faced with a sorting problem, remember the power and elegance of the modern Java Comparator API. Happy coding!