Comparable versus Comparator
Two Interfaces, One Purpose
Java provides two separate interfaces for defining ordering:
Comparable<T>
Implemented by the class itself. Defines the class’s single “natural ordering”.
public interface Comparable<T> {
int compareTo(T o);
}
The method returns:
- A negative integer if
thisis less thano - Zero if
thisis equal too - A positive integer if
thisis greater thano
class Person implements Comparable<Person> {
private String name;
private int age;
Person(String name, int age) { this.name = name; this.age = age; }
@Override
public int compareTo(Person other) {
return name.compareTo(other.name); // natural order: by name
}
}
Usage: Collections.sort(list) — no comparator needed; the list elements’ natural ordering is used.
Comparator<T>
A separate object that implements comparison logic. Allows any number of orderings external to the class.
public interface Comparator<T> {
int compare(T a, T b);
}
class PersonAgeComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return Integer.compare(a.getAge(), b.getAge());
}
}
Usage: list.sort(new PersonAgeComparator()) or Collections.sort(list, new PersonAgeComparator()).
When to Use Which
Use Comparable | Use Comparator |
|---|---|
| The class has one obvious natural ordering | You need multiple orderings (by name, by age, by postcode) |
| You control the class’s source code | You cannot modify the class (library class, third-party) |
| The ordering is intrinsic to the class’s identity | The ordering is context-dependent |
Examples of natural ordering:
Integerby numeric valueStringlexicographically (dictionary order)LocalDatechronologicallyBigDecimalby numeric value
Comparator Static Factory Methods
Since Java 8, Comparator provides static methods for building comparators declaratively, avoiding verbose anonymous classes:
Comparator<Person> byName = Comparator.comparing(Person::getName);
Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
list.sort(byName); // sort by name
list.sort(byAge); // sort by age
list.sort(byName.thenComparing(byAge)); // sort by name, then by age for ties
list.sort(byAge.reversed()); // sort by age descending
Chaining comparators:
Comparator<Person> byNameThenAge = Comparator
.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparingInt(Person::getAge);
Null handling:
Comparator<String> nullSafe = Comparator.nullsFirst(String::compareTo);
// null values come before all non-null values
Comparator<String> nullSafeLast = Comparator.nullsLast(String::compareTo);
// null values come after all non-null values
The Consistency Rule
The compareTo() / compare() results should be consistent with equals():
This is a recommendation, not enforced by the compiler. Violating it causes subtle bugs:
class BadDecimal {
BigDecimal value;
@Override
public boolean equals(Object o) {
return o instanceof BadDecimal
&& value.compareTo(((BadDecimal) o).value) == 0;
}
@Override
public int compareTo(BadDecimal o) {
return value.compareTo(o.value);
}
}
BigDecimal is the canonical example: new BigDecimal("1.0") and new BigDecimal("1.00") are .equals()-unequal (different precision) but .compareTo()-equal (same numeric value). If you use a TreeSet (which relies on compareTo), only one of them can be stored — which one survives depends on insertion order. A HashSet (which relies on equals) stores both.
The SortedSet and SortedMap contracts explicitly state that ordering must be consistent with equals unless the implementation’s documentation says otherwise.
Comparison Contract
Both compareTo() and compare() must satisfy:
- Sign consistency:
sign(x.compareTo(y)) == -sign(y.compareTo(x))— antisymmetry. - Transitivity: if
x.compareTo(y) > 0andy.compareTo(z) > 0, thenx.compareTo(z) > 0. - Equivalence: if
x.compareTo(y) == 0, thensign(x.compareTo(z)) == sign(y.compareTo(z))for allz. - Recommended but not required:
x.compareTo(y) == 0if and only ifx.equals(y).
TreeSet and TreeMap Behaviour
TreeSet and TreeMap use compareTo() (or the supplied Comparator) for ALL ordering — they never call equals():
Set<BigDecimal> set = new TreeSet<>();
set.add(new BigDecimal("1.0"));
set.add(new BigDecimal("1.00"));
System.out.println(set.size()); // 1 — compareTo considers them equal
This is correct behaviour per the SortedSet contract. If your comparator is inconsistent with equals, the set may not obey the general Set contract (which is defined in terms of equals), but it obeys the SortedSet contract.
Implementing compareTo with Primitives
Use the static wrapper methods rather than subtraction (which can overflow):
@Override
public int compareTo(Person other) {
int nameCmp = name.compareTo(other.name);
if (nameCmp != 0) return nameCmp;
return Integer.compare(age, other.age); // safe — no overflow
}
Subtraction (return age - other.age) can overflow for large magnitude differences, producing incorrect sign results. Integer.compare(a, b) handles this correctly under all values.