Java Comparable and Comparator
Define natural ordering with Comparable and external ordering with Comparator in Java, and compose comparators.
Java Comparable and Comparator
Two interfaces, one job: tell Java when one object is "less than" another. They look almost identical at the call site, and their methods even return the same shape of value — a negative int, zero, or a positive int. The difference is where the order lives:
Comparable<T>— the type itself knows how to order its instances. Itsint compareTo(T other)method is the type's natural ordering.Comparator<T>— an external object that orders instances. Itsint compare(T a, T b)method describes one of many possible orderings.
You implement Comparable when there's one obvious "less than" for a type — Integer, String, LocalDate. You write a Comparator for every other ordering — by length, by case-insensitive name, by descending price, by anything you can express in code. Most types have one Comparable (or none) and dozens of useful Comparators.
The contract: −/0/+
Both methods return an int whose sign is the answer:
- negative —
acomes beforeb - zero — equal for ordering purposes
- positive —
acomes afterb
The exact magnitude doesn't matter. -1 and -1_000_000 mean the same thing. Never return a.size - b.size when overflow is possible: subtracting Integer.MIN_VALUE from a positive number wraps around. Use Integer.compare(a.size(), b.size()) instead — it's overflow-safe and the same number of characters to type.
Comparable<T> — natural ordering
A type implements Comparable<Self> and provides compareTo:
public record Version(int major, int minor, int patch) implements Comparable<Version> {
@Override public int compareTo(Version other) {
int m = Integer.compare(this.major, other.major);
if (m != 0) return m;
int n = Integer.compare(this.minor, other.minor);
if (n != 0) return n;
return Integer.compare(this.patch, other.patch);
}
}Now Collections.sort(versions), versions.stream().sorted(), new TreeSet<Version>(), and new TreeMap<Version, X>() all just work without you passing any extra argument.
The contract has three rules every compareTo must honour:
- Anti-symmetric —
a.compareTo(b)andb.compareTo(a)have opposite signs. - Transitive — if
a < bandb < c, thena < c. - Consistent with
equals(strongly recommended) —a.compareTo(b) == 0iffa.equals(b).
The third rule is the one people break by accident. BigDecimal is the famous example: new BigDecimal("1.0").compareTo(new BigDecimal("1.00")) is 0, but .equals returns false. As a result, a TreeSet<BigDecimal> and a HashSet<BigDecimal> will disagree about whether "1.0" and "1.00" are duplicates. If you can, keep them consistent.
Comparator<T> — external ordering
A Comparator is a separate object. It can compare any two Ts, including types you didn't write:
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
list.sort(byLength);Because Comparator<T> is a functional interface (one abstract method, compare), every Comparator is just a lambda or a method reference. That's the modern shape of comparator code — you almost never write a full anonymous class anymore.
The builders on Comparator
The class has static factory methods that make building comparators short and readable:
Comparator<Person> byAge = Comparator.comparingInt(Person::age);
Comparator<Person> byName = Comparator.comparing(Person::name);
Comparator<Person> byNameCi = Comparator.comparing(Person::name, String.CASE_INSENSITIVE_ORDER);
Comparator<Person> oldestFirst = byAge.reversed();
Comparator<String> nullsFirst = Comparator.nullsFirst(Comparator.naturalOrder());Use the primitive-specialised builders — comparingInt, comparingLong, comparingDouble — when the key is a primitive. They avoid boxing on every comparison, which adds up on a long sort.
Chained comparators with thenComparing
The other reason to prefer the builders: you can chain multiple keys.
Comparator<Person> ordering =
Comparator.comparing(Person::lastName)
.thenComparing(Person::firstName)
.thenComparingInt(Person::age);This reads top-to-bottom as "primary key last name; tie-break by first name; then by age." thenComparing is invoked on the previous comparator and returns a new one that only consults the second key when the first reported a tie. There's no limit on the chain.
reversed(), nullsFirst, nullsLast
Three modifiers come up constantly:
reversed()flips the order of any comparator.byAge.reversed()is "oldest first."nullsFirst(cmp)wraps a comparator so thatnullvalues are treated as less than every non-null. Useful when sorting collections that may containnull.nullsLast(cmp)is the symmetric companion.
Don't use reversed() on a chained comparator and expect just the last key to flip — reversed() flips the entire ordering, every key in the chain.
Comparable vs Comparator in JDK APIs
Many methods come in two flavours — one that uses natural ordering, one that takes a Comparator:
| Operation | Natural-order overload | Comparator overload |
|---|---|---|
| Sort a list | Collections.sort(list) | Collections.sort(list, cmp) |
| Sort a list (modern) | list.sort(null) | list.sort(cmp) |
| Sort a stream | stream.sorted() | stream.sorted(cmp) |
| Tree-based set | new TreeSet<>() | new TreeSet<>(cmp) |
| Tree-based map | new TreeMap<>() | new TreeMap<>(cmp) |
| Min/max | Collections.min(list) | Collections.min(list, cmp) |
| Binary search | Collections.binarySearch(list, key) | Collections.binarySearch(list, key, cmp) |
PriorityQueue | natural ordering of element type | constructor takes a Comparator |
The natural-order forms require the element type to implement Comparable. If yours doesn't and you call them anyway, you'll see a ClassCastException at runtime — not a compile error — because the cast happens inside the sorting implementation.
A worked example: natural order, custom comparators, chained keys, nulls
The program below defines a record with a natural order (Comparable) plus three external orderings: by a single key, by chained keys with a reversed secondary, and one that tolerates null entries.
What to take from the run:
- The
Comparableimplementation sorted by name and broke name ties by age. No explicit comparator was needed — natural ordering is the default forCollections.sortand friends. Comparator.comparingDouble(Person::salary)is shorter and faster than writing(a, b) -> Double.compare(a.salary(), b.salary())because it avoids boxing.- The chained comparator sorted primarily by age and used
reversed()only on the salary leg — that's the right pattern when you want different directions on different keys. Compare with calling.reversed()on the whole chain, which would flip both keys. nullsFirstlet the comparator handle a list that containednullentries without anNullPointerException. Without that wrapper, the first comparison involving anullwould have crashed.- The "subtraction trick" produced the wrong answer for
Integer.MAX_VALUE - (-1): that calculation overflows into a negative number, sobadreportsMAX_VALUEas less than-1.Integer.compareproduces the right sign every time. Always prefer it.
What's next
You now have iteration (Iterator / ListIterator) and ordering (Comparable / Comparator) covered. The next chapter pulls them together in the java.util.Collections utility class — the static toolbox of sort, search, reverse, shuffle, min, max, and "wrap this collection as immutable" methods that operate on any List, Set, or Map. After that, two short chapters drill into sorting and searching specifically.
Practice
You write `list.sort((a, b) -> a.scoreDifference(b))` where `scoreDifference` returns `a.score - b.score` as an `int`. The list contains scores including `Integer.MAX_VALUE` and `Integer.MIN_VALUE`, and the result is clearly wrong. What's the fix?