Zum Hauptinhalt springen

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:

  1. What is Comparable?
  2. What is a Comparator?
  3. What does “consistent with equals” mean?
  4. How do TreeSet / TreeMap use comparison?
  5. 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:

Comparable is the type’s natural order. compareTo lives 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:

HashSet uses equals. TreeSet uses compareTo. 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: findByStatusOrderByCreatedAtDesc pushes order to SQL. That is the right default for large data. In-memory sort is for small collections already loaded.
  • Pageable / Sort in Spring Data is a Comparator idea expressed as SQL.
  • Never TreeSet of JPA entities with id-based equals and name-based compareTo.
  • Jackson does not need Comparable on 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

Comparable is natural order on the type. Comparator is an external strategy.

HashSet uses equals. TreeSet uses compareTo.

Do not subtract ints to compare; use Integer.compare or Comparator.comparing.

Next: Week 6 Review