Contents
- Comparable — Natural Order
- Comparator — Custom Order
- Chaining — thenComparing and reversed
- Handling Nulls — nullsFirst and nullsLast
- Sorting with Streams
Comparable<T> is implemented by the class itself to define its natural ordering. compareTo() returns a negative integer, zero, or positive integer when this is less than, equal to, or greater than the argument. Collections.sort(), Arrays.sort(), and sorted collections like TreeSet all rely on this interface:
import java.util.*;
// Comparable — implement on the class to define its natural ordering
// compareTo: negative = this < other, 0 = equal, positive = this > other
record Employee(String name, int salary, String department) implements Comparable<Employee> {
@Override
public int compareTo(Employee other) {
// Natural order: alphabetical by name
return this.name.compareTo(other.name);
}
}
List<Employee> employees = new ArrayList<>(List.of(
new Employee("Charlie", 75000, "Engineering"),
new Employee("Alice", 90000, "Engineering"),
new Employee("Bob", 60000, "Marketing")
));
Collections.sort(employees); // uses compareTo
System.out.println(employees.get(0).name()); // "Alice"
// Arrays also use Comparable
String[] words = {"banana", "apple", "cherry"};
Arrays.sort(words); // natural order (alphabetical)
System.out.println(Arrays.toString(words)); // [apple, banana, cherry]
// Comparable is used by TreeSet/TreeMap for element ordering
TreeSet<Employee> empSet = new TreeSet<>(employees); // sorted by name
System.out.println(empSet.first().name()); // "Alice"
Comparator<T> is an external ordering strategy passed at sort time, allowing multiple sort orders for the same class. Comparator.comparing() and Comparator.comparingInt() create comparators from key-extraction functions and are cleaner than raw lambdas:
// Comparator — external ordering, passed at sort time
// Allows multiple orderings without modifying the class
// Lambda comparator
Comparator<Employee> bySalary = (a, b) -> Integer.compare(a.salary(), b.salary());
// Method reference form — Comparator.comparing is cleaner
Comparator<Employee> bySalary2 = Comparator.comparingInt(Employee::salary);
Comparator<Employee> byName = Comparator.comparing(Employee::name);
Comparator<Employee> byDepartment = Comparator.comparing(Employee::department);
employees.sort(bySalary);
System.out.println(employees.get(0).name()); // "Bob" (lowest salary)
employees.sort(bySalary.reversed()); // descending salary
System.out.println(employees.get(0).name()); // "Alice" (highest salary)
// Comparator.naturalOrder() and reverseOrder() for primitives
List<Integer> nums = new ArrayList<>(List.of(3, 1, 4, 1, 5));
nums.sort(Comparator.naturalOrder()); // [1, 1, 3, 4, 5]
nums.sort(Comparator.reverseOrder()); // [5, 4, 3, 1, 1]
// Comparator for arrays (sort a 2D array by first column, then second)
int[][] grid = {{2, 3}, {1, 5}, {1, 2}, {3, 1}};
Arrays.sort(grid, (a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
// [[1,2], [1,5], [2,3], [3,1]]
thenComparing() adds a tie-breaking secondary (or tertiary) comparator — applied only when the primary comparator considers two elements equal. reversed() flips the entire chain; to reverse only one key, negate the key extractor or wrap just that comparator:
// thenComparing — break ties with a secondary comparator
Comparator<Employee> byDeptThenSalaryDesc =
Comparator.comparing(Employee::department) // primary: department A-Z
.thenComparingInt(Employee::salary) // secondary: salary asc
.reversed() // whole chain reversed
;
// More legible — build in steps
Comparator<Employee> multi =
Comparator.comparing(Employee::department) // 1st key
.thenComparingInt(e -> -e.salary()) // 2nd: salary desc
.thenComparing(Employee::name); // 3rd: name asc
employees.sort(multi);
for (Employee e : employees) {
System.out.printf("%-12s %-10s %d%n", e.department(), e.name(), e.salary());
}
// reversed() flips the entire chain — to reverse just one key:
// thenComparingInt(e -> -e.salary()) or
// thenComparing(Comparator.comparingInt(Employee::salary).reversed())
// Comparing by extracted key with custom logic
List<String> words = List.of("banana", "fig", "apple", "kiwi", "cherry");
List<String> sorted = words.stream()
.sorted(Comparator.comparingInt(String::length) // shorter first
.thenComparing(Comparator.naturalOrder())) // then alpha
.toList();
System.out.println(sorted); // [fig, kiwi, apple, banana, cherry]
Standard comparators throw a NullPointerException if any element or extracted key is null. Comparator.nullsFirst() and Comparator.nullsLast() wrap an existing comparator to handle nulls gracefully, placing them at the beginning or end of the sorted sequence:
// Comparator.comparingInt(Employee::salary) throws NPE if any salary is null
// Wrap with nullsFirst or nullsLast for null-safe sorting
List<String> withNulls = new ArrayList<>(Arrays.asList("banana", null, "apple", null, "cherry"));
// nullsFirst — nulls sort before non-nulls
withNulls.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(withNulls); // [null, null, apple, banana, cherry]
// nullsLast — nulls sort after non-nulls
withNulls.sort(Comparator.nullsLast(Comparator.naturalOrder()));
System.out.println(withNulls); // [apple, banana, cherry, null, null]
// Null-safe field extraction in chained comparators
record Person(String name, String email) {}
List<Person> people = List.of(
new Person("Alice", "alice@x.com"),
new Person("Bob", null),
new Person("Carol", "carol@x.com")
);
Comparator<Person> byEmail =
Comparator.comparing(Person::email, Comparator.nullsLast(Comparator.naturalOrder()));
List<Person> sorted = people.stream().sorted(byEmail).toList();
sorted.forEach(p -> System.out.println(p.name() + " " + p.email()));
// Alice alice@x.com
// Carol carol@x.com
// Bob null
Stream.sorted() returns a new sorted stream without modifying the source. It accepts an optional Comparator; without one it uses natural order. Combine with limit() for top-N queries, or Collectors.groupingBy() to sort within groups:
import java.util.stream.*;
List<Employee> employees = List.of(
new Employee("Charlie", 75000, "Engineering"),
new Employee("Alice", 90000, "Marketing"),
new Employee("Bob", 60000, "Engineering"),
new Employee("Diana", 95000, "Marketing")
);
// Stream.sorted() — returns a new sorted stream (original unchanged)
List<Employee> byName = employees.stream()
.sorted(Comparator.comparing(Employee::name))
.toList();
// Sorted by salary descending, get top 2
List<Employee> topTwo = employees.stream()
.sorted(Comparator.comparingInt(Employee::salary).reversed())
.limit(2)
.toList();
topTwo.forEach(e -> System.out.println(e.name() + ": " + e.salary()));
// Diana: 95000
// Alice: 90000
// Group by department, then sort within each group
Map<String, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.collectingAndThen(
Collectors.toList(),
list -> {
list.sort(Comparator.comparingInt(Employee::salary).reversed());
return list;
}
)
));
byDept.forEach((dept, emps) -> {
System.out.println(dept + ": " + emps.stream().map(Employee::name).toList());
});
// Arrays.sort is in-place; streams produce a new list
int[] arr = {5, 3, 1, 4, 2};
Arrays.sort(arr); // in-place, O(n log n), stable for objects, not for int[]
int[] sorted = Arrays.stream(arr).sorted().toArray(); // stream variant
Both Collections.sort() and List.sort() are stable — elements that compare as equal preserve their original relative order. This makes it safe to sort by a secondary key first, then by the primary key, to achieve multi-key sorting without thenComparing().