Advanced Sorting
In short: Sorting by your own, more complex criteria instead of the natural default order — e.g. by several fields at once or in reverse order.
In more detail: Comparator offers chaining methods for this like thenComparing() (secondary sort criterion for ties) and reversed() (reverse the order). This lets you, for example, sort objects first by last name, then by first name in case of a tie, without writing your own complex comparison logic by hand.
list.sort(Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));In Depth
The standard sort() method on a List either takes no argument at all (in which case the class must implement Comparable and define its “natural” order itself via compareTo()) or a Comparator as an argument, which supplies the sorting logic from outside without having to touch the sorted class itself — this is the usual approach when you want to sort by different criteria without changing the class every time.
record Person(String lastName, String firstName, int age) {}
List<Person> people = new ArrayList<>(List.of(
new Person("Miller", "Anna", 30),
new Person("Miller", "Ben", 25),
new Person("Smith", "Zoe", 40)
));
// By last name, then first name for ties, then age descending
people.sort(
Comparator.comparing(Person::lastName)
.thenComparing(Person::firstName)
.thenComparing(Comparator.comparingInt(Person::age).reversed())
);Important building blocks: Comparator.comparing(getter) for a field, .thenComparing(...) chains further criteria for ties, .reversed() reverses the direction, and Comparator.comparingInt/-Long/-Double avoids unnecessary autoboxing for primitive fields (a Comparator.comparing(Person::age) would first wrap every int value into an Integer object in order to compare it generically — a measurable performance difference for very large lists). For collections with a natural order, Collections.sort(list) is often enough on its own, as soon as the elements implement Comparable.
Comparable vs. Comparator
Comparable<T> (with the compareTo() method) is implemented by the class to be sorted itself and defines ONE fixed “natural” order — e.g. String naturally sorts alphabetically, Integer naturally sorts ascending numerically. Comparator<T>, by contrast, comes from OUTSIDE and can provide any number of different sorting logics for the same class, without modifying the class itself:
class Product implements Comparable<Product> {
String name;
double price;
@Override
public int compareTo(Product other) {
return Double.compare(this.price, other.price); // "natural" order: by price
}
}
// Still sortable differently at any time, without changing Product:
products.sort(Comparator.comparing(p -> p.name)); // alphabetically
products.sort(Comparator.comparingDouble((Product p) -> p.price).reversed()); // most expensive firstStability and typical pitfalls
Java’s sorting algorithms (Collections.sort(), Arrays.sort() for objects) are guaranteed to be stable — elements that are equal according to the comparison logic keep their original relative order. This matters for multi-stage sorting: if you sort first by first name and then (stably) by last name, entries with the same last name still end up sorted alphabetically by first name. A common beginner mistake is an inconsistent Comparator that, for example, returns contradictory signs for a.compareTo(b) and b.compareTo(a) (e.g. through faulty subtraction on int values, which can overflow for very large/small numbers) — this leads to IllegalArgumentException: Comparison method violates its general contract! at runtime, often only for larger lists, because smaller lists are handled with different internal sorting algorithms.
See also: Sorting, Lambda, Generics, Collections