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+)
| Method | Description |
|---|---|
Comparator.comparing(keyExtractor) | Compare by a key extracted with a function. |
Comparator.comparingInt/Long/Double | Type-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
| Scenario | Use Comparable | Use 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.pricecan overflow forintand loses precision fordouble. Always useInteger.compare(),Double.compare(), etc. - Inconsistency with equals() — the contract recommends that
compareTo(x) == 0is consistent withx.equals(this). Violating this causes surprising behaviour inTreeSetandTreeMap. - Null handling — neither
Comparable.compareTo()nor a customComparatorhandles null by default. UseComparator.nullsFirst()ornullsLast().
See Also
- Java Streams API: The Complete Reference Guide
- Java Collections Framework: Choosing the Right Data Structure
- Java BufferedReader Tutorial
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.