6.5 Sorting, Comparable, and Comparator
Key Takeaways
- Comparable<T> defines the natural ordering of a class via int compareTo(T o), returning negative, zero, or positive integer values.
- Comparator<T> defines customizable external sorting strategies via compare(T o1, T o2) and supports rich composition with comparing, thenComparing, and reversed.
- Collections.binarySearch and Arrays.binarySearch require collections/arrays to be pre-sorted according to the searching comparator; otherwise, search results are undefined.
- Binary search returns the non-negative index if found, or (-(insertion point) - 1) if absent, allowing exact deduction of where the searched element belongs.
- Direct integer subtraction (o1.val - o2.val) in comparators is an anti-pattern prone to integer overflow; always use Integer.compare(a, b).
Sorting, Comparable, and Comparator
Sorting, ordering, and binary searching are core capabilities of the Java platform. The Oracle Certified Professional: Java SE 21 Developer (1Z0-830) exam tests your understanding of natural ordering with Comparable, flexible multi-criteria sorting using Comparator factory and chaining methods, in-place sorting APIs, and the exact return value contract of Collections.binarySearch and Arrays.binarySearch.
1. Natural Ordering with Comparable<T>
The java.lang.Comparable<T> interface imposes a natural ordering on the objects of each class that implements it.
package java.lang;
public interface Comparable<T> {
int compareTo(T o);
}
The compareTo Contract
- Returns a negative integer if
this < o. - Returns zero (
0) ifthisis equal too. - Returns a positive integer if
this > o. - Symmetry:
sgn(x.compareTo(y)) == -sgn(y.compareTo(x)). - Transitivity: If
x.compareTo(y) > 0andy.compareTo(z) > 0, thenx.compareTo(z) > 0. - Null Handling: Must throw
NullPointerExceptionifoisnull.
public record Employee(int id, String name, double salary) implements Comparable<Employee> {
@Override
public int compareTo(Employee other) {
// Safe integer comparison avoiding arithmetic overflow
return Integer.compare(this.id, other.id);
}
}
Consistency with equals
A natural ordering is said to be consistent with equals if and only if:
While not strictly required by the compiler, violating this rule creates unexpected behavior in sorted collections (TreeSet, TreeMap) which rely exclusively on compareTo rather than equals to determine element uniqueness:
// BigDecimal: compareTo checks numeric value, equals checks numeric value AND scale!
BigDecimal d1 = new BigDecimal("2.0");
BigDecimal d2 = new BigDecimal("2.00");
Set<BigDecimal> hashSet = new HashSet<>();
hashSet.add(d1); hashSet.add(d2); // size = 2 (d1.equals(d2) is false)
Set<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(d1); treeSet.add(d2); // size = 1 (d1.compareTo(d2) == 0 -> considered duplicate!)
2. Custom Ordering with Comparator<T>
The java.util.Comparator<T> functional interface enables custom sorting logic external to the class definition:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
Static Factory Methods on Comparator
Java provides static helper methods to construct comparators declaratively:
| Factory Method | Description |
|---|---|
Comparator.naturalOrder() | Sorts according to natural Comparable order |
Comparator.reverseOrder() | Sorts in reverse of natural order |
Comparator.comparing(keyExtractor) | Extracts a Comparable sort key (e.g., Employee::name) |
Comparator.comparing(keyExtractor, keyComp) | Extracts key and compares using custom comparator |
Comparator.comparingInt(toIntFunc) | Extracts primitive int to avoid autoboxing overhead |
Comparator.comparingLong(toLongFunc) | Extracts primitive long |
Comparator.comparingDouble(toDoubleFunc) | Extracts primitive double |
Comparator.nullsFirst(comp) | Places null elements at the beginning |
Comparator.nullsLast(comp) | Places null elements at the end |
3. Comparator Chaining and Combinator Methods
Complex multi-tier sorting criteria can be composed cleanly using default methods on Comparator:
// Multi-level sort: Sort by Department ASC, then by Salary DESC, then by Name ASC
Comparator<Employee> comp = Comparator
.comparing(Employee::department)
.thenComparing(Comparator.comparingDouble(Employee::salary).reversed())
.thenComparing(Employee::name);
List<Employee> staff = getEmployees();
staff.sort(comp); // In-place sort using List.sort
Precedence and the .reversed() Placement Trap
The .reversed() method inverts the comparator instance on which it is called. Its placement in a chain dramatically affects the sorting order:
// Trap 1: reversed() at the end inverts the ENTIRE composite comparator!
Comparator<Employee> c1 = Comparator.comparing(Employee::department)
.thenComparing(Employee::salary)
.reversed();
// Result: Department DESC, then Salary DESC
// Trap 2: reversed() attached to the first comparator inverts ONLY that tier
Comparator<Employee> c2 = Comparator.comparing(Employee::department)
.reversed()
.thenComparing(Employee::salary);
// Result: Department DESC, then Salary ASC
Null-Safe Sorting
When collections contain null elements, using a plain comparator throws NullPointerException. Wrap the comparator with nullsFirst or nullsLast:
// Sort names alphabetically, placing null entries at the very end
Comparator<String> safeComp = Comparator.nullsLast(Comparator.naturalOrder());
List<String> names = new ArrayList<>(Arrays.asList("Charlie", null, "Alice", "Bob"));
names.sort(safeComp);
System.out.println(names); // [Alice, Bob, Charlie, null]
4. Sorting APIs in Modern Java
| API Method | Target | In-Place? | Null Comparator Meaning | Sorting Algorithm |
|---|---|---|---|---|
list.sort(comparator) | List<E> | Yes | Uses natural ordering (Comparable) | Timsort (Stable, $O(n \log n)$) |
Collections.sort(list) | List<T extends Comparable> | Yes | Uses natural ordering | Timsort |
Collections.sort(list, comp) | List<T> | Yes | Uses custom Comparator | Timsort |
Arrays.sort(array) | T[] (Objects) | Yes | Uses natural ordering | Timsort (Stable) |
Arrays.sort(primitiveArray) | Primitives (int[], etc.) | Yes | N/A | Dual-Pivot Quicksort (Unstable) |
Arrays.parallelSort(array) | T[] or Primitives | Yes | Natural or Custom | Parallel Merge-Sort |
5. Searching Sorted Collections: binarySearch
Collections.binarySearch and Arrays.binarySearch perform fast $O(\log n)$ searches on sorted datasets.
Mandatory Preconditions
- The collection/array MUST be sorted in ascending order prior to calling
binarySearch. - If sorted using a custom
Comparator, the exact same comparator must be passed tobinarySearch(list, key, comparator). - If the list is unsorted or sorted in reverse without passing the reverse comparator, the return value is undefined.
- If searching a
LinkedList,binarySearchdegrades to $O(n)$ time due to sequential node traversal.
Return Value Mathematical Formula
- Key Found: Returns the index ($i \ge 0$) of the search key. If multiple matching elements exist, which matching index is returned is not guaranteed.
- Key Not Found: Returns a negative integer:
- Insertion Point Definition: The index of the first element greater than the search key, or
list.size()if all elements in the list are smaller than the search key. - Deducing Insertion Point from Return Value:
List<Integer> list = List.of(10, 20, 30, 50, 60);
// Search for 30 (Present at index 2)
int idx1 = Collections.binarySearch(list, 30); // returns 2
// Search for 40 (Absent; would be inserted at index 3)
// Formula: -(3) - 1 = -4
int idx2 = Collections.binarySearch(list, 40); // returns -4
// Search for 5 (Absent; would be inserted at index 0)
// Formula: -(0) - 1 = -1
int idx3 = Collections.binarySearch(list, 5); // returns -1
// Search for 100 (Absent; would be inserted at index 5)
// Formula: -(5) - 1 = -6
int idx4 = Collections.binarySearch(list, 100);// returns -6
6. Common Sorting Pitfalls and Anti-Patterns
1. Integer Subtraction Overflow Anti-Pattern
Writing a comparator using raw arithmetic subtraction:
// DANGEROUS ANTI-PATTERN!
Comparator<Integer> badComp = (a, b) -> a - b;
If a = Integer.MIN_VALUE ($-2,147,483,648$) and b = 1, a - b overflows to $+2,147,483,647$ (positive!), falsely indicating that $a > b$. Always use Integer.compare(a, b).
2. Modifying Sort Keys of Elements in TreeSet / TreeMap
If an object is added to a TreeSet and its sort key field is subsequently modified, the tree's internal ordering invariants are corrupted. The collection can no longer find or remove the modified element, leading to severe memory leaks and data corruption.
Given the sorted list of integers [10, 25, 40, 55, 70], what is the return value of Collections.binarySearch(list, 30)?
Consider the following comparator definition and list sort operation: Comparator<String> comp = Comparator.comparingInt(String::length) .reversed() .thenComparing(Comparator.naturalOrder()); List<String> words = new ArrayList<>(List.of("pear", "apple", "fig", "banana", "kiwi", "plum")); words.sort(comp); System.out.println(words); What is the output of this program?
A developer stores instances of a Person class in a TreeSet. The Person class implements Comparable but defines compareTo based only on 'id', while equals and hashCode are based on both 'id' and 'name'. What happens when adding two Person objects with the same 'id' but different 'name' values to the TreeSet?
Why is the subtraction expression (o1, o2) -> o1.getValue() - o2.getValue() considered an anti-pattern when writing a Comparator for integers?