Comparable vs Comparator
Sorting numbers and strings is easy — Java already knows how to order them. But what about your own custom objects? That’s where Comparable and Comparator come in. Both interfaces let you define ordering rules, but they serve different purposes and live in different places in your design.
The Core Difference
| Feature | Comparable | Comparator |
|---|---|---|
| Package | java.lang | java.util |
| Method | compareTo(T o) | compare(T o1, T o2) |
| Defined in | The class being sorted | A separate class / lambda |
| Sorts by | One natural order | Any number of custom orders |
| Modifies original class? | Yes | No |
| Used by | Collections.sort(list), Arrays.sort() | Collections.sort(list, cmp), Arrays.sort(arr, cmp) |
Think of Comparable as the object saying “I know how to compare myself.” Comparator is an external judge that says “I’ll decide the order for you.”
Comparable — Natural Ordering
Implement Comparable<T> directly in your class when there is one obvious, logical ordering — like sorting employees by ID or products by price.
import java.util.*;
public class Employee implements Comparable<Employee> {
int id;
String name;
Employee(int id, String name) {
this.id = id;
this.name = name;
}
// Natural order: ascending by ID
@Override
public int compareTo(Employee other) {
return Integer.compare(this.id, other.id);
}
@Override
public String toString() {
return id + ":" + name;
}
public static void main(String[] args) {
List<Employee> list = new ArrayList<>();
list.add(new Employee(3, "Alice"));
list.add(new Employee(1, "Bob"));
list.add(new Employee(2, "Carol"));
Collections.sort(list); // uses compareTo
System.out.println(list);
}
}
Output:
[1:Bob, 2:Carol, 3:Alice]
The compareTo Contract
Your compareTo method must return:
- negative —
thiscomes beforeother - zero — they are considered equal
- positive —
thiscomes afterother
Warning: Never return hardcoded
1,0,-1by subtracting integers (e.g.this.id - other.id). Integer subtraction can overflow for large or negative values. UseInteger.compare(),Double.compare(), orString.compareTo()instead.
Comparator — Custom Ordering
Comparator<T> lives outside your class. Use it when you need multiple sort orders, when you cannot modify the class (e.g. a library type), or when the ordering is context-dependent.
import java.util.*;
public class SortByName {
public static void main(String[] args) {
List<Employee> list = new ArrayList<>();
list.add(new Employee(3, "Alice"));
list.add(new Employee(1, "Bob"));
list.add(new Employee(2, "Carol"));
// Sort by name using a Comparator lambda
list.sort(Comparator.comparing(e -> e.name));
System.out.println(list);
}
}
Output:
[3:Alice, 1:Bob, 2:Carol]
Tip:
list.sort(comparator)(added in Java 8) is equivalent toCollections.sort(list, comparator)and is generally preferred today.
Classic Comparator with an Anonymous Class
Before Java 8, you wrote a full anonymous class. You may still see this in older codebases:
Comparator<Employee> byName = new Comparator<Employee>() {
@Override
public int compare(Employee a, Employee b) {
return a.name.compareTo(b.name);
}
};
Collections.sort(list, byName);
Since Java 8, Comparator is a functional interface, so a lambda expression is far cleaner.
Chaining Comparators — Multiple Sort Keys
One of Comparator’s superpowers is easy chaining with thenComparing:
import java.util.*;
public class ChainedSort {
public static void main(String[] args) {
List<Employee> list = new ArrayList<>();
list.add(new Employee(2, "Carol"));
list.add(new Employee(1, "Bob"));
list.add(new Employee(2, "Alice")); // same id as Carol
// Primary: id ascending; Secondary: name ascending
list.sort(Comparator.comparingInt((Employee e) -> e.id)
.thenComparing(e -> e.name));
System.out.println(list);
}
}
Output:
[1:Bob, 2:Alice, 2:Carol]
Reverse Order
// Descending by ID
list.sort(Comparator.comparingInt((Employee e) -> e.id).reversed());
Null-safe Sorting
// Nulls first, then sort by name
list.sort(Comparator.nullsFirst(Comparator.comparing(e -> e.name)));
Sorting with Arrays
Both interfaces work identically with arrays via Arrays.sort():
import java.util.Arrays;
import java.util.Comparator;
public class ArraySort {
public static void main(String[] args) {
String[] words = {"banana", "apple", "cherry"};
// Natural order (String already implements Comparable)
Arrays.sort(words);
System.out.println(Arrays.toString(words));
// Custom: sort by string length
Arrays.sort(words, Comparator.comparingInt(String::length));
System.out.println(Arrays.toString(words));
}
}
Output:
[apple, banana, cherry]
[apple, banana, cherry]
Comparable and Comparator Together
You can implement Comparable for the default order and still pass a Comparator when you need something different — they do not conflict.
// Natural order: sort by ID (Comparable)
Collections.sort(list);
// Custom order: sort by name (Comparator) — original class unchanged
list.sort(Comparator.comparing(e -> e.name));
This pattern is common in the Collections Framework itself. TreeSet and TreeMap use natural ordering (via Comparable) unless you supply a Comparator at construction time.
When to Use Which
- Use
Comparablewhen there is one clear, universally agreed-upon natural order for your objects (e.g. dates ordered by time, employees ordered by ID). - Use
Comparatorwhen you need multiple sort strategies, when you cannot touch the class source code, or when sorting by different fields in different contexts (e.g. sort by salary in one report, by name in another).
Note: If your class will be used as a key in a
TreeMapor stored in aTreeSet, it must implementComparable(or you must provide aComparatorat construction time). Forgetting this causes aClassCastExceptionat runtime.
Under the Hood
Java’s Collections.sort() and Arrays.sort() both use TimSort (a hybrid of merge sort and insertion sort) since Java 7. TimSort is stable — equal elements stay in their original relative order — which makes chained comparisons work correctly.
When you call Collections.sort(list), the JVM casts each element to Comparable internally. If your class does not implement it, you get a ClassCastException, not a compile-time error (unless you use generics properly). This is why parameterizing your list (List<Employee>) and implementing Comparable<Employee> (not raw Comparable) matters: the compiler can catch type mismatches early.
Under the hood, the Comparator.comparing() static factory methods return lambda-backed Comparator instances. The JVM represents these as invokedynamic call sites, so there is no object allocation overhead for the lambda capture in the common case where no variables are captured — the JDK generates a static field-cached instance.
For primitive-heavy sorting, Comparator.comparingInt/comparingLong/comparingDouble avoid boxing overhead by accepting ToIntFunction, ToLongFunction, or ToDoubleFunction references, which are worth preferring over Comparator.comparing() when sorting large collections of objects with primitive fields.
Related Topics
- Comparable — deep dive into implementing the Comparable interface
- Comparator — deep dive into Comparator, lambdas, and method references with sorting
- Sorting Collections — practical guide to sorting lists, maps, and arrays in Java
- Collections Utility Class —
Collections.sort(),reverse(),shuffle(), and more - TreeSet — a sorted Set that relies on natural ordering or a Comparator
- TreeMap — a sorted Map that uses Comparable or Comparator for key ordering
- Lambda Expressions — write concise Comparators with lambdas and method references