Comparable and Comparator
Natural order vs. external order — how Java models "what does it mean for two values to be in order?"
A collection cannot sort what it cannot compare. So before any
sort, TreeSet, TreeMap, or PriorityQueue works, something
has to tell Java how two elements relate.
There are two ways to provide that ordering:
Comparable<T>— the type itself defines a natural order.Comparator<T>— an external object defines an order on the type, often one of many possible orderings.
Comparable: the natural order
Comparable<T> has a single method, int compareTo(T other), which
returns a negative number, zero, or a positive number depending on
whether this is less than, equal to, or greater than other.
Many built-in types implement it: Integer, Double, String
(lexicographic), LocalDate, etc.
Define it for your own types when there is one obvious order that makes sense most of the time:
Notice the use of Integer.compare(a, b) — never write a - b,
which silently overflows for very large or very negative values.
The equals-consistency advisory
Whenever practical, compareTo should be consistent with
equals: a.compareTo(b) == 0 iff a.equals(b).
When it is not consistent, sorted collections quietly do
surprising things. For example, a TreeSet<BigDecimal> treats
new BigDecimal("1.0") and new BigDecimal("1.00") as the same
element (because compareTo returns 0 even though equals
returns false). The Javadoc explicitly warns about this.
If your "natural order" is by, say, a single field, but two objects
with the same field value should not be treated as equal, prefer a
Comparator and leave Comparable out of it.
Comparator: an external order
A Comparator<T> is a small object that knows how to compare two
Ts. Because it is external, you can have many of them for the same
type:
Comparator.comparing and comparingInt are key extractor
factories: pass a function from T to a sortable key, get a
Comparator<T> back.
Composing comparators
The real elegance of Comparator is composition. You can chain
thenComparing to break ties:
This is far more readable than a 10-line compareTo method that
mixes three fields, and you can construct different orderings for
different views without touching the entity class.
null-safe comparators
By default, comparators throw NullPointerException on null keys.
Comparator.nullsFirst and nullsLast wrap any comparator into a
null-tolerant one:
Sorting in place vs. producing a sorted copy
| Need | Use |
|---|---|
Sort an existing List in place | list.sort(cmp) or Collections.sort(list, cmp) |
Get a sorted copy of any Collection | list.stream().sorted(cmp).toList() |
| Sort an array | Arrays.sort(arr, cmp) |
| Keep a continuously sorted view | TreeSet / TreeMap, or PriorityQueue for partial order |
Multi-file example: sorting library users
Naming each ordering as a method gives it intent — activeFirstThenOldestThenName()
reads like the requirement. The class that owns the data does not
have to know any of this.
Practice
Implement Leaderboard.top3(List<Player>) returning the three players with the highest score, breaking ties by name ascending.
Expected output:
Bjarne 95
Linus 95
Grace 90
Test your understanding
When should a class implement Comparable<T>?
Always, just in case
When there is one obvious natural order that is consistent with equals and serves most callers
Only when the class will be stored in a TreeSet
Whenever the class has numeric fields
Why is return a - b; a bad implementation of compareTo for int values?
It is slower than Integer.compare
It can overflow when a and b are far apart in sign or magnitude, returning a wrong sign
It does not compile
It returns a value outside {-1, 0, 1}
What does Comparator.comparing(X::getA).thenComparing(X::getB) do?
Sorts by b and then by a
Sorts by a ascending; uses b to break ties
Sums the two comparison results
Sorts only by a