Definition
- Comparable
( java.lang): the class implementscompareTo(T other)to define its natural ordering; one ordering per class, baked in - Comparator
( java.util): a separate object or lambda that implementscompare(T a, T b)to define external ordering; multiple orderings possible without modifying the class compareToreturns: negative (this < other), zero (equal), positive (this > other)- Use Comparable for THE default sort (accounts by account ID); use Comparator for alternate sorts (by balance, by date, by holder name)
Comparable: the account card knows how to sort itself: "I compare myself by account number." One built-in order. Comparator: the bank manager decides the sort order: "Sort by balance today, sort by name tomorrow, sort by branch next week." The account card does not change – only the external sorting rule changes.
Understanding the Concepts
What is the difference between Comparable and Comparator?
| Feature | Comparable | Comparator |
|---|---|---|
| Package | java.lang | java.util |
| Method | compareTo(T other) | compare(T a, T b) |
| Who defines | The class itself | External class/lambda |
| Number of orderings | One (natural) | Unlimited |
| Modifies the class | Yes | No |
| Used by default | Collections.sort, TreeSet | Passed explicitly |
| Functional interface | No (has compareTo) | Yes (SAM = compare) |
package com.bank.model;
import java.math.BigDecimal;
import java.time.LocalDate;
// Comparable: natural ordering by account ID
public class Account implements Comparable<Account> {
private final String accountId;
private final String holder;
private final BigDecimal balance;
private final LocalDate openDate;
public Account(String accountId, String holder, BigDecimal balance, LocalDate openDate) {
this.accountId = accountId;
this.holder = holder;
this.balance = balance;
this.openDate = openDate;
}
@Override
public int compareTo(Account other) {
return this.accountId.compareTo(other.accountId); // natural order: by ID
}
// getters
public String getAccountId() { return accountId; }
public String getHolder() { return holder; }
public BigDecimal getBalance() { return balance; }
public LocalDate getOpenDate() { return openDate; }
@Override
public String toString() {
return accountId + " (" + holder + ": " + balance + ")";
}
}
package com.bank.demo;
import com.bank.model.Account;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
public class ComparableVsComparatorDemo {
public static void main(String[] args) {
List<Account> accounts = new ArrayList<>(List.of(
new Account("SAV2001", "Rajesh", new BigDecimal("150000"), LocalDate.of(2020, 1, 15)),
new Account("CUR3001", "Priya", new BigDecimal("75000"), LocalDate.of(2021, 6, 1)),
new Account("FD4001", "Amit", new BigDecimal("500000"), LocalDate.of(2019, 3, 20))
));
// Comparable: natural order (by account ID)
Collections.sort(accounts);
System.out.println("Natural order (Comparable):");
accounts.forEach(a -> System.out.println(" " + a));
// Comparator: sort by balance ascending
accounts.sort(Comparator.comparing(Account::getBalance));
System.out.println("\nBy balance (Comparator):");
accounts.forEach(a -> System.out.println(" " + a));
// Comparator: sort by balance descending, then by name
accounts.sort(
Comparator.comparing(Account::getBalance).reversed()
.thenComparing(Account::getHolder)
);
System.out.println("\nBy balance desc, then name:");
accounts.forEach(a -> System.out.println(" " + a));
}
}
Natural order (Comparable):
CUR3001 (Priya: 75000)
FD4001 (Amit: 500000)
SAV2001 (Rajesh: 150000)
By balance (Comparator):
CUR3001 (Priya: 75000)
SAV2001 (Rajesh: 150000)
FD4001 (Amit: 500000)
By balance desc, then name:
FD4001 (Amit: 500000)
SAV2001 (Rajesh: 150000)
CUR3001 (Priya: 75000)
What are the Java 8 Comparator factory methods?
Java 8 made Comparators composable and readable:
// Basic comparing
Comparator.comparing(Account::getBalance) // by balance
Comparator.comparing(Account::getHolder) // by holder name
Comparator.comparing(Account::getOpenDate) // by open date
// Primitive-specific (avoid autoboxing)
Comparator.comparingInt(Employee::getAge) // int field
Comparator.comparingLong(Trade::getTimestamp) // long field
Comparator.comparingDouble(Product::getPrice) // double field
// Reverse
Comparator.comparing(Account::getBalance).reversed() // descending
// Chaining (multi-level sort)
Comparator.comparing(Account::getHolder)
.thenComparing(Account::getBalance) // tie-breaker
.thenComparing(Account::getOpenDate) // second tie-breaker
// Null handling
Comparator.nullsFirst(Comparator.comparing(Account::getHolder)) // nulls first
Comparator.nullsLast(Comparator.naturalOrder()) // nulls last
// Case-insensitive
Comparator.comparing(Account::getHolder, String.CASE_INSENSITIVE_ORDER)
What is the TreeSet/TreeMap equality gotcha?
TreeSet and TreeMap use compareTo() (or the Comparator's compare()) for equality, NOT equals(). If compareTo() returns 0, the elements are treated as duplicates – even if equals() returns false.
package com.bank.demo;
import java.util.*;
public class CompareToEqualityGotcha {
public static void main(String[] args) {
// Case-insensitive comparator: "abc".compareTo("ABC") == 0
TreeSet<String> set = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
set.add("abc");
set.add("ABC"); // compareTo == 0 → treated as duplicate!
System.out.println("TreeSet size: " + set.size()); // 1, not 2!
System.out.println("TreeSet: " + set); // [abc]
// HashSet uses equals: "abc".equals("ABC") == false
HashSet<String> hashSet = new HashSet<>();
hashSet.add("abc");
hashSet.add("ABC");
System.out.println("HashSet size: " + hashSet.size()); // 2
}
}
TreeSet size: 1
TreeSet: [abc]
HashSet size: 2
When do you use Comparable vs Comparator?
| Situation | Use |
|---|---|
| Class has one obvious natural ordering | Comparable |
| Sorting by multiple fields at different times | Comparator |
| Cannot modify the class (third-party library) | Comparator |
| Need reverse, null-safe, or chained ordering | Comparator |
| TreeSet/TreeMap default ordering | Comparable |
| TreeSet/TreeMap alternate ordering | Comparator |
| Lambda/method reference for ad-hoc sort | Comparator |
What are common mistakes?
Mistake 1: compareTo inconsistent with equals
// WRONG: compareTo returns 0 for accounts with same balance
// but equals checks account ID
@Override public int compareTo(Account other) {
return this.balance.compareTo(other.balance);
}
@Override public boolean equals(Object o) {
return o instanceof Account a && a.accountId.equals(this.accountId);
}
// TreeSet with this: two accounts with same balance → one is dropped!
// Fix: add a tiebreaker to compareTo
@Override public int compareTo(Account other) {
int cmp = this.balance.compareTo(other.balance);
return cmp != 0 ? cmp : this.accountId.compareTo(other.accountId);
}
Mistake 2: Using subtraction for compareTo with integers
// WRONG: can overflow for large values
@Override public int compareTo(Account other) {
return this.priority - other.priority; // overflow if values are far apart!
}
// CORRECT: use Integer.compare
@Override public int compareTo(Account other) {
return Integer.compare(this.priority, other.priority);
}
What should a senior engineer know?
- Comparable contract. compareTo must be consistent: if a.compareTo(b) > 0 and b.compareTo(c) > 0, then a.compareTo(c) > 0 (transitivity). Violating this contract can cause TreeMap/TreeSet to behave incorrectly.
- compareTo and equals consistency. The Comparable documentation strongly recommends that compareTo be consistent with equals: a.compareTo(b) == 0 should imply a.equals(b). When this is violated, sorted collections (TreeSet) and hash-based collections (HashSet) behave differently for the same data.
- Comparator.thenComparing prevents duplicate dropping. When two elements compare equal on the primary field, the tiebreaker ensures they are not treated as duplicates in TreeSet.
What are common interview follow-up questions?
- "What is the return value of compareTo?" – Negative, zero, or positive.
- "Can a class implement both Comparable and use a Comparator?" – Yes. The Comparator overrides the natural order when passed to TreeSet/Collections.sort.
- "What happens if compareTo returns 0 in a TreeSet?" – The element is treated as a duplicate and not added.
- "How do you sort by multiple fields?" – Comparator.comparing().thenComparing().
Visual Overview
Comparable vs Comparator
Comparable (inside the class):
┌──────────────────────────────────┐
│ class Account implements │
│ Comparable<Account> { │
│ compareTo(Account other) { │
│ return id.compareTo(other.id)│
│ } │
│ } │
│ ONE ordering: by account ID │
└──────────────────────────────────┘
Comparator (outside the class):
┌──────────────────────────────────┐
│ Comparator.comparing(::balance) │ → by balance
│ Comparator.comparing(::holder) │ → by holder
│ Comparator.comparing(::date) │ → by date
│ MULTIPLE orderings, no class mod │
└──────────────────────────────────┘
TreeSet equality:
compareTo(a, b) == 0 → treated as SAME element
(even if equals returns false!)
Key Takeaway
Comparable and Comparator are the two ways to define ordering in Java. Comparable is baked into the class: one natural ordering via compareTo. Comparator is external: unlimited orderings via compare, composable with Java 8 factory methods like comparing, thenComparing, reversed, and nullsFirst.
Use Comparable for the one default sort (accounts by ID, trades by trade number). Use Comparator for everything else: sorting by balance, by date, by holder name, or any combination. Java 8 Comparators compose beautifully: Comparator.comparing(Account::getBalance).reversed().thenComparing(Account::getHolder) reads like English.
The critical gotcha: TreeSet and TreeMap use compareTo for equality, not equals. If compareTo returns 0, the elements are treated as duplicates. In the National Bank of India's account leaderboard, Rajesh's comparator sorts by balance and uses account ID as a tiebreaker to prevent accounts with identical balances from being silently dropped.
Remember one sentence: Comparable is the class's built-in ordering; Comparator is an external, composable ordering strategy – and TreeSet treats compareTo-0 as equal, not equals.
---