Java Sorting: Comparable, Comparator, and Multi-key Sort
Java sorts via two interfaces: Comparable defines a class's natural ordering (implemented by the class itself), and Comparator defines an external ordering (passed at sort time). Comparator's fluent API (comparing(), thenComparing(), reversed(), nullsFirst()) allows building complex multi-key sort criteria without any if/else logic. Arrays.sort and Collections.sort both use a stable mergesort, so equal elements preserve their original relative order.
Contents
Comparable: Natural Order
Comparable<T>compareTo()thisCollections.sort()Arrays.sort()TreeSet
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: Custom Order
Comparator<T>Comparator.comparing()Comparator.comparingInt()
// 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]]
Chaining: thenComparing and reversed
thenComparing()reversed()
// 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]
Handling Nulls: nullsFirst and nullsLast
Standard comparators throw a NullPointerExceptionnullComparator.nullsFirst()Comparator.nullsLast()
// 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
Sorting with Streams
Stream.sorted()Comparatorlimit()Collectors.groupingBy()
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
Collections.sort()List.sort()thenComparing()