W3docs

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. Its int compareTo(T other) method is the type's natural ordering.
  • Comparator<T> — an external object that orders instances. Its int 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:

  • negativea comes before b
  • zero — equal for ordering purposes
  • positivea comes after b

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:

  1. Anti-symmetrica.compareTo(b) and b.compareTo(a) have opposite signs.
  2. Transitive — if a < b and b < c, then a < c.
  3. Consistent with equals (strongly recommended)a.compareTo(b) == 0 iff a.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 that null values are treated as less than every non-null. Useful when sorting collections that may contain null.
  • 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:

OperationNatural-order overloadComparator overload
Sort a listCollections.sort(list)Collections.sort(list, cmp)
Sort a list (modern)list.sort(null)list.sort(cmp)
Sort a streamstream.sorted()stream.sorted(cmp)
Tree-based setnew TreeSet<>()new TreeSet<>(cmp)
Tree-based mapnew TreeMap<>()new TreeMap<>(cmp)
Min/maxCollections.min(list)Collections.min(list, cmp)
Binary searchCollections.binarySearch(list, key)Collections.binarySearch(list, key, cmp)
PriorityQueuenatural ordering of element typeconstructor 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.

java— editable, runs on the server

What to take from the run:

  • The Comparable implementation sorted by name and broke name ties by age. No explicit comparator was needed — natural ordering is the default for Collections.sort and 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.
  • nullsFirst let the comparator handle a list that contained null entries without an NullPointerException. Without that wrapper, the first comparison involving a null would have crashed.
  • The "subtraction trick" produced the wrong answer for Integer.MAX_VALUE - (-1): that calculation overflows into a negative number, so bad reports MAX_VALUE as less than -1. Integer.compare produces 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

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?