Comparable & Comparator

Comparable defines a class's single natural ordering from the inside; Comparator defines an ordering from the outside, and you can have as many Comparators as you need.

Java gives two interfaces for ordering objects: Comparable<T>, implemented by the class itself to define one natural ordering, and Comparator<T>, a separate object that defines an ordering independent of the class. Understanding when to use each — and how to chain and reverse them — is essential for sorting and ordered collections like TreeMap/TreeSet.

Comparable: Natural Ordering

A class implements Comparable<T> by providing compareTo(T other), returning a negative number if this should sort before other, zero if they're considered equal for ordering purposes, and a positive number if this should sort after other. This defines the class's single, intrinsic 'natural' ordering.

public class Employee implements Comparable<Employee> {
    private final String name;
    private final double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    @Override
    public int compareTo(Employee other) {
        return Double.compare(this.salary, other.salary); // natural order: by salary ascending
    }

    public String getName() { return name; }
    public double getSalary() { return salary; }

    @Override
    public String toString() { return name + ":" + salary; }
}

Using Comparable

Classes implementing Comparable sort correctly with Collections.sort(list), Arrays.sort(array), and work naturally as elements of TreeSet/keys of TreeMap without any extra configuration.

List<Employee> staff = new ArrayList<>(List.of(
    new Employee("Dana", 75000),
    new Employee("Sam", 62000)
));
Collections.sort(staff); // uses compareTo - natural order by salary
System.out.println(staff); // [Sam:62000.0, Dana:75000.0]

Comparator: External Ordering

Comparator<T> defines ordering from outside the class via compare(T a, T b), with the same sign convention as compareTo. This lets you sort the same class multiple different ways without modifying the class, and lets you order classes you don't control (e.g. from a library) or that have no natural ordering at all.

Comparator<Employee> byName = new Comparator<Employee>() {
    @Override
    public int compare(Employee a, Employee b) {
        return a.getName().compareTo(b.getName());
    }
};

Collections.sort(staff, byName);

Modern Comparator: Lambdas and Factory Methods

Since Java 8, Comparator is a functional interface with convenient static/default methods, so full anonymous classes are rarely needed anymore.

Comparator<Employee> byName = Comparator.comparing(Employee::getName);
Collections.sort(staff, byName);

// Or with a lambda directly:
staff.sort((a, b) -> a.getName().compareTo(b.getName()));

// Or via comparingDouble for primitive fields (avoids boxing overhead):
Comparator<Employee> bySalary = Comparator.comparingDouble(Employee::getSalary);

Chaining Comparators

thenComparing(...) chains a secondary ordering used to break ties in the primary comparator, and reversed() inverts a comparator's order — both compose fluently.

Comparator<Employee> bySalaryThenName = Comparator
    .comparingDouble(Employee::getSalary)
    .thenComparing(Employee::getName);

Comparator<Employee> highestPaidFirst = Comparator
    .comparingDouble(Employee::getSalary)
    .reversed();

staff.sort(highestPaidFirst);

Comparator.nullsFirst / nullsLast

Comparator.nullsFirst(cmp) / nullsLast(cmp) handle null elements gracefully instead of throwing NullPointerException when a plain comparator hits a null.

List<String> withNulls = new ArrayList<>(Arrays.asList("b", null, "a"));
withNulls.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(withNulls); // [null, a, b]

Consistency with equals

It's strongly recommended (though not strictly enforced by the compiler) that compareTo/compare returning zero for two objects be consistent with equals() returning true for those same objects. Inconsistency doesn't break compilation, but it produces surprising behavior in sorted collections: TreeSet/TreeMap use compareTo/compare — not equals — to determine uniqueness, so two 'unequal-by-equals' objects that compare as zero will be treated as duplicates and only one will be kept.

// Employees compared only by salary - NOT consistent with equals (which likely compares name+salary)
TreeSet<Employee> set = new TreeSet<>(); // uses compareTo
set.add(new Employee("Dana", 75000));
set.add(new Employee("Sam", 75000)); // same salary!
System.out.println(set.size()); // 1 - Sam was rejected as a "duplicate" by compareTo, even though equals() would say they differ

Rules and Behavior

