Sorting a list of numbers is easy — every number already knows whether it is less than another. But what about a list of Person objects? Should they sort by last name, by age, or by employee ID? Java refuses to guess. It gives you two tools for teaching a collection what comes first: Comparable, for the one natural order a class possesses, and Comparator, for every custom order you invent later. Choosing the right tool, and writing it without hidden bugs, is what separates working code from code that sorts silently wrong or crashes in production with an overflow you never saw coming. Understanding the difference now will save you hours of debugging later.
flowchart TD A["Comparable"] --> B["compareTo"] B --> C["Collections.sort(list)"] D["Comparator"] --> E["compare"] E --> F["list.sort(comparator)"] C --> G["sorted list"] F --> G style A fill:#3776ab,color:#fff style D fill:#c2410c,color:#fff
Comparable: the natural order
When a class knows its own natural sorting rule — alphabetical by name, chronological by date, numerical by ID — it implements Comparable<T> and defines compareTo. The method returns a negative integer if this should come before the other object, zero if they are equal for sorting purposes, and a positive integer if this should come after.
The contract is strict and the Collections framework relies on it. If a.compareTo(b) is negative, then b.compareTo(a) must be positive. If a.compareTo(b) is zero, then a.equals(b) should ideally be true as well; when the two disagree, sorted collections like TreeSet behave unpredictably, treating objects as equal for sorting but not for lookup. Keep compareTo and equals consistent. Breaking the contract — for example, making compareTo asymmetric — does not trigger a compiler error, but it corrupts the output of sorted sets and maps in ways that are maddening to debug.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Main {
static class Person implements Comparable<Person> {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Mara", 30));
people.add(new Person("Jules", 25));
Collections.sort(people);
for (Person p : people) {
System.out.println(p.name + ":" + p.age);
}
}
}
What sort actually does
Collections.sort and list.sort both delegate to TimSort, a hybrid sorting algorithm derived from merge sort and insertion sort. It guarantees O(n log n) time in the worst case and performs well on real-world data that is already partially ordered. For arrays of primitives, Arrays.sort uses a dual-pivot quicksort. You do not choose the algorithm; you choose the rule, and the library chooses the algorithm.
Comparator: order without changing the class
Sometimes you cannot edit the class — it comes from a library — or you need more than one ordering: users by name for a directory, by join date for a report, by reputation for a leaderboard. A Comparator<T> is a separate object that holds the comparison rule. You pass it to list.sort(comparator) or Collections.sort(list, comparator). The method is compare(a, b) and returns the same signed integers as compareTo.
The cleanest way to build a Comparator is a lambda: Comparator<Person> byName = (a, b) -> a.name.compareTo(b.name). Because the compiler infers types from the variable declaration, the lambda stays short. If the logic grows beyond one line, extract it into a static method and obtain a Comparator with Comparator.comparing. That separation is powerful: the same Person class can coexist in a name-sorted directory and an age-sorted leaderboard without either knowing about the other.
flowchart LR L["List of objects"] -->|"natural order"| N["Collections.sort(list)"] L -->|"custom rule"| C["list.sort(comparator)"] N --> R["sorted list"] C --> R style N fill:#3776ab,color:#fff style C fill:#c2410c,color:#fff
import java.util.ArrayList;
import java.util.List;
public class Main {
static class Person {
String name;
int age;
Person(String name, int age) { this.name = name; this.age = age; }
}
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Mara", 30));
people.add(new Person("Jules", 25));
people.sort((a, b) -> a.name.compareTo(b.name));
for (Person p : people) {
System.out.println(p.name);
}
}
}
Chaining: sort by one thing, then another
Real sorting rarely stops at a single field. You want employees by department first, then by seniority within each department, then by name within each seniority level. Writing that as one nested ternary is readable only to the person who wrote it and sometimes not even then.
Comparator offers thenComparing to chain rules left to right: the first comparator decides, and if it returns zero the next one breaks the tie. Each link only runs when every link before it could not decide. The result is a stable, readable sort order that mirrors the business rule. For primitive fields, use thenComparingInt and thenComparingLong to avoid boxing overhead. You can also fall back to a manual chain inside a lambda when you need to compare calculated values that do not map neatly to a field getter.
import java.util.ArrayList;
import java.util.List;
public class Main {
static class Task {
int priority;
String name;
Task(int p, String n) { priority = p; name = n; }
}
public static void main(String[] args) {
List<Task> list = new ArrayList<>();
list.add(new Task(2, "B"));
list.add(new Task(1, "C"));
list.add(new Task(1, "A"));
list.sort((a, b) -> {
int c = Integer.compare(a.priority, b.priority);
if (c != 0) return c;
return a.name.compareTo(b.name);
});
for (Task t : list) {
System.out.println(t.name);
}
}
}
flowchart LR A["compare priority"] -->|"not zero"| R1["use that result"] A -->|"zero"| B["compare name"] B -->|"not zero"| R2["use that result"] B -->|"zero"| R3["equal"] style A fill:#3776ab,color:#fff style B fill:#c2410c,color:#fff
Comparing with method references
Once you are comfortable with lambdas, you can shorten further with a method reference: Comparator.comparing(Person::getName) builds a comparator that calls getName on each object and compares the results. For primitives, comparingInt, comparingLong, and comparingDouble avoid boxing the result. The chained example from earlier becomes Comparator.comparingInt(Task::getPriority).thenComparing(Task::getName) — the same logic, expressed as a pipeline.
Stable sorts preserve original order
Java's sort is stable: if two elements compare as equal, their original relative order is preserved. That matters when you sort in stages — first by date, then by status — because the second sort will not scramble the ordering the first sort established within each status group. Without stability, multi-pass sorting becomes unpredictable.
Comparable sort. This list of Person is sorted by age using Comparable. Predict the exact output (two lines).
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Main {
static class Person implements Comparable<Person> {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Mara", 30));
people.add(new Person("Jules", 25));
Collections.sort(people);
for (Person p : people) {
System.out.println(p.name + ":" + p.age);
}
}
}
Comparable sorts by age, lowest first. Jules is 25, Mara is 30.
Each println produces one line with name:age.
You are using a class from a library and need to sort its instances by two different criteria in different parts of your program. Which approach fits best?
Comparator is designed for external ordering rules. You can create as many as you need — by name, by date, by priority — without changing the original class or its natural order.
Comparator sort. The list is sorted by name using a lambda Comparator. Predict the exact output (two lines).
import java.util.ArrayList;
import java.util.List;
public class Main {
static class Person {
String name;
int age;
Person(String name, int age) { this.name = name; this.age = age; }
}
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Mara", 30));
people.add(new Person("Jules", 25));
people.sort((a, b) -> a.name.compareTo(b.name));
for (Person p : people) {
System.out.println(p.name);
}
}
}
The comparator compares names alphabetically. 'Jules' comes before 'Mara'.
Each println prints one name.
Chained comparator. Tasks are sorted by priority, then by name within the same priority. Predict the exact output (three lines).
import java.util.ArrayList;
import java.util.List;
public class Main {
static class Task {
int priority;
String name;
Task(int p, String n) { priority = p; name = n; }
}
public static void main(String[] args) {
List<Task> list = new ArrayList<>();
list.add(new Task(2, "B"));
list.add(new Task(1, "C"));
list.add(new Task(1, "A"));
list.sort((a, b) -> {
int c = Integer.compare(a.priority, b.priority);
if (c != 0) return c;
return a.name.compareTo(b.name);
});
for (Task t : list) {
System.out.println(t.name);
}
}
}
Priority 1 comes before priority 2, so A and C are before B.
Within priority 1, 'A' comes before 'C' alphabetically.
What is the main risk of writing return this.age - other.age inside compareTo?
Subtracting two ints can overflow when the values are far apart. The wrapped result is negative instead of positive, so the comparator lies to the sort algorithm. Integer.compare avoids this entirely.
Recap
- Comparable gives a class its natural order via compareTo.
- Comparator provides external sort rules via compare without touching the class.
- Return negative, zero, or positive to say before, equal, or after.
- Chain rules with thenComparing when the first field ties.
- Never subtract int fields in compareTo — overflow corrupts order. Use Integer.compare.
- Keep compareTo consistent with equals so sorted collections behave predictably.
Next you will learn to measure the speed of these operations and choose the right collection for the work at hand.