Comparable & Comparator
Java provides two mechanisms for ordering objects: Comparable (natural ordering built into the class) and Comparator (external, flexible ordering strategies). Understanding both is essential for sorting collections, using TreeSet/TreeMap, and processing ordered streams.
Comparable — Natural Ordering
Implement Comparable<T> to define the default sort order for a class:
public class Person implements Comparable<Person> {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name() { return name; }
public int age() { return age; }
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name); // sort by name alphabetically
}
}
List<Person> people = new ArrayList<>(List.of(
new Person("Charlie", 30),
new Person("Alice", 25),
new Person("Bob", 35)
));
Collections.sort(people); // uses compareTo — Alice, Bob, Charlie
people.stream().sorted().forEach(System.out::println);
compareTo Contract
The compareTo method must return:
- Negative integer if
thisis less than the argument - Zero if they are equal (by sort order)
- Positive integer if
thisis greater than the argument
Additional requirements:
- Transitive: if
a > bandb > c, thena > c - Consistent with equals: if
compareToreturns 0,equalsshould return true (recommended forTreeSet/TreeMap)
@Override
public int compareTo(Person other) {
int nameCompare = this.name.compareTo(other.name);
if (nameCompare != 0) return nameCompare;
return Integer.compare(this.age, other.age); // tie-break by age
}
Violating the contract causes unpredictable behavior in sorted collections.
Comparator — Custom Ordering
Use Comparator when you need multiple sort orders or cannot modify the class:
// Sort by age
Comparator<Person> byAge = Comparator.comparingInt(Person::age);
people.sort(byAge);
// Sort by name, then by age
Comparator<Person> byNameThenAge = Comparator
.comparing(Person::name)
.thenComparingInt(Person::age);
// Reverse order
people.sort(byAge.reversed());
// Lambda comparator
people.sort((a, b) -> Integer.compare(a.age(), b.age()));
Comparator Factory Methods
Comparator<String> byLength = Comparator.comparingInt(String::length);
Comparator<String> natural = Comparator.naturalOrder();
Comparator<String> reverse = Comparator.reverseOrder();
// Null-safe sorting
Comparator<String> nullsFirst = Comparator.nullsFirst(Comparator.naturalOrder());
Comparator<String> nullsLast = Comparator.nullsLast(Comparator.naturalOrder());
// Extract key then compare
Comparator<Person> byNameLength = Comparator.comparing(p -> p.name().length());
Comparable vs Comparator
| Feature | Comparable | Comparator |
|---|---|---|
| Package | java.lang |
java.util |
| Method | compareTo(T o) |
compare(T o1, T o2) |
| Location | Inside the class | Separate class, lambda, or method reference |
| Sort orders | One (natural) | Multiple independent orders |
| Modifies class | Yes — implements interface | No — external to class |
Sorting Collections and Arrays
// List sorting
List<String> names = new ArrayList<>(List.of("Charlie", "Alice", "Bob"));
names.sort(String::compareToIgnoreCase);
// Array sorting
String[] arr = {"Charlie", "Alice", "Bob"};
Arrays.sort(arr, String.CASE_INSENSITIVE_ORDER);
// TreeSet / TreeMap — require ordering
TreeSet<Person> byAgeSet = new TreeSet<>(Comparator.comparingInt(Person::age));
TreeMap<Person, String> byNameMap = new TreeMap<>(byNameThenAge);
byAgeSet.add(new Person("Alice", 25));
byAgeSet.add(new Person("Bob", 35));
TreeSet and TreeMap use ordering for both sorting and uniqueness — two elements that compare as equal are considered duplicates in a TreeSet.
Sorting with Streams
List<Person> sortedByAge = people.stream()
.sorted(Comparator.comparingInt(Person::age))
.toList();
// Top 3 oldest
List<Person> top3 = people.stream()
.sorted(Comparator.comparingInt(Person::age).reversed())
.limit(3)
.toList();
// min/max with comparator
Person youngest = Collections.min(people, Comparator.comparingInt(Person::age));
Person oldest = Collections.max(people, Comparator.comparingInt(Person::age));
Records and Comparable
Records can implement Comparable directly:
public record Product(String name, BigDecimal price) implements Comparable<Product> {
@Override
public int compareTo(Product other) {
return this.price.compareTo(other.price);
}
}
Or use external comparators without modifying the record:
products.sort(Comparator.comparing(Product::price));
Practical Example: Multi-Field Sort
public record Employee(String department, String name, int salary) { }
List<Employee> staff = new ArrayList<>(/* ... */);
staff.sort(Comparator
.comparing(Employee::department)
.thenComparing(Employee::name)
.thenComparing(Employee::salary, Comparator.reverseOrder()));
This sorts by department (A-Z), then name (A-Z), then salary (highest first within same name).
Best Practices
- Implement
Comparableonly when a single natural order is meaningful and stable - Use
Comparator.comparingand method references instead of anonymous classes - Chain comparators with
thenComparingfor multi-field sorting - Ensure
compareTois consistent withequalswhen usingTreeSet/TreeMap - Use
Comparator.nullsFirst()ornullsLast()when null values are possible - Prefer
Integer.compare(a, b)overa - bto avoid integer overflow bugs
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
TreeSet drops elements |
compareTo returns 0 for non-equal objects |
Make compareTo consistent with equals |
IllegalArgumentException: Comparison method violates contract |
Transitivity broken | Review compareTo logic with edge cases |
| Unexpected sort order | Case sensitivity | Use String.CASE_INSENSITIVE_ORDER |
NullPointerException during sort |
Null elements in collection | Use nullsFirst/nullsLast comparator |
| Integer overflow in subtraction | Using a - b for large values |
Use Integer.compare(a, b) |
Comparable defines how a class sorts itself; Comparator provides flexible, reusable ordering strategies for any type — together they power every sorting operation in the Java collections framework.