Java Comparable vs Comparator
Java Comparable vs Comparator
The question "which one should I use — Comparable or Comparator?" comes up every time you need to sort a custom class in Java. Both achieve the same end result: a sorted collection. But the problem they solve is different. Comparable answers "what is the natural order of this type?" — defined once, inside the class, used everywhere automatically. Comparator answers "how should I order these objects right now?" — defined outside the class, passed explicitly, and as many as needed.
What Are Comparable and Comparator?
Comparable<T> lives in java.lang. It is implemented by the class being sorted and defines a single natural ordering through compareTo(T other). Comparator<T> lives in java.util. It is implemented separately — as a lambda, a static method, or a dedicated class — and defines an alternate ordering through compare(T a, T b). Both return the same three-valued signal: negative, zero, or positive.
COMPARABLE COMPARATOR
─────────────────────────────── ───────────────────────────────────────
Package : java.lang Package : java.util
Method : compareTo(T other) Method : compare(T a, T b)
Location : INSIDE the class Location : OUTSIDE the class
Count : ONE per class Count : UNLIMITED per class
Used by : Collections.sort(list) Used by : Collections.sort(list, cmp)
Arrays.sort(array) Arrays.sort(array, cmp)
TreeSet (default) TreeSet(comparator)
TreeMap keys (default) TreeMap(comparator)
Modified : requires source access Modified : no source access needed
@FI : No @FI : Yes (lambda since Java 8)
RETURN VALUE (identical contract):
negative → first argument comes BEFORE second
zero → both are equal in this ordering
positive → first argument comes AFTER second
Basic Overview — Side-by-Side Comparison
FEATURE COMPARABLE COMPARATOR
──────────────────── ────────────────────── ──────────────────────────────
Implemented by The class itself Any class, lambda, or method ref
Sort orders possible One (natural) Unlimited per class
Modifying the class Required NOT required
Lambda syntax Not applicable (a, b) -> compare logic
Java 8 factories Not applicable Comparator.comparing(), etc.
Null arguments Must throw NPE Can handle via nullsFirst/Last
Consistency w/equals Strongly recommended Not required (but warned)
Package java.lang java.util
Used automatically Yes — sort uses it No — must be passed explicitly
Override natural Not without change Yes — pass Comparator to sort
EXAMPLE (sort by salary):
COMPARABLE approach: COMPARATOR approach:
class Employee Comparator<Employee> bySalary =
implements Comparable<Employee> Comparator.comparingDouble(
{ Employee::getSalary);
public int compareTo( employees.sort(bySalary);
Employee other) {
return Double.compare(
this.salary,
other.salary);
}
}
Comparable — Natural Ordering Inside the Class
Comparable is the contract that says: "this type has an obvious, inherent sort order that applies everywhere." Integer, String, LocalDate, BigDecimal all implement Comparable — their natural ordering is universally understood and applies to every sort without configuration.
When you implement Comparable on your own class, you are making the same declaration. The compareTo() method defines the single default sequence that all JDK sort utilities use automatically.
WHERE compareTo() IS CALLED AUTOMATICALLY: Collections.sort(List<T>) → T must implement Comparable Arrays.sort(T[]) → T must implement Comparable new TreeSet<T>() → T must implement Comparable new TreeMap<K, V>() → K must implement Comparable Collections.min(Collection<T>) → T must implement Comparable Collections.max(Collection<T>) → T must implement Comparable Collections.binarySearch(list, key)→ T must implement Comparable RULE: if a class has one obvious, stable sort order that belongs to the type itself — not to the caller's context — implement Comparable.
Comparator — External Ordering Defined by the Caller
Comparator is the contract that says: "sort these objects this way for this particular operation." It belongs to the caller, not the class. Three sorting requirements for the same Employee class — by salary for payroll, by name for a directory, by department then experience for an org chart — are three separate Comparator instances, none of which require touching the Employee class.
WHEN Comparator IS THE RIGHT CHOICE:
1. Multiple sort orders for the same class
— Employee: by salary, by name, by hire date, by department
2. Class is from a library or cannot be modified
— Sort a third-party DTO, a JDK class with no natural order, a record
3. Sort order is determined at runtime
— User selects "sort by rating" or "sort by price" from a UI
— Generic utility accepts a Comparator parameter
4. Inverse of natural ordering is needed
— Comparator.reverseOrder() for descending String sort
— comparator.reversed() to flip any existing Comparator
5. Null-safe sorting is needed
— Comparator.nullsFirst(Comparator) or nullsLast(Comparator)
— compareTo(null) must throw NPE by contract
6. Complex multi-field sort chain
— .thenComparing() for primary, secondary, tertiary fields
— Each level inverted independently if needed
Core Operations — Both in Action
Implementing Comparable — the Natural Order
// File: Order.java
import java.time.LocalDateTime;
import java.util.Objects;
// Natural ordering: chronological — oldest order first
// This is the universal sort order for Order: always by creation time
public class Order implements Comparable<Order> {
private final String orderId;
private final String customerId;
private final double totalAmount;
private final LocalDateTime createdAt;
private String status;
public Order(String orderId, String customerId,
double totalAmount, LocalDateTime createdAt) {
this.orderId = orderId;
this.customerId = customerId;
this.totalAmount = totalAmount;
this.createdAt = createdAt;
this.status = "PENDING";
}
public String getOrderId() { return orderId; }
public String getCustomerId() { return customerId; }
public double getTotalAmount() { return totalAmount; }
public LocalDateTime getCreatedAt() { return createdAt; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
// Natural ordering: chronological — oldest created first
// LocalDateTime.compareTo() handles all the date-time comparison
@Override
public int compareTo(Order other) {
// Primary: creation time (chronological)
int timeOrder = this.createdAt.compareTo(other.createdAt);
if (timeOrder != 0) return timeOrder;
// Secondary: orderId as stable tiebreaker for same-second orders
return this.orderId.compareTo(other.orderId);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Order)) return false;
return Objects.equals(this.orderId, ((Order) obj).orderId);
}
@Override
public int hashCode() { return Objects.hash(orderId); }
@Override
public String toString() {
return String.format("[%s] %-10s Rs.%7.2f %s %s",
orderId, customerId, totalAmount, createdAt.toLocalTime(), status);
}
}// File: ComparableDemo.java
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.TreeSet;
public class ComparableDemo {
public static void main(String[] args) {
List<Order> orders = new ArrayList<>();
LocalDateTime base = LocalDateTime.of(2024, 6, 10, 9, 0);
orders.add(new Order("ORD-004", "C-Rohan", 1299.0, base.plusMinutes(45)));
orders.add(new Order("ORD-001", "C-Priya", 549.0, base.plusMinutes(5)));
orders.add(new Order("ORD-003", "C-Karan", 2199.0, base.plusMinutes(30)));
orders.add(new Order("ORD-002", "C-Ananya", 899.0, base.plusMinutes(12)));
orders.add(new Order("ORD-005", "C-Divya", 349.0, base.plusMinutes(60)));
System.out.println("=== Before sort ===");
orders.forEach(System.out::println);
// Uses compareTo() — no Comparator needed, natural order is chronological
Collections.sort(orders);
System.out.println("\n=== After Collections.sort() — natural chronological order ===");
orders.forEach(System.out::println);
System.out.println();
// TreeSet auto-sorts using compareTo() — no Comparator constructor argument
System.out.println("=== TreeSet — chronological insertion ===");
TreeSet<Order> orderQueue = new TreeSet<>();
orderQueue.add(orders.get(2)); // ORD-003
orderQueue.add(orders.get(0)); // ORD-001
orderQueue.add(orders.get(4)); // ORD-005
orderQueue.forEach(System.out::println);
System.out.println("\nOldest order : " + Collections.min(orders));
System.out.println("Newest order : " + Collections.max(orders));
}
}Output:
=== Before sort ===
[ORD-004] C-Rohan Rs. 1299.00 09:45 PENDING
[ORD-001] C-Priya Rs. 549.00 09:05 PENDING
[ORD-003] C-Karan Rs. 2199.00 09:30 PENDING
[ORD-002] C-Ananya Rs. 899.00 09:12 PENDING
[ORD-005] C-Divya Rs. 349.00 10:00 PENDING
=== After Collections.sort() — natural chronological order ===
[ORD-001] C-Priya Rs. 549.00 09:05 PENDING
[ORD-002] C-Ananya Rs. 899.00 09:12 PENDING
[ORD-003] C-Karan Rs. 2199.00 09:30 PENDING
[ORD-004] C-Rohan Rs. 1299.00 09:45 PENDING
[ORD-005] C-Divya Rs. 349.00 10:00 PENDING
=== TreeSet — chronological insertion ===
[ORD-001] C-Priya Rs. 549.00 09:05 PENDING
[ORD-003] C-Karan Rs. 2199.00 09:30 PENDING
[ORD-005] C-Divya Rs. 349.00 10:00 PENDING
Oldest order : [ORD-001] C-Priya Rs. 549.00 09:05 PENDING
Newest order : [ORD-005] C-Divya Rs. 349.00 10:00 PENDING
Implementing Comparator — Multiple Orderings
// File: OrderComparators.java
import java.util.Comparator;
// All Comparator strategies for Order live here — centralised, named, reusable
public final class OrderComparators {
private OrderComparators() {}
// Natural order is chronological (from Comparable) — no Comparator needed
// Alternative 1: highest amount first (for revenue dashboard)
public static final Comparator<Order> BY_AMOUNT_DESC =
Comparator.comparingDouble(Order::getTotalAmount).reversed();
// Alternative 2: by customer ID then amount descending (customer ledger)
public static final Comparator<Order> BY_CUSTOMER_THEN_AMOUNT_DESC =
Comparator.comparing(Order::getCustomerId)
.thenComparingDouble(Order::getTotalAmount).reversed()
.thenComparing(Order::getOrderId); // stable tiebreaker
// Alternative 3: by status, then chronological within status (ops dashboard)
public static final Comparator<Order> BY_STATUS_THEN_TIME =
Comparator.comparing(Order::getStatus)
.thenComparing(Order::getCreatedAt)
.thenComparing(Order::getOrderId);
// Alternative 4: amount ascending, for low-value order prioritisation
public static final Comparator<Order> BY_AMOUNT_ASC =
Comparator.comparingDouble(Order::getTotalAmount)
.thenComparing(Order::getOrderId);
}// File: ComparatorDemo.java
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.TreeSet;
public class ComparatorDemo {
public static void main(String[] args) {
List<Order> orders = new ArrayList<>();
LocalDateTime base = LocalDateTime.of(2024, 6, 10, 9, 0);
orders.add(new Order("ORD-004", "C-Rohan", 1299.0, base.plusMinutes(45)));
orders.add(new Order("ORD-001", "C-Priya", 549.0, base.plusMinutes(5)));
orders.add(new Order("ORD-003", "C-Karan", 2199.0, base.plusMinutes(30)));
orders.add(new Order("ORD-002", "C-Ananya", 899.0, base.plusMinutes(12)));
orders.add(new Order("ORD-005", "C-Divya", 349.0, base.plusMinutes(60)));
orders.get(1).setStatus("DELIVERED");
orders.get(3).setStatus("FAILED");
// Sort using named Comparator — explicitly passed, overrides natural order
List<Order> byAmount = new ArrayList<>(orders);
byAmount.sort(OrderComparators.BY_AMOUNT_DESC);
System.out.println("=== BY_AMOUNT_DESC ===");
byAmount.forEach(System.out::println);
System.out.println();
List<Order> byStatus = new ArrayList<>(orders);
byStatus.sort(OrderComparators.BY_STATUS_THEN_TIME);
System.out.println("=== BY_STATUS_THEN_TIME ===");
byStatus.forEach(System.out::println);
System.out.println();
// TreeSet with custom Comparator — overrides natural order for this TreeSet
Comparator<Order> byAmountAndId =
Comparator.comparingDouble(Order::getTotalAmount)
.thenComparing(Order::getOrderId);
TreeSet<Order> amountTree = new TreeSet<>(byAmountAndId);
orders.forEach(amountTree::add);
System.out.println("=== TreeSet(byAmountAndId) — cheapest first ===");
amountTree.forEach(System.out::println);
System.out.println();
// Runtime sort selection — Comparator chosen by user preference
String userChoice = "AMOUNT_DESC"; // could come from request param
Comparator<Order> selected = switch (userChoice) {
case "AMOUNT_ASC" -> OrderComparators.BY_AMOUNT_ASC;
case "AMOUNT_DESC" -> OrderComparators.BY_AMOUNT_DESC;
case "STATUS" -> OrderComparators.BY_STATUS_THEN_TIME;
default -> Comparator.naturalOrder(); // falls back to compareTo
};
List<Order> userSorted = new ArrayList<>(orders);
userSorted.sort(selected);
System.out.println("=== User-selected sort: " + userChoice + " ===");
userSorted.forEach(System.out::println);
}
}Output:
=== BY_AMOUNT_DESC ===
[ORD-003] C-Karan Rs. 2199.00 09:30 PENDING
[ORD-004] C-Rohan Rs. 1299.00 09:45 PENDING
[ORD-002] C-Ananya Rs. 899.00 09:12 FAILED
[ORD-001] C-Priya Rs. 549.00 09:05 DELIVERED
[ORD-005] C-Divya Rs. 349.00 10:00 PENDING
=== BY_STATUS_THEN_TIME ===
[ORD-001] C-Priya Rs. 549.00 09:05 DELIVERED
[ORD-002] C-Ananya Rs. 899.00 09:12 FAILED
[ORD-003] C-Karan Rs. 2199.00 09:30 PENDING
[ORD-004] C-Rohan Rs. 1299.00 09:45 PENDING
[ORD-005] C-Divya Rs. 349.00 10:00 PENDING
=== TreeSet(byAmountAndId) — cheapest first ===
[ORD-005] C-Divya Rs. 349.00 10:00 PENDING
[ORD-001] C-Priya Rs. 549.00 09:05 DELIVERED
[ORD-002] C-Ananya Rs. 899.00 09:12 FAILED
[ORD-004] C-Rohan Rs. 1299.00 09:45 PENDING
[ORD-003] C-Karan Rs. 2199.00 09:30 PENDING
=== User-selected sort: AMOUNT_DESC ===
[ORD-003] C-Karan Rs. 2199.00 09:30 PENDING
[ORD-004] C-Rohan Rs. 1299.00 09:45 PENDING
[ORD-002] C-Ananya Rs. 899.00 09:12 FAILED
[ORD-001] C-Priya Rs. 549.00 09:05 DELIVERED
[ORD-005] C-Divya Rs. 349.00 10:00 PENDING
Using Both Together — Natural Order and Custom Sort
// File: UsingBothDemo.java
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class UsingBothDemo {
public static void main(String[] args) {
List<Order> orders = new ArrayList<>();
LocalDateTime base = LocalDateTime.of(2024, 6, 10, 9, 0);
orders.add(new Order("ORD-004", "C-Rohan", 1299.0, base.plusMinutes(45)));
orders.add(new Order("ORD-001", "C-Priya", 549.0, base.plusMinutes(5)));
orders.add(new Order("ORD-003", "C-Karan", 2199.0, base.plusMinutes(30)));
orders.add(new Order("ORD-002", "C-Ananya", 899.0, base.plusMinutes(12)));
orders.add(new Order("ORD-005", "C-Divya", 349.0, base.plusMinutes(60)));
// Comparator.naturalOrder() delegates to compareTo() — uses Comparable
// Useful when you need to pass an explicit Comparator but want natural order
List<Order> natural = new ArrayList<>(orders);
natural.sort(Comparator.naturalOrder()); // same as Collections.sort(natural)
System.out.println("=== naturalOrder() — delegates to compareTo() ===");
natural.forEach(System.out::println);
System.out.println();
// Reverse of natural order — newest orders first
List<Order> reverseNatural = new ArrayList<>(orders);
reverseNatural.sort(Comparator.reverseOrder()); // reverse of compareTo()
System.out.println("=== reverseOrder() — reverse of compareTo() ===");
reverseNatural.forEach(System.out::println);
System.out.println();
// Natural order used as tiebreaker WITHIN a Comparator chain
// Sort: lowest amount first, then natural order (chronological) on ties
Comparator<Order> amountThenNatural =
Comparator.comparingDouble(Order::getTotalAmount)
.thenComparing(Comparator.naturalOrder()); // uses compareTo() for ties
List<Order> mixed = new ArrayList<>(orders);
mixed.sort(amountThenNatural);
System.out.println("=== Amount ASC, natural order (chronological) on ties ===");
mixed.forEach(System.out::println);
}
}Output:
=== naturalOrder() — delegates to compareTo() ===
[ORD-001] C-Priya Rs. 549.00 09:05 PENDING
[ORD-002] C-Ananya Rs. 899.00 09:12 PENDING
[ORD-003] C-Karan Rs. 2199.00 09:30 PENDING
[ORD-004] C-Rohan Rs. 1299.00 09:45 PENDING
[ORD-005] C-Divya Rs. 349.00 10:00 PENDING
=== reverseOrder() — reverse of compareTo() ===
[ORD-005] C-Divya Rs. 349.00 10:00 PENDING
[ORD-004] C-Rohan Rs. 1299.00 09:45 PENDING
[ORD-003] C-Karan Rs. 2199.00 09:30 PENDING
[ORD-002] C-Ananya Rs. 899.00 09:12 PENDING
[ORD-001] C-Priya Rs. 549.00 09:05 PENDING
=== Amount ASC, natural order (chronological) on ties ===
[ORD-005] C-Divya Rs. 349.00 10:00 PENDING
[ORD-001] C-Priya Rs. 549.00 09:05 PENDING
[ORD-002] C-Ananya Rs. 899.00 09:12 PENDING
[ORD-004] C-Rohan Rs. 1299.00 09:45 PENDING
[ORD-003] C-Karan Rs. 2199.00 09:30 PENDING
Basic Example
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Main {
static class Student implements Comparable<Student> {
String name;
int score;
Student(String name, int score) {
this.name = name;
this.score = score;
}
// Natural ordering: alphabetical by name
@Override
public int compareTo(Student other) {
return this.name.compareTo(other.name);
}
@Override
public String toString() {
return name + "(" + score + ")";
}
}
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("Ravi", 82));
students.add(new Student("Asha", 95));
students.add(new Student("Meena", 70));
students.sort(null); // uses compareTo() — natural order, by name
System.out.println("By name (Comparable): " + students);
Comparator<Student> byScore = Comparator.comparingInt(s -> s.score);
students.sort(byScore); // uses a Comparator — an alternative order
System.out.println("By score (Comparator): " + students);
}
}Output:
By name (Comparable): [Asha(95), Meena(70), Ravi(82)]
By score (Comparator): [Meena(70), Ravi(82), Asha(95)]
Student implements Comparable<Student> with compareTo() defined by name, so that is its one natural ordering — calling sort(null) uses it automatically. byScore is a separate Comparator<Student> built with Comparator.comparingInt(), defined completely outside the Student class, and passing it to sort() produces a different order without touching compareTo(). This shows the core distinction: Comparable is the single ordering built into the type, while Comparator is an alternative ordering supplied by the caller.
Performance Considerations
Both compareTo() and compare() are called O(n log n) times during a sort. The performance difference between Comparable and Comparator at runtime is negligible — both reduce to field comparisons. The practical performance distinction is at a higher level.
PERFORMANCE COMPARISON:
comparable — compareTo():
Called automatically, no allocation overhead
JIT inlines it aggressively (simple method, no virtual dispatch overhead)
Comparator lambda:
Minor object allocation per Comparator.comparing() call (cached by JVM)
Lambda body inlined by JIT after first few calls — effectively zero overhead
Comparator.comparing() chains:
Key extractor called ONCE per comparison — same as manual lambda
.thenComparing() only evaluates secondary comparison when primary returns 0
For common cases (primary rarely ties): nearly identical to single-field sort
REAL PERFORMANCE RISKS:
— Doing I/O, network calls, or O(n) operations inside compare/compareTo
— Not pre-computing expensive keys before sort (Schwartzian transform)
— Using null checks in compareTo instead of Comparator.nullsFirst/Last
MEMORY:
— Comparable: zero extra allocation — method on existing object
— Comparator lambda: ~64 bytes per lambda instance (singleton after warmup)
— Comparator.comparing() chain: one wrapper object per chain step
— All negligible at typical list sizes
Best Practices
Implement Comparable when the class has one obvious, intrinsic sort order. Employee by employeeId, Product by SKU, Event by timestamp — these are orderings that "belong to" the type. Every caller that sorts a list of these objects expects the same result. Comparable makes that expectation explicit and removes the need for every caller to provide a Comparator.
Use Comparator for every situational, context-dependent, or alternative ordering. A report needs employees by salary. A directory needs them by name. An org chart needs them by department, then seniority. None of these is "the inherent order of an Employee" — they are orderings chosen by specific features. Each lives in its own named Comparator constant, not in the Employee class.
Store Comparator instances as named static final constants. public static final Comparator<Order> BY_AMOUNT_DESC = Comparator.comparingDouble(Order::getTotalAmount).reversed() is self-documenting, testable in isolation, and reusable across the codebase without re-creating the lambda on each call. Inline anonymous comparators in sort calls are harder to test and name.
Always add a unique tiebreaker as the last comparison in any chain. Comparator.comparing(X::status).thenComparingInt(X::id) — the id field ensures the comparator returns 0 only for truly identical objects. Without a tiebreaker, two objects that share a status compare as equal, which causes silent data loss in TreeSet and TreeMap. The same applies to compareTo() — always chain to a unique ID field.
Common Mistakes
Mistake 1 — Implementing compareTo() Inconsistently With equals()
// WRONG — compareTo sorts by salary, equals uses employeeId
// Same salary → TreeSet rejects second insert (compareTo = 0)
// Different ID → HashMap keeps both (equals = false)
// Mixed use produces contradictory results
public int compareTo(Employee other) {
return Double.compare(this.salary, other.salary); // mismatched with equals
}
public boolean equals(Object o) {
return this.employeeId == ((Employee) o).employeeId; // uses ID
}
// CORRECT — compareTo uses salary as primary, ID as tiebreaker
// compareTo returns 0 only when ID also matches → consistent with equals
public int compareTo(Employee other) {
int salaryOrder = Double.compare(this.salary, other.salary);
if (salaryOrder != 0) return salaryOrder;
return Integer.compare(this.employeeId, other.employeeId); // tiebreaker = equals field
}Mistake 2 — Using a Comparator Without a Tiebreaker in TreeSet
// WRONG — two employees in the same department → compare() = 0 → second dropped
TreeSet<Employee> byDept = new TreeSet<>(
Comparator.comparing(Employee::getDepartment)
);
byDept.add(new Employee(1, "Priya", "Engineering", 92000));
byDept.add(new Employee(2, "Rohan", "Engineering", 78000)); // silently dropped!
System.out.println(byDept.size()); // 1, not 2
// CORRECT — chain to unique ID field as tiebreaker
TreeSet<Employee> byDeptSafe = new TreeSet<>(
Comparator.comparing(Employee::getDepartment)
.thenComparingInt(Employee::getEmployeeId)
);Mistake 3 — Modifying the Class for Sort Flexibility When Comparator Fits Better
// WRONG — changing natural order or adding multiple compareTo strategies
// because some feature needs a different sort. Comparable is for ONE natural order.
public int compareTo(Order other) {
if (sortByAmount) { // WRONG — state in compareTo!
return Double.compare(this.amount, other.amount);
} else {
return this.createdAt.compareTo(other.createdAt);
}
}
// CORRECT — natural order is one thing (chronological); all other orderings
// are Comparators defined in a dedicated class
public int compareTo(Order other) {
return this.createdAt.compareTo(other.createdAt); // one natural order
}
// Different sorts are separate Comparator constantsMistake 4 — Forgetting That Comparator.reverseOrder() Requires Comparable
// WRONG — Comparator.reverseOrder() on a class that does NOT implement Comparable
// throws ClassCastException at runtime
List<Employee> employees = ...;
employees.sort(Comparator.reverseOrder()); // ClassCastException: Employee not Comparable
// CORRECT for reversing a custom Comparator:
Comparator<Employee> byId = Comparator.comparingInt(Employee::getEmployeeId);
Comparator<Employee> byIdDesc = byId.reversed(); // reverses byId — no Comparable needed
// Comparator.reverseOrder() only works for types that already implement Comparable:
List<String> names = ...;
names.sort(Comparator.reverseOrder()); // fine — String implements ComparableInterview Questions
Q1. What is the difference between Comparable and Comparator in Java?
Comparable<T> is in java.lang, implemented inside the class, and defines one natural ordering through compareTo(T other). Every JDK sort utility uses it automatically — no explicit parameter needed. Comparator<T> is in java.util, defined outside the class, and defines alternative orderings through compare(T a, T b). It must be passed explicitly to sort methods or collection constructors. A class implements Comparable once; any number of Comparator instances can exist for the same class. Use Comparable for the inherent, universally expected order; use Comparator for context-specific or multiple orderings.
Q2. Can a class implement both Comparable and Comparator at the same time?
Yes. A class can implement Comparable for its natural ordering and also provide static Comparator constants or factory methods for alternative orderings. This is the cleanest design: Employee implements Comparable<Employee> for natural order by employeeId, plus public static final Comparator<Employee> BY_SALARY and BY_DEPARTMENT as static constants or a factory method. When Comparator.naturalOrder() is passed to a sort, it delegates to compareTo(), allowing both to coexist cleanly.
Q3. When would you choose Comparator over Comparable?
Use Comparator when: the class is from an external library and cannot be modified; multiple sort orders are needed for the same type; the sort order is chosen at runtime (user selects a sort column); or null-safe sorting is required (Comparator.nullsFirst/Last handles nulls, while compareTo(null) must throw NullPointerException). Comparator.comparing() and its chain methods also provide cleaner multi-field sort syntax than nested if blocks inside compareTo().
Q4. How does Collections.sort() decide whether to use compareTo() or compare()?
Collections.sort(List<T>) with no second argument casts elements to Comparable<T> and calls compareTo(). Collections.sort(List<T>, Comparator<T>) calls compare() on the provided Comparator and does not call compareTo() at all. When a Comparator is passed to new TreeSet<>(comparator), the Comparator.compare() replaces compareTo() for that collection's ordering and uniqueness decisions. Comparator.naturalOrder() wraps compareTo() as a Comparator object, allowing natural order to be passed where a Comparator parameter is required.
Q5. What is the consistency-with-equals rule and which interface does it apply to?
The rule states that x.compareTo(y) == 0 should imply x.equals(y) == true — and vice versa. It is formally recommended for Comparable. When violated, sorted collections (TreeSet, TreeMap) behave inconsistently with hash-based collections (HashSet, HashMap): a TreeSet considers two objects with compareTo == 0 as duplicates and rejects the second; a HashSet uses equals() and hashCode(), so it may store both. BigDecimal is the standard Java example of a violation: new BigDecimal("2.0").compareTo(new BigDecimal("2.00")) == 0 but their equals() returns false. For Comparator, consistency with equals is not required — a Comparator for a TreeSet may intentionally use a stricter or looser equality than equals().
Q6. How do you implement a null-safe sort and which interface makes it easier?
Comparator makes null handling straightforward through Comparator.nullsFirst(comparator) and Comparator.nullsLast(comparator). These wrap any comparator and sort null elements before or after all non-null elements without any manual null checks in the comparison logic. Comparable.compareTo(null) must throw NullPointerException by contract — null-safe comparison is not possible through Comparable alone. If a collection may contain null elements, use Comparator.nullsLast(Comparator.naturalOrder()) to pass to the sort method instead of relying on compareTo().
FAQs
Which is older — Comparable or Comparator?
Both were introduced in Java 1.2 with the Collections Framework. Comparable is in java.lang, which predates the framework, but the generics-based Comparable<T> was added in Java 5 alongside Comparator<T>. The major Comparator API improvements — comparing(), thenComparing(), reversed(), nullsFirst() — were added in Java 8 when it became a @FunctionalInterface.
Does implementing Comparable affect HashMap or HashSet behaviour?
No. HashMap and HashSet use equals() and hashCode() — they are entirely unaware of compareTo(). A class can implement Comparable and have it affect only sorted collections (TreeSet, TreeMap, sort methods) while hash-based collections continue using equality. The consistency-with-equals rule is a recommendation to prevent contradictions between sorted and hash-based behaviour, not a JVM enforcement.
Can I use a Comparator with Arrays.sort()?
Yes. Arrays.sort(T[] array, Comparator<T> comparator) accepts a Comparator. Arrays.sort(T[] array) (no comparator) uses natural ordering — T must implement Comparable. The same dual API exists for all sort methods in Java: one overload for natural order (Comparable), one for custom order (Comparator).
What happens if compareTo() is called on null?
By contract, x.compareTo(null) must throw NullPointerException even if null were otherwise a valid value. This is why Comparator.nullsFirst() exists — to wrap any comparator with null handling that compareTo() cannot provide. For Comparator.compare(null, y), the behaviour depends on the implementation; Comparator.nullsFirst() and nullsLast() explicitly support null.
Is it safe to use only a Comparator on a TreeSet without the class implementing Comparable?
Yes. When a Comparator is passed to new TreeSet<>(comparator), the TreeSet uses comparator.compare() for all operations. It never calls compareTo(). The class does not need to implement Comparable. This is why third-party classes with no natural ordering can still be stored in a sorted collection — just provide the appropriate Comparator.
When does reverseOrder() fail at runtime?
Comparator.reverseOrder() returns a comparator that casts elements to Comparable and calls compareTo() in reverse. If the element type does not implement Comparable, this throws ClassCastException at sort time. Use it only for types that implement Comparable (String, Integer, LocalDate, etc.). For reversing a custom class without Comparable, use .reversed() on an explicit comparator: Comparator.comparingInt(Employee::getId).reversed().
Summary
Comparable and Comparator solve the same problem — ordering objects — but at different levels of responsibility. Comparable defines the intrinsic, universal sort order of a class: one definition, inside the class, used automatically everywhere. Comparator defines a contextual, caller-supplied sort order: unlimited definitions, outside the class, passed explicitly when needed.
The practical rule: if every piece of code that sorts a collection of this type would expect the same default order, that order belongs in Comparable. If the order depends on the context — which report is being generated, what the user selected, which field the API sorts on — it belongs in a named Comparator constant.
Both can coexist cleanly on the same class. Comparator.naturalOrder() and Comparator.reverseOrder() bridge between the two, allowing natural order to be used where a Comparator object is required. The four rules that prevent the most common bugs are consistent across both: use Integer.compare() not subtraction, chain to a unique tiebreaker, keep compareTo consistent with equals, and handle nulls through Comparator.nullsFirst/Last rather than manual null checks.
What to Read Next
Use Java's built-in helper methods for working with collections.
Practice Programs
Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.
Implements Comparable on a custom class to define its natural ordering.
Sorts a list of objects by one field, then breaks ties using a second field.
Sorts a List of custom objects using the natural ordering defined by Comparable.
Sorts a list of custom objects using an external Comparator instead of natural ordering.