Comparable
Comparable is a functional interface in java.lang that defines a natural ordering for a class. A class that implements Comparable<T> provides a compareTo() method that compares the current instance with another instance of type T, returning a negative integer if this instance is less than, zero if equal to, and a positive integer if greater than the argument. This natural ordering is used by sort algorithms (Arrays.sort, Collections.sort), sorted collections (TreeSet, TreeMap), and binary search. Understanding Comparable means understanding the compareTo contract, the consistency-with-equals requirement, correct implementation patterns, and the performance implications of different comparison strategies. This entry covers the complete compareTo contract, common implementation patterns, the subtraction antipattern, and when to use Comparable versus Comparator.
The compareTo Contract
// ── Comparable<T> interface ───────────────────────────────────────────
// public interface Comparable<T> {
// int compareTo(T other);
// }
// Returns: negative if this < other
// zero if this == other
// positive if this > other
// ── Simple single-field implementation ───────────────────────────────
public class Temperature implements Comparable<Temperature> {
private final double celsius;
public Temperature(double celsius) {
this.celsius = celsius;
}
@Override
public int compareTo(Temperature other) {
// Use Double.compare — never subtraction for floating point!
return Double.compare(this.celsius, other.celsius);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Temperature t)) return false;
return Double.compare(celsius, t.celsius) == 0;
}
@Override public int hashCode() { return Double.hashCode(celsius); }
@Override public String toString() { return celsius + "°C"; }
}
// ── Contract verification ──────────────────────────────────────────────
Temperature t1 = new Temperature(20);
Temperature t2 = new Temperature(30);
Temperature t3 = new Temperature(40);
// Antisymmetry: t1 < t2 means t2 > t1
System.out.println(t1.compareTo(t2)); // negative — t1 < t2
System.out.println(t2.compareTo(t1)); // positive — t2 > t1 (sign flipped)
// Transitivity: t1 < t2 && t2 < t3 → t1 < t3
System.out.println(t1.compareTo(t3)); // negative — t1 < t3 ✓
// Consistency with equals:
Temperature same1 = new Temperature(25.0);
Temperature same2 = new Temperature(25.0);
System.out.println(same1.compareTo(same2) == 0); // true
System.out.println(same1.equals(same2)); // true — consistent ✓
// ── Natural ordering used by sort and collections ─────────────────────
List<Temperature> temps = new ArrayList<>(List.of(
new Temperature(30), new Temperature(10),
new Temperature(25), new Temperature(-5)));
Collections.sort(temps); // uses compareTo
System.out.println(temps); // [-5.0°C, 10.0°C, 25.0°C, 30.0°C]
TreeSet<Temperature> treeSet = new TreeSet<>(temps);
System.out.println(treeSet.first()); // -5.0°C — minimumMulti-Field Comparison and the Subtraction Antipattern
// ── Multi-field comparison ────────────────────────────────────────────
public class Employee implements Comparable<Employee> {
private final String department;
private final String lastName;
private final String firstName;
private final int salary;
// Constructor + getters omitted for brevity
@Override
public int compareTo(Employee other) {
// Cascading comparison — compare in priority order
int dept = this.department.compareTo(other.department);
if (dept != 0) return dept;
int last = this.lastName.compareTo(other.lastName);
if (last != 0) return last;
int first = this.firstName.compareTo(other.firstName);
if (first != 0) return first;
return Integer.compare(this.salary, other.salary);
}
}
// ── Cleaner: delegate to Comparator.comparing chain ───────────────────
public class EmployeeClean implements Comparable<EmployeeClean> {
private final String department;
private final String lastName;
private final String firstName;
private final int salary;
private static final Comparator<EmployeeClean> NATURAL_ORDER =
Comparator.comparing(e -> e.department)
.thenComparing(e -> e.lastName)
.thenComparing(e -> e.firstName)
.thenComparingInt(e -> e.salary);
@Override
public int compareTo(EmployeeClean other) {
return NATURAL_ORDER.compare(this, other);
}
}
// ── DANGEROUS: subtraction antipattern ───────────────────────────────
// WRONG — integer overflow corrupts ordering:
public class BadComparable implements Comparable<BadComparable> {
int value;
@Override
public int compareTo(BadComparable other) {
return this.value - other.value; // OVERFLOW BUG!
}
}
BadComparable big = new BadComparable(); big.value = Integer.MAX_VALUE;
BadComparable neg = new BadComparable(); neg.value = -1;
System.out.println(big.compareTo(neg)); // should be positive (MAX > -1)
// But: MAX_VALUE - (-1) = MAX_VALUE + 1 = Integer.MIN_VALUE → NEGATIVE!
// The comparator falsely says MAX_VALUE < -1 — catastrophic sort corruption!
// CORRECT:
@Override public int compareTo(BadComparable other) {
return Integer.compare(this.value, other.value); // safe, no overflow
}
// ── Null handling — compareTo must throw NPE for null ─────────────────
Temperature t = new Temperature(20);
// t.compareTo(null); // NullPointerException — correct and expected
// To sort a list containing nulls, use a Comparator wrapper:
List<Temperature> withNulls = new ArrayList<>(
List.of(new Temperature(20), null, new Temperature(10)));
withNulls.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(withNulls); // [null, 10.0°C, 20.0°C]Comparable vs Comparator — When to Use Each
// ── Comparable: natural ordering for well-understood types ──────────
public class Priority implements Comparable<Priority> {
private static final Map<String, Integer> ORDER =
Map.of("LOW", 1, "MEDIUM", 2, "HIGH", 3, "CRITICAL", 4);
private final String level;
public Priority(String level) {
if (!ORDER.containsKey(level))
throw new IllegalArgumentException("Unknown level: " + level);
this.level = level;
}
@Override
public int compareTo(Priority other) {
return Integer.compare(ORDER.get(this.level), ORDER.get(other.level));
}
// Now Priority works naturally with all sorted collections:
TreeSet<Priority> sorted = new TreeSet<>();
// TreeSet uses Priority.compareTo automatically
}
// ── Comparator: multiple orderings for the same class ─────────────────
record Product(String name, BigDecimal price, int stock, double rating) {}
// Multiple ways to order Products — none is "the" natural order
Comparator<Product> byPrice = Comparator.comparing(Product::price);
Comparator<Product> byRating = Comparator.comparingDouble(Product::rating).reversed();
Comparator<Product> byName = Comparator.comparing(Product::name);
Comparator<Product> byStock = Comparator.comparingInt(Product::stock);
// Best value: price per unit * rating
Comparator<Product> byValue = Comparator.comparingDouble(
p -> p.price().doubleValue() / p.rating());
// Sort differently in different contexts:
List<Product> products = new ArrayList<>(/* ... */);
// Show cheapest first:
products.sort(byPrice);
// Show highest rated first, break ties by name:
products.sort(byRating.thenComparing(byName));
// ── Comparator.naturalOrder() — use Comparable's compareTo via Comparator ─
List<String> strs = new ArrayList<>(List.of("banana", "apple", "cherry"));
strs.sort(Comparator.naturalOrder()); // delegates to String.compareTo
strs.sort(Comparator.reverseOrder()); // reverses String.compareTo
// ── When class does not implement Comparable ──────────────────────────
// java.awt.Point does not implement Comparable
// Must use external Comparator:
Comparator<java.awt.Point> byX = Comparator.comparingInt(p -> p.x);
Comparator<java.awt.Point> byDistance =
Comparator.comparingDouble(p -> Math.sqrt(p.x * p.x + p.y * p.y));
List<java.awt.Point> points = new ArrayList<>(List.of(
new java.awt.Point(3, 4),
new java.awt.Point(1, 1),
new java.awt.Point(5, 0)));
points.sort(byDistance);
System.out.println(points); // sorted by distance from origin