A well-behaved compareTo/compare must be consistent and (for a total ordering) transitive: if compare(a,b) < 0 and compare(b,c) < 0, then compare(a,c) < 0 must also hold. It should also be antisymmetric: compare(a,b) and compare(b,a) should have opposite signs (or both be zero).

compareTo can throw ClassCastException if the types being compared aren't mutually comparable — this happens automatically when compareTo is generic-typed correctly, but is a risk with raw types.

Practical Usage

Use Comparable when a class has one obvious, canonical ordering that should travel with the class everywhere (e.g. String's lexicographic order, Integer's numeric order). Use Comparator for situational, alternate, or multiple orderings, or when you can't modify the class.

// Sorting a List of a class you don't own, e.g. java.io.File, by size:
List<java.io.File> files = new ArrayList<>();
// files.sort(Comparator.comparingLong(java.io.File::length));

Edge Cases

Never compute a comparator result by naive subtraction of two ints (a - b) — this can silently overflow for large magnitude values with opposite signs, giving a wrong-signed result. Use Integer.compare(a, b) (or the equivalent wrapper compare methods) instead.

int a = Integer.MIN_VALUE;
int b = 1;
System.out.println(a - b); // overflows! wrong sign, NOT a reliable comparator
System.out.println(Integer.compare(a, b)); // correct: negative, a < b

Complete Example

import java.util.*;

record Product(String name, double price, int stock) {}

public class SortDemo {
    public static void main(String[] args) {
        List<Product> products = new ArrayList<>(List.of(
            new Product("Widget", 9.99, 100),
            new Product("Gadget", 9.99, 20),
            new Product("Gizmo", 4.99, 50)
        ));

        Comparator<Product> byPriceThenStockDesc = Comparator
            .comparingDouble(Product::price)
            .thenComparing(Comparator.comparingInt(Product::stock).reversed());

        products.sort(byPriceThenStockDesc);
        products.forEach(System.out::println);
        // Gizmo (4.99), Widget (9.99, stock 100), Gadget (9.99, stock 20)
    }
}

Mental Model

