24. Comparable, Comparator & Sorting in Java
Table of Contents
- 24.1 Comparable — Natural Ordering
- 24.2 Comparator — Custom Ordering
- 24.3 Comparable vs Comparator
- 24.4 Sorting Arrays and Collections
- 24.5 Multi-Level Sorting thenComparing
- 24.6 Comparing Primitives Efficiently
- 24.7 Common Traps
- 24.8 Full Example
- 24.9 Summary
Java provides two main strategies for sorting and comparing: Comparable (natural ordering) and Comparator (custom ordering).
Understanding their rules, constraints, and interactions with generics is essential.
- For numeric types, sorting follows natural numerical order, meaning smaller values come before larger ones.
- Sorting strings follows lexicographical (Unicode code point) order: character-by-character comparison; digits come before uppercase, uppercase before lowercase.
This ordering is based on each character’s Unicode code point, not alphabetical intuition.
A Unicode code point is a unique numerical value assigned to a character in the Unicode standard.
More precisely:
a Unicode code point is an integer (written in hexadecimal as U+XXXX) that represents a specific character, symbol, or control mark—independent of font, language, or platform.
- Examples:
- U+0041 → A
- U+0061 → a
- U+0030 → 0
- U+1F600 → 😀
A code point is not a byte sequence. It’s an abstract number.
How a code point is stored in memory depends on the encoding (UTF-8, UTF-16, UTF-32).
Unicode defines code points from U+0000 to U+10FFFF.
In short: Unicode code points define what the character is; encodings define how it is represented in bytes.
- Example natural ordering
List<String> items = List.of("10", "2", "A", "Z", "a", "b");
List<String> sorted = new ArrayList<>(items);
Collections.sort(sorted);
System.out.println(sorted);
Output:
[10, 2, A, Z, a, b]
Note
Natural ordering is only defined for types that implement Comparable.
24.1 Comparable — Natural Ordering
The interface Comparable<T> defines the natural order of a type.
A class implements it when it wants to define its default sorting rule.
24.1.1 Comparable Method Contract
public interface Comparable<T> {
int compareTo(T other);
}
Rules and expectations:
- Return negative →
this<other - Return zero →
this==other - Return positive →
this>other
Important
- Natural ordering must be consistent with
equals(), unless explicitly documented otherwise: compareTo()is consistent withequals()if, and only if,a.compareTo(b) == 0anda.equals(b) is true.
Warning
compareTo may throw ClassCastException if given a non-comparable type — but this usually appears only with raw types.
24.1.2 Example: Class Implementing Comparable
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String n, int a) {
this.name = n;
this.age = a;
}
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
var list = List.of(new Person("Bob", 40), new Person("Alice", 30));
list.stream().sorted().forEach(p -> System.out.println(p.getAge()));
The list sorts by age, because that is the natural numbering order.
24.1.3 Common Comparable Pitfalls
- Compare all relevant fields → inconsistent results if not
- Violating transitivity → leads to undefined behavior
- Throwing exceptions inside compareTo() breaks sorting
- Failing to implement the same logic as equals() → common trap
24.2 Comparator — Custom Ordering
The interface Comparator<T> allows defining multiple sorting strategies
without modifying the class itself.
24.2.1 Comparator Core Methods
int compare(T a, T b);
Additional helper methods:
24.2.1.1 Comparator Helper Static Methods
| Method | Static / Instance | Return Type | Parameters | Description |
|---|---|---|---|---|
Comparator.comparing(keyExtractor) |
static | Comparator |
Function<? super T, ? extends U> | Builds a comparator comparing extracted keys using natural ordering. |
Comparator.comparing(keyExtractor, keyComparator) |
static | Comparator |
Function |
Builds comparator comparing extracted keys using a custom comparator. |
Comparator.comparingInt(keyExtractor) |
static | Comparator |
ToIntFunction |
Optimized comparator for int keys (avoids boxing). |
Comparator.comparingLong(keyExtractor) |
static | Comparator |
ToLongFunction |
Optimized comparator for long keys. |
Comparator.comparingDouble(keyExtractor) |
static | Comparator |
ToDoubleFunction |
Optimized comparator for double keys. |
Comparator.naturalOrder() |
static | Comparator |
none | Comparator using natural ordering (Comparable). |
Comparator.reverseOrder() |
static | Comparator |
none | Reverse natural ordering. |
Comparator.nullsFirst(comparator) |
static | Comparator |
Comparator |
Wraps comparator so nulls compare before non-nulls. |
Comparator.nullsLast(comparator) |
static | Comparator |
Comparator |
Wraps comparator so nulls compare after non-nulls. |
24.2.1.2 Instance Methods on Comparator
| Method | Static / Instance | Return Type | Parameters | Description |
|---|---|---|---|---|
thenComparing(otherComparator) |
instance | Comparator |
Comparator |
Adds a secondary comparator when the primary compares equal. |
thenComparing(keyExtractor) |
instance | Comparator |
Function |
Secondary comparison using natural ordering of extracted key. |
thenComparing(keyExtractor, keyComparator) |
instance | Comparator |
Function |
Secondary comparison with custom comparator. |
thenComparingInt(keyExtractor) |
instance | Comparator |
ToIntFunction |
Secondary numeric comparison (optimized). |
thenComparingLong(keyExtractor) |
instance | Comparator |
ToLongFunction |
Secondary numeric comparison. |
thenComparingDouble(keyExtractor) |
instance | Comparator |
ToDoubleFunction |
Secondary numeric comparison. |
reversed() |
instance | Comparator |
none | Returns a reversed comparator for the same comparison logic. |
24.2.2 Comparator Example
var people = List.of(new Person("Bob",40), new Person("Ann",30));
Comparator<Person> byName = Comparator.comparing(Person::getName);
Comparator<Person> byAgeDesc = Comparator.comparingInt(Person::getAge).reversed();
var sorted = people.stream().sorted(byName.thenComparing(byAgeDesc)).toList();
24.3 Comparable vs Comparator
| Feature | Comparable | Comparator |
|---|---|---|
| Package | java.lang | java.util |
| Method | compareTo(T) | compare(T,T) |
| Sorting Type | Natural (default) | Custom (multiple strategies) |
| Modifies Source Class | YES | NO |
| Useful For | Default ordering | External or alternate ordering |
| Allows Multiple Orders | NO | YES |
| Used By Collections.sort | YES | YES |
| Used By Arrays.sort | YES | YES |
24.4 Sorting Arrays and Collections
24.4.1 Arrays sort()
int[] nums = {3,1,2};
Arrays.sort(nums); // natural order
Person[] arr = {...};
Arrays.sort(arr); // Person must implement Comparable
Arrays.sort(arr, byName); // using Comparator
24.4.2 Collections sort()
Collections.sort(list); // natural order
Collections.sort(list, byName); // comparator
Note
Collections.sort(list) delegates to list.sort(comparator) since Java 8.
24.5 Multi-Level Sorting (thenComparing)
var cmp = Comparator
.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparingInt(Person::getAge);
24.6 Comparing Primitives Efficiently
Comparator.comparingInt(Person::getAge)
Comparator.comparingLong(...)
Comparator.comparingDouble(...)
Note
These avoid boxing and are preferred in performance-sensitive code.
24.7 Common Traps
- Sorting a list of Objects without Comparable → runtime ClassCastException
- compareTo inconsistent with equals → unpredictable behavior
- Comparator that breaks transitivity → sorting becomes undefined
- Null elements → unless Comparator handles them, sorting throws NPE
- Comparator comparing fields of mixed types → ClassCastException
- Using subtraction to compare ints can overflow → always use
Integer.compare() - Sorting a list with null elements and natural order → NPE
- compareTo must never return inconsistent negative/zero/positive on same two objects (no randomness)
24.8 Full Example
record Book(String title, double price, int year) {}
var books = List.of(
new Book("Java 17", 40.0, 2021),
new Book("Algorithms", 55.0, 2019),
new Book("Java 21", 42.0, 2023)
);
Comparator<Book> cmp =
Comparator
.comparingDouble(Book::price)
.thenComparing(Book::year)
.reversed();
books.stream().sorted(cmp)
.forEach(System.out::println);
Note
reversed() applies to the entire composed comparator, not just the first comparison key.
24.9 Summary
- Use
Comparablefor natural ordering (1 default order). - Use
Comparatorfor flexible or multiple sorting strategies. - Comparators can compose (reversed, thenComparing).
- Sorting requires consistent comparison logic.
- Arrays.sort and Collections.sort use both Comparable and Comparator.