Week 6 Day 5 — Comparable vs Comparator
Goal
Today I want natural order vs external order, and the TreeSet trap that uniqueness follows comparison.
Main questions:
- What is
Comparable? - What is a
Comparator? - What does “consistent with equals” mean?
- How do
TreeSet/TreeMapuse comparison? - How do I sort a list in modern Java?
1. Comparable — natural order
The type knows how it orders itself.
public record Money(String currency, long cents) implements Comparable<Money> {
@Override
public int compareTo(Money other) {
int byCurrency = currency.compareTo(other.currency);
if (byCurrency != 0) {
return byCurrency;
}
return Long.compare(cents, other.cents);
}
}
compareTo returns negative, zero, or positive. I use Integer.compare / Long.compare / Comparator.comparing — not subtraction (overflow).
String, Integer, LocalDate, UUID already have a natural order. I implement Comparable on value types that have one obvious order.
Memory sentence:
Comparableis the type’s natural order.compareTolives on the class.
2. Comparator — external order
A separate object defines order. I can have several: by name, by date, by salary.
Comparator<Order> byDate = Comparator.comparing(Order::createdAt);
Comparator<Order> byDateThenId = byDate.thenComparingLong(Order::id);
Comparator<Order> newestFirst = byDate.reversed();
orders.sort(byDateThenId);
Comparator.comparing plus thenComparing is the default style. nullsFirst / nullsLast when fields can be null.
I pass a Comparator to TreeSet, TreeMap, Collections.sort, Stream.sorted.
3. Consistent with equals
The contract interviewers want:
(a.compareTo(b) == 0) == a.equals(b)
If compareTo says equal and equals says not (or the reverse), sorted sets and maps break uniqueness.
public int compareTo(User other) {
return name.compareTo(other.name); // two users named Ada look the same to TreeSet
}
User a = new User(1L, "Ada");
User b = new User(2L, "Ada");
a.equals(b); // false (id-based)
new TreeSet<User>().add(a);
new TreeSet<User>().add(b); // b may be dropped — compareTo == 0
Fix: compare the same fields equals uses, or do not put those objects in a TreeSet. Sort a List with a Comparator instead.
HashSet uses equals/hashCode. TreeSet uses comparison only. That is the Day 2 trap in full.
Memory sentence:
HashSetusesequals.TreeSetusescompareTo. Keep them consistent, or do not mix the type into a tree.
4. Sorting a list
List<Order> orders = new ArrayList<>(incoming);
orders.sort(Comparator.comparing(Order::createdAt).reversed());
List<Order> copy = incoming.stream()
.sorted(Comparator.comparing(Order::id))
.toList();
List.sort mutates. Stream sorted produces a new list (Week 7). Collections.sort(list) delegates to list.sort.
Comparable types can use Collections.sort(list) with no comparator. I still prefer an explicit Comparator when the order is not obvious.
5. Spring connection
- Query methods:
findByStatusOrderByCreatedAtDescpushes order to SQL. That is the right default for large data. In-memorysortis for small collections already loaded. Pageable/Sortin Spring Data is aComparatoridea expressed as SQL.- Never
TreeSetof JPA entities with id-basedequalsand name-basedcompareTo. - Jackson does not need
Comparableon DTOs. I sort in the service if the API promises order and SQL did not.
6. Common traps
Trap 1: return this.age - other.age — overflow. Use Integer.compare.
Trap 2: TreeSet as “a sorted HashSet.” Uniqueness rules differ.
Trap 3: compareTo returning 0 for objects that are not equals.
Trap 4: Comparator that throws on null when the list has nulls.
Trap 5: Implementing Comparable on an entity just to sort once. A one-off Comparator in the service is enough.
Practice Questions and Answers
Question 1
Comparable vs Comparator?
Answer:
Comparable.compareTo is the type’s natural order, implemented on the class. A Comparator is an external strategy I pass into sort, TreeSet, or Stream.sorted. One type, many comparators. Natural order should be unique and consistent with equals if I put the type in a TreeSet.
Question 2
Why can TreeSet drop an element that HashSet would keep?
Answer:
TreeSet treats compareTo == 0 as “already present.” HashSet uses equals. If two users share a name but not an id, the tree keeps one and the hash set keeps both.
Question 3
What does “consistent with equals” mean?
Answer:
compareTo returns 0 exactly when equals is true. Sorted maps/sets then agree with the equals contract. If I cannot make them agree, I do not use TreeSet/TreeMap for that type.
Question 4
How do I sort by date descending, then id?
Answer:
Comparator.comparing(Order::createdAt).reversed().thenComparingLong(Order::id) — or comparing(Order::createdAt, reverseOrder()).thenComparingLong(Order::id) depending on whether I reverse only the date. I pass that to list.sort or stream().sorted.
Question 5
Should a JPA @Entity implement Comparable?
Answer:
Usually no. Order belongs in the query or in a Comparator at the use site. compareTo on an entity is easy to make inconsistent with id-based equals and then break a TreeSet of children.
Memory sentences
Comparableis natural order on the type.Comparatoris an external strategy.
HashSetusesequals.TreeSetusescompareTo.
Do not subtract ints to compare; use
Integer.compareorComparator.comparing.
Next: Week 6 Review