Comparable is a property a class carries about itself ('I know how to order myself'). Comparator is a strategy object handed to a sorting algorithm from outside ('here's how YOU should order these'). A sort call always needs one ordering source — either the elements' own compareTo, or an explicitly supplied Comparator that overrides it.

Quick Reference

class X implements Comparable<X> {
    public int compareTo(X other) { ... }
}

Comparator<X> c1 = Comparator.comparing(X::getField);
Comparator<X> c2 = c1.thenComparing(X::getOtherField);
Comparator<X> c3 = c1.reversed();
Comparator<X> c4 = Comparator.nullsFirst(c1);

Collections.sort(list);        // uses natural order (Comparable)
Collections.sort(list, cmp);   // uses supplied Comparator
list.sort(cmp);                // same, via List.sort

Key Takeaways

Comparable is implemented by the class to define one natural ordering (compareTo); Comparator is a separate strategy object for any ordering (compare).

Comparator.comparing/thenComparing/reversed compose fluently to build multi-key orderings without anonymous classes.

TreeSet/TreeMap treat compareTo/compare returning 0 as equality for uniqueness purposes — inconsistency with equals() causes silently dropped 'duplicates'.

Never build a comparator with raw int subtraction — it can overflow; use Integer.compare or the typed wrapper compare methods.

A good comparator must be consistent, antisymmetric, and transitive to behave predictably in sorting and ordered collections.

@Override public int compareTo(Employee other) { return (int) (this.salary - other.salary); // can overflow / lose precision }

@Override public int compareTo(Employee other) { return Double.compare(this.salary, other.salary); }

Subtracting doubles and casting to int can overflow or truncate meaningful differences; the typed compare() methods handle this correctly.

TreeSet<Employee> set = new TreeSet<>(Comparator.comparingDouble(Employee::getSalary)); // two employees with the same salary but different names both get added, then one silently vanishes

TreeSet<Employee> set = new TreeSet<>( Comparator.comparingDouble(Employee::getSalary).thenComparing(Employee::getName) );

TreeSet uses the comparator (not equals()) to detect duplicates; a comparator that ties on salary alone will drop distinct employees sharing a salary. Add a tiebreaker field.

int a = Integer.MIN_VALUE, b = 5; int diff = a - b; // overflows, gives wrong sign int result = Integer.compare(a, b); // correct sign regardless of magnitude

Naive subtraction-based comparators can overflow for extreme values; Integer.compare (and its Long/Double equivalents) are overflow-safe.

list.sort(null); // works but relies on elements implementing Comparable, unclear at call site list.sort(Comparator.naturalOrder()); // explicit and equally correct

Passing null to List.sort silently falls back to natural ordering (throwing ClassCastException if elements aren't Comparable) — Comparator.naturalOrder() documents the same intent explicitly.

What is the key conceptual difference between Comparable and Comparator?

Comparable is implemented by the class itself to define one intrinsic natural ordering (via compareTo); Comparator is a separate object supplied externally that can define any number of alternate orderings (via compare), without modifying the class.

Why might TreeSet silently lose an element you expected it to contain?

TreeSet determines uniqueness using its comparator's (or the elements' compareTo's) result, not equals(). If the comparator returns 0 for two objects that are not equal by equals(), the second insertion is treated as a duplicate and dropped.

Why is int a - b a risky way to implement a comparator?

It can silently overflow for large-magnitude values with opposite signs, producing a wrong-signed (or even wrong-zero) comparison result. Integer.compare(a, b) avoids this by not relying on subtraction.

How does Comparator.thenComparing help when sorting by multiple fields?

It lets you chain a secondary (then tertiary, etc.) comparator that only gets consulted to break ties when the primary comparator considers two elements equal, composing a multi-key sort without writing a custom compare() by hand.

What does Comparator.reversed() do?

It returns a new Comparator that produces the opposite ordering of the one it's called on, without modifying the original comparator.

Can a class implement Comparable and still be sorted differently in a particular call? How?

Yes — pass an explicit Comparator to the sort call (e.g. list.sort(comparator) or Collections.sort(list, comparator)); the supplied Comparator overrides the class's natural ordering for that call only.

Why does the Java convention recommend compareTo/compare be consistent with equals?

Because ordered collections like TreeSet/TreeMap and methods like binarySearch rely on the comparator's notion of 'equal' (return 0) for correctness of membership/uniqueness checks; a mismatch with equals() causes confusing, hard-to-debug behavior even though it isn't a compile error.

Write a class Point implementing Comparable<Point> that orders points by distance from the origin, then write code that sorts a list of Points.

import java.util.*; class Point implements Comparable<Point> { final int x, y; Point(int x, int y) { this.x = x; this.y = y; } double distance() { return Math.sqrt(x * x + y * y); } @Override public int compareTo(Point other) { return Double.compare(this.distance(), other.distance()); } @Override public String toString() { return "(" + x + "," + y + ")"; } } public class PointDemo { public static void main(String[] args) { List<Point> points = new ArrayList<>(List.of( new Point(3, 4), new Point(1, 1), new Point(0, 5))); Collections.sort(points); System.out.println(points); // [(1,1), (0,5), (3,4)] } } compareTo uses Double.compare on the computed distance, so Collections.sort orders by distance ascending: (1,1) dist~1.41, (0,5) dist=5, (3,4) dist=5 -> ties resolved by original relative order (stable sort), giving [(1,1), (0,5), (3,4)].

What does this print?

[ccc, bb, a]. byLength orders shortest to longest (a, bb, ccc); reversed() flips that to longest to shortest.

Write code using Comparator chaining to sort a List<String> first by length ascending, then alphabetically for equal lengths.

import java.util.*; public class ChainDemo { public static void main(String[] args) { List<String> words = new ArrayList<>(List.of("bb", "a", "cc", "aa", "b")); Comparator<String> byLengthThenAlpha = Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()); words.sort(byLengthThenAlpha); System.out.println(words); // [a, b, aa, bb, cc] } } comparingInt(String::length) groups by length first; thenComparing(naturalOrder()) breaks ties alphabetically, giving [a, b, aa, bb, cc].

Comparator<String> byLength = Comparator.comparingInt(String::length);
List<String> words = new ArrayList<>(List.of("ccc", "a", "bb"));
words.sort(byLength.reversed());
System.out.println(words);