Java Comparable and Comparator
Whenever you sort a list of custom objects in Java — employees by salary, products by price, students by grade — the runtime needs a rule for deciding which of two objects comes first. Java gives you two tools for this: Comparable, which lets a class define its own single "natural" ordering, and Comparator, which lets you define any number of external, swappable orderings without touching the class at all. Understanding both is essential for sorting lists and for using ordered collections like TreeSet, TreeMap, and PriorityQueue.
Overview: How It Works
Comparable<T> lives in java.lang and declares one method: int compareTo(T other). A class implements it to say "this is how instances of me are naturally ordered." The method must return a negative number if this comes before other, zero if they are considered equal for ordering purposes, and a positive number if this comes after other.
The contract is strict: compareTo must be an antisymmetric total ordering. If x.compareTo(y) is negative, then y.compareTo(x) must be positive (and vice versa). If x.compareTo(y) == 0, then x and y must compare equally to every other object in the same way — this is called transitivity. Java strongly recommends (though does not require) that compareTo be consistent with equals: if x.compareTo(y) == 0, then x.equals(y) should also be true. Sorted collections like TreeSet and TreeMap rely entirely on compareTo (or a supplied comparator) to determine both ordering and uniqueness — they never call equals or hashCode for element identity.
Comparator<T> lives in java.util and is a separate, standalone object with one abstract method: int compare(T o1, T o2). Because it is a functional interface, you can write one as a lambda or method reference. Unlike Comparable, a comparator does not live inside the class being compared, so you can define as many orderings as you like — by name, by price, by date — without modifying the original class, and you can even sort classes you don’t own (like String or classes from a library). Comparator also ships with powerful default and static methods: reversed(), thenComparing(), Comparator.comparing(), Comparator.naturalOrder(), and more, which let you build multi-field comparisons declaratively instead of writing nested if-statements by hand.
Syntax
Implementing natural ordering with Comparable:
class ClassName implements Comparable<ClassName> {
public int compareTo(ClassName other) {
// return negative, zero, or positive
}
}
Defining an external ordering with Comparator:
Comparator<ClassName> byField = (a, b) -> { /* return negative, zero, or positive */ };
// or, using the fluent builder
Comparator<ClassName> byField = Comparator.comparing(ClassName::getField);
| Piece | Meaning |
|---|---|
Comparable<T> |
Interface a class implements to define its own natural ordering; one method, compareTo |
compareTo(T other) |
Compares this to other; negative/zero/positive result |
Comparator<T> |
Standalone object passed to sorting/collection methods to define an external ordering |
compare(T o1, T o2) |
Compares o1 to o2; negative/zero/positive result |
Comparator.comparing(keyExtractor) |
Builds a comparator from a field-extracting method reference or lambda |
thenComparing(...) |
Adds a tie-breaking comparison used when the previous one returns zero |
reversed() |
Returns a comparator with the opposite ordering |
Examples
Example 1: Natural ordering with Comparable
import java.util.*;
class Employee implements Comparable<Employee> {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() { return name; }
public double getSalary() { return salary; }
@Override
public int compareTo(Employee other) {
return Double.compare(this.salary, other.salary);
}
@Override
public String toString() {
return name + ": $" + salary;
}
}
public class Main {
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>();
employees.add(new Employee("Alice", 75000));
employees.add(new Employee("Bob", 52000));
employees.add(new Employee("Carol", 91000));
Collections.sort(employees);
for (Employee e : employees) {
System.out.println(e);
}
}
}
Output:
Bob: $52000.0
Alice: $75000.0
Carol: $91000.0
Because Employee implements Comparable<Employee>, Collections.sort knows exactly how to order the list without any extra arguments — it simply calls compareTo on pairs of employees as it sorts. Salary comparison uses Double.compare rather than manual subtraction, which is the safe, recommended approach (explained in Common Mistakes below).
Example 2: Custom orderings with Comparator
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>(Arrays.asList("banana", "kiwi", "apple", "fig"));
Comparator<String> byLength = (a, b) -> a.length() - b.length();
names.sort(byLength);
System.out.println(names);
Comparator<String> byLengthThenAlpha = Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder());
names.sort(byLengthThenAlpha);
System.out.println(names);
names.sort(byLengthThenAlpha.reversed());
System.out.println(names);
}
}
Output:
[fig, kiwi, apple, banana]
[fig, kiwi, apple, banana]
[banana, apple, kiwi, fig]
This example never modifies String itself — instead it builds three different Comparator objects. The first sorts by length using a lambda. The second uses Comparator.comparingInt combined with thenComparing to add a tie-breaker (here it produces the same order, since all lengths happen to be unique). The third reuses that comparator and flips it with reversed(). This is the core value of Comparator: you can express many orderings without ever touching the class being sorted.
Example 3: Multi-field sorting on a real object
import java.util.*;
class Person implements Comparable<Person> {
private String firstName;
private String lastName;
private int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return firstName + " " + lastName + " (" + age + ")";
}
}
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("John", "Smith", 34));
people.add(new Person("Anna", "Adams", 28));
people.add(new Person("John", "Adams", 34));
people.add(new Person("Zoe", "Baker", 22));
Collections.sort(people);
System.out.println("By age (natural order): " + people);
Comparator<Person> byLastThenFirst = Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName);
people.sort(byLastThenFirst);
System.out.println("By last name, then first: " + people);
}
}
Output:
By age (natural order): [Zoe Baker (22), Anna Adams (28), John Smith (34), John Adams (34)]
By last name, then first: [Anna Adams (28), John Adams (34), Zoe Baker (22), John Smith (34)]
Person defines age as its one natural ordering via Comparable. Notice that the two 34-year-olds keep their original relative order (John Smith before John Adams) — this is because Java’s sort is stable. The second sort swaps in a completely different, unrelated ordering (last name, then first name) using a Comparator, built with thenComparing as a tie-breaker, all without changing Person or its compareTo method.
Under the Hood
Collections.sort and List.sort use a highly optimized, stable sorting algorithm called TimSort (a hybrid of merge sort and insertion sort). Stability means elements that compare as equal retain their original relative order, which is why the tied ages in Example 3 didn’t get shuffled. TimSort runs in O(n log n) time and calls your compareTo or compare method roughly n log n times, so keeping that method fast and side-effect-free matters for performance.
Because TimSort assumes the comparator obeys the total-ordering contract, a broken comparator (one that isn’t transitive or isn’t antisymmetric) can make the sort throw IllegalArgumentException: Comparison method violates its general contract! at runtime, even though the code compiles fine. This exception is Java actively protecting you from a subtly wrong comparator.
TreeSet and TreeMap work differently: internally they are red-black trees, and every insertion walks the tree calling compareTo (or the tree’s comparator) to decide whether to go left or right. If that call ever returns 0 for two elements, the tree treats them as duplicates — the new one is discarded, or in a map, the old value is replaced. This is why compareTo being inconsistent with equals is dangerous specifically for sorted collections: two objects that are "different" by equals can silently vanish if they compare as equal.
Common Mistakes
Mistake 1: Subtracting numbers directly in compareTo
A very common shortcut for comparing integers is subtraction, but it can silently overflow for large or negative values, producing a wrong sign and a broken ordering:
// Inside a class implementing Comparable<Person>
@Override
public int compareTo(Person other) {
return this.age - other.age; // BUG: can overflow with extreme int values
}
If this.age is a large positive number and other.age is a large negative one, the subtraction can overflow int‘s range and wrap around to the wrong sign. Always use the boxed type’s static compare method instead, which handles this correctly:
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age); // safe, no overflow
}
Mistake 2: compareTo inconsistent with equals in a TreeSet
Because TreeSet uses compareTo alone to decide uniqueness, comparing on only part of an object’s state can cause elements to disappear:
import java.util.*;
class Point implements Comparable<Point> {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public int compareTo(Point other) {
return Integer.compare(this.x, other.x); // only compares x!
}
@Override
public String toString() { return "(" + x + ", " + y + ")"; }
}
public class Main {
public static void main(String[] args) {
Set<Point> points = new TreeSet<>();
points.add(new Point(1, 5));
points.add(new Point(1, 9));
points.add(new Point(2, 3));
System.out.println(points);
System.out.println("Size: " + points.size());
}
}
Output:
[(1, 5), (2, 3)]
Size: 2
(1, 9) is silently dropped: since it has the same x as (1, 5), compareTo returns 0, and the TreeSet treats it as a duplicate. The fix is to compare every field that should distinguish two points, using a second field as a tie-breaker:
@Override
public int compareTo(Point other) {
int cmp = Integer.compare(this.x, other.x);
if (cmp != 0) return cmp;
return Integer.compare(this.y, other.y); // tie-break on y too
}
Best Practices
- Use
Integer.compare,Double.compare,Long.compare, etc. instead of subtracting values yourself — they avoid overflow bugs entirely. - Reserve
Comparablefor a single, genuinely natural ordering (like aMoneyclass ordering by amount); useComparatorfor every other ordering, especially ones only needed in one place. - Build multi-field comparators with
Comparator.comparing(...).thenComparing(...)instead of writing manual nestedifchains — it is shorter and far less error-prone. - Try to keep
compareToconsistent withequals, especially for any class you plan to put in aTreeSetor use as aTreeMapkey; document it clearly if you intentionally break this rule. - Never mutate a field that a sorted collection is using for ordering while the object is still inside that collection — the tree structure will become corrupted.
- Keep
compareTo/comparefast and free of side effects, since sorting algorithms may call it many times.
Practice Exercises
- Create a
Bookclass withtitle(String) andprice(double) fields. ImplementComparable<Book>so books sort alphabetically by title, then sort a list of a few books and print the result. - Using the same
Bookclass, write aComparator<Book>(without changingcompareTo) that sorts by price from highest to lowest, using title as a tie-breaker for equal prices. Hint: combineComparator.comparing,reversed(), andthenComparing. - Put several
Bookobjects with the same price but different titles into aTreeSetordered only by price. Predict how many books will remain in the set, then verify by running the code and explaining the result in your own words.
Summary
Comparable<T>is implemented by a class to define one natural ordering viacompareTo.Comparator<T>is a standalone, swappable object withcompare, used for orderings that live outside the class or when you need more than one ordering.- Both methods must return negative, zero, or positive to signal "less than," "equal," or "greater than," and must obey a consistent, transitive contract.
Collections.sort/List.sortuse a stable TimSort algorithm that relies on this contract; violating it can throw an exception at runtime.TreeSet/TreeMapusecompareToor a comparator for both ordering and uniqueness, so an incomplete comparison can silently drop elements.- Use
Integer.compare/Double.compareover manual subtraction, and build multi-field orderings withComparator.comparing().thenComparing().
