Java Comparable vs Comparator: The Definitive Guide with Examples

Every Java developer eventually reaches for Collections.sort() or List.sort() and immediately faces a choice: implement Comparable on the class, or pass a Comparator at the call site? The two interfaces serve different purposes, and picking the wrong one leads to inflexible designs and subtle ordering bugs. In this guide you will understand exactly what each interface does, see annotated side-by-side examples, and learn the rules for when to use which — including the modern lambda and method-reference syntax that makes comparators a joy to write.

The Core Difference in One Sentence

Comparable defines the natural order of a class — one fixed ordering baked into the class itself. Comparator defines an external order — any number of separate comparison strategies that can be created and passed without modifying the class.

Comparable — Natural Ordering

java.lang.Comparable<T> has a single method:

public interface Comparable<T> {
    int compareTo(T other);
}

The contract: return a negative integer if this < other, zero if equal, and a positive integer if this > other.

Example: Product Sorted by Price

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// Product implements Comparable: natural order = ascending price
public class Product implements Comparable<Product> {

    private final String name;
    private final double price;

    public Product(String name, double price) {
        this.name  = name;
        this.price = price;
    }

    @Override
    public int compareTo(Product other) {
        // Double.compare avoids the subtraction pitfall with floating-point numbers
        return Double.compare(this.price, other.price);
    }

    @Override
    public String toString() {
        return name + "($" + price + ")";
    }

    public static void main(String[] args) {
        List<Product> products = new ArrayList<>();
        products.add(new Product("Keyboard", 49.99));
        products.add(new Product("Monitor",  299.00));
        products.add(new Product("Mouse",     29.50));
        products.add(new Product("Webcam",    79.99));

        Collections.sort(products);   // uses compareTo() automatically
        System.out.println(products);
    }
}
// Output
[Mouse($29.5), Keyboard($49.99), Webcam($79.99), Monitor($299.0)]

Comparator — Custom / External Ordering

java.util.Comparator<T> is a functional interface:

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

Because it is a functional interface you can create comparators with lambdas and method references — no boilerplate class required.

Example: Multiple Sort Orders for the Same Class

import java.util.*;

public class ProductComparatorDemo {

    public static void main(String[] args) {
        List<Product> products = new ArrayList<>();
        products.add(new Product("Keyboard", 49.99));
        products.add(new Product("Monitor",  299.00));
        products.add(new Product("Mouse",     29.50));
        products.add(new Product("Webcam",    79.99));

        // Sort by name ascending (lambda)
        products.sort(Comparator.comparing(p -> p.getName()));
        System.out.println("By name  : " + products);

        // Sort by price descending (method reference + reversed)
        products.sort(Comparator.comparingDouble(Product::getPrice).reversed());
        System.out.println("By price-: " + products);

        // Sort by price asc, then by name asc (chained comparators)
        Comparator<Product> byPriceThenName =
            Comparator.comparingDouble(Product::getPrice)
                      .thenComparing(Product::getName);
        products.sort(byPriceThenName);
        System.out.println("Price+Name: " + products);
    }
}
By name  : [Keyboard($49.99), Monitor($299.0), Mouse($29.5), Webcam($79.99)]
By price-: [Monitor($299.0), Webcam($79.99), Keyboard($49.99), Mouse($29.5)]
Price+Name: [Mouse($29.5), Keyboard($49.99), Webcam($79.99), Monitor($299.0)]

Useful Comparator Factory Methods (Java 8+)

MethodDescription
Comparator.comparing(keyExtractor)Compare by a key extracted with a function.
Comparator.comparingInt/Long/DoubleType-specific variants that avoid boxing.
.thenComparing(keyExtractor)Break ties with a secondary comparator.
.reversed()Flip the current comparator to descending order.
Comparator.naturalOrder()Returns a comparator that uses compareTo().
Comparator.reverseOrder()Descending natural order.
Comparator.nullsFirst(c)Treat null values as smaller than non-null.
Comparator.nullsLast(c)Treat null values as larger than non-null.

Comparable vs Comparator: When to Use Which

ScenarioUse ComparableUse Comparator
There is one obvious, logical ordering✔
You own the class and can modify it✔
You need multiple sort orders✔
You cannot modify the class (third-party)✔
The order is context-dependent (e.g. UI vs storage)✔
You want TreeSet/TreeMap to sort automatically✔✔ (pass to constructor)

Using Both Together: TreeSet with Natural and Custom Order

import java.util.TreeSet;

public class TreeSetDemo {
    public static void main(String[] args) {

        // Uses Product.compareTo() -- natural order (ascending price)
        TreeSet<Product> byNaturalOrder = new TreeSet<>();
        byNaturalOrder.add(new Product("Monitor", 299.0));
        byNaturalOrder.add(new Product("Mouse",    29.5));
        byNaturalOrder.add(new Product("Keyboard", 49.99));
        System.out.println("Natural : " + byNaturalOrder);

        // Pass a Comparator to override order (descending price)
        TreeSet<Product> byPriceDesc =
            new TreeSet<>(Comparator.comparingDouble(Product::getPrice).reversed());
        byPriceDesc.add(new Product("Monitor", 299.0));
        byPriceDesc.add(new Product("Mouse",    29.5));
        byPriceDesc.add(new Product("Keyboard", 49.99));
        System.out.println("Desc    : " + byPriceDesc);
    }
}
Natural : [Mouse($29.5), Keyboard($49.99), Monitor($299.0)]
Desc    : [Monitor($299.0), Keyboard($49.99), Mouse($29.5)]

Common Pitfalls

  • Subtraction trick is dangerous for numeric comparisons — return o1.price - o2.price can overflow for int and loses precision for double. Always use Integer.compare(), Double.compare(), etc.
  • Inconsistency with equals() — the contract recommends that compareTo(x) == 0 is consistent with x.equals(this). Violating this causes surprising behaviour in TreeSet and TreeMap.
  • Null handling — neither Comparable.compareTo() nor a custom Comparator handles null by default. Use Comparator.nullsFirst() or nullsLast().

See Also

Conclusion

Implement Comparable when your class has one obvious, universally agreed-upon ordering that belongs to the class itself. Reach for Comparator whenever you need flexibility: multiple orderings, descending sorts, chained criteria, or sorting a class you do not own. Java 8’s Comparator.comparing() factory and its chaining methods make external comparators concise and readable — often a one-liner. Master both interfaces and you will handle any sorting requirement the Java platform throws at you.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.