Two ways to say "smaller"
Almost every Java program eventually has to put things in order, and the JDK gives you
two complementary hooks for it. Comparable lets a type declare its own ordering;
Comparator lets you describe an ordering from the outside, as many different ways
as you like. Knowing which to reach for — and how the sorting machinery, the compareTo
contract, and sorted collections all lean on them — is the difference between code that
sorts correctly and code that quietly returns garbage. This guide ties the whole picture
together.
Natural ordering with Comparable
A type that implements Comparable<T> (from java.lang) defines its natural
ordering by overriding a single method, compareTo. The contract is the same one
every sort relies on: return a negative int when this sorts before the argument,
zero when they tie, and positive when this sorts after. Only the sign
matters, never the magnitude.
class Person implements Comparable<Person> {
String name; int age;
@Override public int compareTo(Person other) {
return Integer.compare(this.age, other.age); // natural order = by age
}
}
// Now Person works with no comparator anywhere:
Collections.sort(people); // uses compareTo
TreeSet<Person> ts = new TreeSet<>(); // uses compareTo
Implement Comparable for the one obvious ordering a type genuinely is — numbers by
value, strings alphabetically, money by amount. Once it's there, Collections.sort,
Arrays.sort, TreeSet, TreeMap, and Stream.sorted() all just work without an extra
argument.
External ordering with Comparator
A Comparator<T> (from java.util) is a separate object that orders two elements
via compare(a, b), using the identical sign convention. Because it lives outside the
class, you can define unlimited orderings, and you can sort types you can't edit (String,
library classes, your colleague's untouchable model).
// order Person by name without touching the Person class
Comparator<Person> byName = (a, b) -> a.name.compareTo(b.name);
people.sort(byName);
The two interfaces map onto each other cleanly:
Comparable<T> | Comparator<T> | |
|---|---|---|
| Package | java.lang | java.util |
| Method | int compareTo(T o) | int compare(T a, T b) |
| Lives | inside the class | a separate object |
| Orderings | one (the natural one) | as many as you want |
| Edits the class? | yes | no |
Rule of thumb: Comparable for the single intrinsic order, Comparator for
everything situational — especially for types you don't own.
The contract — and the subtraction trap
The contract isn't a suggestion. A valid ordering must be antisymmetric
(sgn(a.compareTo(b)) == -sgn(b.compareTo(a))), transitive (if a > b and b > c
then a > c), and consistent (equal inputs always compare equally). Break
transitivity and modern Arrays.sort will detect it and throw
IllegalArgumentException: Comparison method violates its general contract!.
The most common way to break the contract by accident is the "clever" one-liner
return a - b;. Integer subtraction can overflow: when a is large positive and b
is large negative, a - b wraps past Integer.MAX_VALUE and comes back negative, so
a value that should sort after sorts before.
int a = Integer.MAX_VALUE, b = -1;
System.out.println(a - b); // -2147483648 (wrong sign!)
System.out.println(Integer.compare(a, b)); // positive (correct)
The fix is Integer.compare (or Long.compare, Double.compare): overflow-safe,
self-documenting, no cleverness required. Never subtract to compare.
Building comparators by composition
You rarely hand-write compare anymore. The factory methods on Comparator build one
from a key extractor and compose into multi-level orderings. comparing pulls a
Comparable key out of each element; thenComparing adds a tie-breaker that's only
consulted when the previous level returns zero; reversed flips direction; and
comparingInt/comparingLong/comparingDouble take a primitive extractor to skip the
autoboxing that plain comparing would do on every comparison.
people.sort(
Comparator.comparing(Person::getLastName) // primary key
.thenComparing(Person::getFirstName) // tie-breaker
.thenComparingInt(Person::getAge) // primitive, no boxing
);
people.sort(Comparator.comparingInt(Person::getAge).reversed()); // oldest first
Two placement gotchas worth internalizing: reversed() flips the entire chain built so
far, not just the last thenComparing, so parenthesize deliberately or reverse a single
key with thenComparing(Person::getAge, reverseOrder()). And because comparators are
stateless and thread-safe, build the ones you reuse as static final constants instead of
rebuilding the chain in a hot loop.
Sorting null fields safely
A plain comparator throws NullPointerException the moment it meets a null element
or a null key. Wrap it with Comparator.nullsFirst(...) or nullsLast(...) to
decide where nulls land, and combine it with naturalOrder()/reverseOrder() so you never
write (a, b) -> a.compareTo(b) by hand.
List<String> s = Arrays.asList("b", null, "a");
s.sort(Comparator.nullsFirst(Comparator.naturalOrder())); // [null, a, b]
// guard a possibly-null key inside a chain
people.sort(Comparator.comparing(Person::getName,
Comparator.nullsLast(Comparator.naturalOrder())));
Rule of thumb: if a field can be null, wrap its comparator rather than hoping the
data is clean.
One ordering, every sort
The payoff of having Comparable/Comparator as a shared currency is that every sorting
entry point in the JDK accepts the same comparators. They split into two families: those
that mutate in place and those that produce a new ordering.
Collections.sort(list); // in place, natural order
list.sort(Comparator.comparing(Person::getName)); // in place, modern idiom
Arrays.sort(boxed, Comparator.reverseOrder()); // object array, in place
List<Person> byAge = people.stream() // NEW list, source untouched
.sorted(Comparator.comparingInt(Person::getAge))
.toList();
new TreeSet<>(String.CASE_INSENSITIVE_ORDER); // kept sorted on every insert
new PriorityQueue<>(Comparator.reverseOrder()); // max-heap; head only
A few distinctions matter under questioning. Collections.sort simply delegates to
list.sort, so prefer the method on the object you already hold. Arrays.sort accepts a
comparator only for object arrays — primitive arrays sort natural-ascending only,
so to sort int[] descending you box to Integer[] first. Stream.sorted does not
mutate the source and is stateful (it buffers everything before emitting). And a
PriorityQueue only guarantees the head is the min/max — iterating it does not yield
sorted order, so trust peek/poll and nothing else. Object sorts (TimSort) are
stable; primitive Arrays.sort (dual-pivot quicksort) is not.
Consistency with equals and the TreeSet dedup trap
Here's the subtlety that trips up even experienced developers. An ordering is consistent
with equals when a.compareTo(b) == 0 exactly when a.equals(b). It's only
recommended, not enforced — but sorted collections judge equality by comparison, not by
equals. A TreeSet/TreeMap decides two elements are duplicates purely because their
compare returns 0; equals and hashCode are never consulted.
// comparator looks only at age:
TreeSet<Person> set = new TreeSet<>(Comparator.comparingInt(Person::getAge));
set.add(new Person("Ada", 30));
set.add(new Person("Bob", 30)); // NOT added — same age -> compare == 0
System.out.println(set.size()); // 1, even though Ada != Bob
// the classic JDK inconsistency:
new BigDecimal("1.0").equals(new BigDecimal("1.00")); // false
new BigDecimal("1.0").compareTo(new BigDecimal("1.00")); // 0 -> "equal" to a TreeSet
So a coarse comparator silently swallows distinct objects, and BigDecimal lands as one
element in a TreeSet but two in a HashSet. Rule of thumb: make any
TreeSet/TreeMap comparator total enough that only truly-equal elements compare to 0,
and keep compareTo consistent with equals unless you have a documented reason not to.
Recap
Comparable defines a type's one natural ordering from the inside via compareTo;
Comparator defines any number of external orderings via compare — both governed by
the same sign-based, antisymmetric, transitive, consistent contract. Never subtract to
compare (use Integer.compare to dodge overflow); build comparators with comparing /
thenComparing / reversed / nullsFirst / comparingInt instead of hand-rolling
compare; and remember that one comparator feeds every sort — Collections.sort,
List.sort, Arrays.sort, Stream.sorted, TreeSet, and PriorityQueue alike. Above
all, keep consistency with equals in mind: sorted collections de-duplicate by
comparison, so a too-loose comparator will quietly drop elements you meant to keep.