Java Tutorial
🔍

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

1// File: Order.java 2 3import java.time.LocalDateTime; 4import java.util.Objects; 5 6// Natural ordering: chronological — oldest order first 7// This is the universal sort order for Order: always by creation time 8public class Order implements Comparable<Order> { 9 10 private final String orderId; 11 private final String customerId; 12 private final double totalAmount; 13 private final LocalDateTime createdAt; 14 private String status; 15 16 public Order(String orderId, String customerId, 17 double totalAmount, LocalDateTime createdAt) { 18 this.orderId = orderId; 19 this.customerId = customerId; 20 this.totalAmount = totalAmount; 21 this.createdAt = createdAt; 22 this.status = "PENDING"; 23 } 24 25 public String getOrderId() { return orderId; } 26 public String getCustomerId() { return customerId; } 27 public double getTotalAmount() { return totalAmount; } 28 public LocalDateTime getCreatedAt() { return createdAt; } 29 public String getStatus() { return status; } 30 public void setStatus(String status) { this.status = status; } 31 32 // Natural ordering: chronological — oldest created first 33 // LocalDateTime.compareTo() handles all the date-time comparison 34 @Override 35 public int compareTo(Order other) { 36 // Primary: creation time (chronological) 37 int timeOrder = this.createdAt.compareTo(other.createdAt); 38 if (timeOrder != 0) return timeOrder; 39 // Secondary: orderId as stable tiebreaker for same-second orders 40 return this.orderId.compareTo(other.orderId); 41 } 42 43 @Override 44 public boolean equals(Object obj) { 45 if (this == obj) return true; 46 if (!(obj instanceof Order)) return false; 47 return Objects.equals(this.orderId, ((Order) obj).orderId); 48 } 49 50 @Override 51 public int hashCode() { return Objects.hash(orderId); } 52 53 @Override 54 public String toString() { 55 return String.format("[%s] %-10s Rs.%7.2f %s %s", 56 orderId, customerId, totalAmount, createdAt.toLocalTime(), status); 57 } 58}
1// File: ComparableDemo.java 2 3import java.time.LocalDateTime; 4import java.util.ArrayList; 5import java.util.Collections; 6import java.util.List; 7import java.util.TreeSet; 8 9public class ComparableDemo { 10 11 public static void main(String[] args) { 12 13 List<Order> orders = new ArrayList<>(); 14 LocalDateTime base = LocalDateTime.of(2024, 6, 10, 9, 0); 15 16 orders.add(new Order("ORD-004", "C-Rohan", 1299.0, base.plusMinutes(45))); 17 orders.add(new Order("ORD-001", "C-Priya", 549.0, base.plusMinutes(5))); 18 orders.add(new Order("ORD-003", "C-Karan", 2199.0, base.plusMinutes(30))); 19 orders.add(new Order("ORD-002", "C-Ananya", 899.0, base.plusMinutes(12))); 20 orders.add(new Order("ORD-005", "C-Divya", 349.0, base.plusMinutes(60))); 21 22 System.out.println("=== Before sort ==="); 23 orders.forEach(System.out::println); 24 25 // Uses compareTo() — no Comparator needed, natural order is chronological 26 Collections.sort(orders); 27 28 System.out.println("\n=== After Collections.sort() — natural chronological order ==="); 29 orders.forEach(System.out::println); 30 31 System.out.println(); 32 33 // TreeSet auto-sorts using compareTo() — no Comparator constructor argument 34 System.out.println("=== TreeSet — chronological insertion ==="); 35 TreeSet<Order> orderQueue = new TreeSet<>(); 36 orderQueue.add(orders.get(2)); // ORD-003 37 orderQueue.add(orders.get(0)); // ORD-001 38 orderQueue.add(orders.get(4)); // ORD-005 39 orderQueue.forEach(System.out::println); 40 41 System.out.println("\nOldest order : " + Collections.min(orders)); 42 System.out.println("Newest order : " + Collections.max(orders)); 43 } 44}
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

1// File: OrderComparators.java 2 3import java.util.Comparator; 4 5// All Comparator strategies for Order live here — centralised, named, reusable 6public final class OrderComparators { 7 8 private OrderComparators() {} 9 10 // Natural order is chronological (from Comparable) — no Comparator needed 11 12 // Alternative 1: highest amount first (for revenue dashboard) 13 public static final Comparator<Order> BY_AMOUNT_DESC = 14 Comparator.comparingDouble(Order::getTotalAmount).reversed(); 15 16 // Alternative 2: by customer ID then amount descending (customer ledger) 17 public static final Comparator<Order> BY_CUSTOMER_THEN_AMOUNT_DESC = 18 Comparator.comparing(Order::getCustomerId) 19 .thenComparingDouble(Order::getTotalAmount).reversed() 20 .thenComparing(Order::getOrderId); // stable tiebreaker 21 22 // Alternative 3: by status, then chronological within status (ops dashboard) 23 public static final Comparator<Order> BY_STATUS_THEN_TIME = 24 Comparator.comparing(Order::getStatus) 25 .thenComparing(Order::getCreatedAt) 26 .thenComparing(Order::getOrderId); 27 28 // Alternative 4: amount ascending, for low-value order prioritisation 29 public static final Comparator<Order> BY_AMOUNT_ASC = 30 Comparator.comparingDouble(Order::getTotalAmount) 31 .thenComparing(Order::getOrderId); 32}
1// File: ComparatorDemo.java 2 3import java.time.LocalDateTime; 4import java.util.ArrayList; 5import java.util.Comparator; 6import java.util.List; 7import java.util.TreeSet; 8 9public class ComparatorDemo { 10 11 public static void main(String[] args) { 12 13 List<Order> orders = new ArrayList<>(); 14 LocalDateTime base = LocalDateTime.of(2024, 6, 10, 9, 0); 15 orders.add(new Order("ORD-004", "C-Rohan", 1299.0, base.plusMinutes(45))); 16 orders.add(new Order("ORD-001", "C-Priya", 549.0, base.plusMinutes(5))); 17 orders.add(new Order("ORD-003", "C-Karan", 2199.0, base.plusMinutes(30))); 18 orders.add(new Order("ORD-002", "C-Ananya", 899.0, base.plusMinutes(12))); 19 orders.add(new Order("ORD-005", "C-Divya", 349.0, base.plusMinutes(60))); 20 orders.get(1).setStatus("DELIVERED"); 21 orders.get(3).setStatus("FAILED"); 22 23 // Sort using named Comparator — explicitly passed, overrides natural order 24 List<Order> byAmount = new ArrayList<>(orders); 25 byAmount.sort(OrderComparators.BY_AMOUNT_DESC); 26 System.out.println("=== BY_AMOUNT_DESC ==="); 27 byAmount.forEach(System.out::println); 28 29 System.out.println(); 30 31 List<Order> byStatus = new ArrayList<>(orders); 32 byStatus.sort(OrderComparators.BY_STATUS_THEN_TIME); 33 System.out.println("=== BY_STATUS_THEN_TIME ==="); 34 byStatus.forEach(System.out::println); 35 36 System.out.println(); 37 38 // TreeSet with custom Comparator — overrides natural order for this TreeSet 39 Comparator<Order> byAmountAndId = 40 Comparator.comparingDouble(Order::getTotalAmount) 41 .thenComparing(Order::getOrderId); 42 43 TreeSet<Order> amountTree = new TreeSet<>(byAmountAndId); 44 orders.forEach(amountTree::add); 45 System.out.println("=== TreeSet(byAmountAndId) — cheapest first ==="); 46 amountTree.forEach(System.out::println); 47 48 System.out.println(); 49 50 // Runtime sort selection — Comparator chosen by user preference 51 String userChoice = "AMOUNT_DESC"; // could come from request param 52 Comparator<Order> selected = switch (userChoice) { 53 case "AMOUNT_ASC" -> OrderComparators.BY_AMOUNT_ASC; 54 case "AMOUNT_DESC" -> OrderComparators.BY_AMOUNT_DESC; 55 case "STATUS" -> OrderComparators.BY_STATUS_THEN_TIME; 56 default -> Comparator.naturalOrder(); // falls back to compareTo 57 }; 58 List<Order> userSorted = new ArrayList<>(orders); 59 userSorted.sort(selected); 60 System.out.println("=== User-selected sort: " + userChoice + " ==="); 61 userSorted.forEach(System.out::println); 62 } 63}
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

1// File: UsingBothDemo.java 2 3import java.time.LocalDateTime; 4import java.util.ArrayList; 5import java.util.Comparator; 6import java.util.List; 7 8public class UsingBothDemo { 9 10 public static void main(String[] args) { 11 12 List<Order> orders = new ArrayList<>(); 13 LocalDateTime base = LocalDateTime.of(2024, 6, 10, 9, 0); 14 orders.add(new Order("ORD-004", "C-Rohan", 1299.0, base.plusMinutes(45))); 15 orders.add(new Order("ORD-001", "C-Priya", 549.0, base.plusMinutes(5))); 16 orders.add(new Order("ORD-003", "C-Karan", 2199.0, base.plusMinutes(30))); 17 orders.add(new Order("ORD-002", "C-Ananya", 899.0, base.plusMinutes(12))); 18 orders.add(new Order("ORD-005", "C-Divya", 349.0, base.plusMinutes(60))); 19 20 // Comparator.naturalOrder() delegates to compareTo() — uses Comparable 21 // Useful when you need to pass an explicit Comparator but want natural order 22 List<Order> natural = new ArrayList<>(orders); 23 natural.sort(Comparator.naturalOrder()); // same as Collections.sort(natural) 24 System.out.println("=== naturalOrder() — delegates to compareTo() ==="); 25 natural.forEach(System.out::println); 26 27 System.out.println(); 28 29 // Reverse of natural order — newest orders first 30 List<Order> reverseNatural = new ArrayList<>(orders); 31 reverseNatural.sort(Comparator.reverseOrder()); // reverse of compareTo() 32 System.out.println("=== reverseOrder() — reverse of compareTo() ==="); 33 reverseNatural.forEach(System.out::println); 34 35 System.out.println(); 36 37 // Natural order used as tiebreaker WITHIN a Comparator chain 38 // Sort: lowest amount first, then natural order (chronological) on ties 39 Comparator<Order> amountThenNatural = 40 Comparator.comparingDouble(Order::getTotalAmount) 41 .thenComparing(Comparator.naturalOrder()); // uses compareTo() for ties 42 List<Order> mixed = new ArrayList<>(orders); 43 mixed.sort(amountThenNatural); 44 System.out.println("=== Amount ASC, natural order (chronological) on ties ==="); 45 mixed.forEach(System.out::println); 46 } 47}
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

Real-World Example — CRED Credit Report Sorting

CRED's credit report module has a CreditEntry that represents a single credit account event. The natural ordering is chronological — always the default, used by the transaction timeline. But the same entries are sorted six different ways across different report sections: highest outstanding balance for risk analysis, oldest delinquency first for recovery prioritisation, lender alphabetically for account summary. These are all Comparator instances — the CreditEntry class itself only knows its natural order.

1// File: CreditEntry.java 2 3import java.time.LocalDate; 4import java.util.Objects; 5 6// Natural ordering: most recent entries first (reverse chronological) 7// This is the universally expected order for a credit report timeline 8public class CreditEntry implements Comparable<CreditEntry> { 9 10 private final String accountId; 11 private final String lender; 12 private final String accountType; // HOME_LOAN, AUTO_LOAN, CREDIT_CARD, PERSONAL_LOAN 13 private final double creditLimit; 14 private final double outstanding; 15 private final LocalDate openedDate; 16 private final LocalDate lastUpdated; 17 private final String paymentStatus; // REGULAR, OVERDUE, WRITTEN_OFF 18 19 public CreditEntry(String accountId, String lender, String accountType, 20 double creditLimit, double outstanding, 21 LocalDate openedDate, LocalDate lastUpdated, 22 String paymentStatus) { 23 this.accountId = accountId; 24 this.lender = lender; 25 this.accountType = accountType; 26 this.creditLimit = creditLimit; 27 this.outstanding = outstanding; 28 this.openedDate = openedDate; 29 this.lastUpdated = lastUpdated; 30 this.paymentStatus = paymentStatus; 31 } 32 33 public String getAccountId() { return accountId; } 34 public String getLender() { return lender; } 35 public String getAccountType() { return accountType; } 36 public double getCreditLimit() { return creditLimit; } 37 public double getOutstanding() { return outstanding; } 38 public LocalDate getOpenedDate() { return openedDate; } 39 public LocalDate getLastUpdated() { return lastUpdated; } 40 public String getPaymentStatus() { return paymentStatus;} 41 public double getUtilisation() { 42 return creditLimit > 0 ? (outstanding / creditLimit) * 100 : 0; 43 } 44 45 // Natural order: most recent lastUpdated first, then accountId as tiebreaker 46 @Override 47 public int compareTo(CreditEntry other) { 48 int dateOrder = other.lastUpdated.compareTo(this.lastUpdated); // reversed: newest first 49 if (dateOrder != 0) return dateOrder; 50 return this.accountId.compareTo(other.accountId); 51 } 52 53 @Override 54 public boolean equals(Object obj) { 55 if (this == obj) return true; 56 if (!(obj instanceof CreditEntry)) return false; 57 return Objects.equals(this.accountId, ((CreditEntry) obj).accountId); 58 } 59 60 @Override 61 public int hashCode() { return Objects.hash(accountId); } 62 63 @Override 64 public String toString() { 65 return String.format("[%s] %-12s %-14s Limit:%7.0f Owed:%7.0f Util:%.0f%% %s %s", 66 accountId, lender, accountType, 67 creditLimit, outstanding, getUtilisation(), 68 paymentStatus, lastUpdated); 69 } 70}
1// File: CreditReportService.java 2 3import java.time.LocalDate; 4import java.util.ArrayList; 5import java.util.Collections; 6import java.util.Comparator; 7import java.util.List; 8 9public class CreditReportService { 10 11 // All Comparator strategies for CreditEntry — separate from the class 12 // Natural order (compareTo) = most recent lastUpdated first — for timeline 13 private static final Comparator<CreditEntry> BY_OUTSTANDING_DESC = 14 Comparator.comparingDouble(CreditEntry::getOutstanding).reversed(); 15 16 private static final Comparator<CreditEntry> BY_UTILISATION_DESC = 17 Comparator.comparingDouble(CreditEntry::getUtilisation).reversed(); 18 19 private static final Comparator<CreditEntry> BY_LENDER_ALPHA = 20 Comparator.comparing(CreditEntry::getLender) 21 .thenComparing(CreditEntry::getAccountType); 22 23 private static final Comparator<CreditEntry> RISK_SORT = 24 Comparator.comparing(CreditEntry::getPaymentStatus, 25 // WRITTEN_OFF first, then OVERDUE, then REGULAR 26 Comparator.comparingInt(s -> switch(s) { 27 case "WRITTEN_OFF" -> 0; 28 case "OVERDUE" -> 1; 29 default -> 2; 30 })) 31 .thenComparing(BY_OUTSTANDING_DESC); 32 33 private static final Comparator<CreditEntry> BY_OPENED_ASC = 34 Comparator.comparing(CreditEntry::getOpenedDate) 35 .thenComparing(CreditEntry::getAccountId); 36 37 private void printSection(String heading, List<CreditEntry> entries) { 38 System.out.println("=".repeat(90)); 39 System.out.println(" " + heading); 40 System.out.println("=".repeat(90)); 41 entries.forEach(e -> System.out.println(" " + e)); 42 System.out.println("=".repeat(90)); 43 System.out.println(); 44 } 45 46 public void generateReport(List<CreditEntry> entries) { 47 48 // Section 1: Timeline — natural order (compareTo) — most recent first 49 List<CreditEntry> timeline = new ArrayList<>(entries); 50 Collections.sort(timeline); // uses compareTo() — no Comparator needed 51 printSection("TIMELINE (natural order — most recent first)", timeline); 52 53 // Section 2: Risk analysis — Comparator overrides natural order 54 List<CreditEntry> riskView = new ArrayList<>(entries); 55 riskView.sort(RISK_SORT); 56 printSection("RISK ANALYSIS (WRITTEN_OFF → OVERDUE → REGULAR, then outstanding DESC)", riskView); 57 58 // Section 3: Utilisation — high credit usage first 59 List<CreditEntry> utilisationView = new ArrayList<>(entries); 60 utilisationView.sort(BY_UTILISATION_DESC); 61 printSection("UTILISATION (highest utilisation % first)", utilisationView); 62 63 // Section 4: Account summary — alphabetical by lender 64 List<CreditEntry> lenderView = new ArrayList<>(entries); 65 lenderView.sort(BY_LENDER_ALPHA); 66 printSection("ACCOUNT SUMMARY (alphabetical by lender)", lenderView); 67 68 // Section 5: Credit history — oldest accounts first 69 List<CreditEntry> historyView = new ArrayList<>(entries); 70 historyView.sort(BY_OPENED_ASC); 71 printSection("CREDIT HISTORY (oldest account first)", historyView); 72 } 73 74 public static void main(String[] args) { 75 76 List<CreditEntry> entries = List.of( 77 new CreditEntry("AC001","HDFC Bank", "CREDIT_CARD", 150000, 82000, 78 LocalDate.of(2020,3,1), LocalDate.of(2024,5,28), "OVERDUE"), 79 new CreditEntry("AC002","SBI", "HOME_LOAN", 3500000,2100000, 80 LocalDate.of(2018,7,15), LocalDate.of(2024,6,1), "REGULAR"), 81 new CreditEntry("AC003","Bajaj Fin", "PERSONAL_LOAN", 200000, 45000, 82 LocalDate.of(2023,1,10), LocalDate.of(2024,5,31), "REGULAR"), 83 new CreditEntry("AC004","Axis Bank", "CREDIT_CARD", 100000, 98000, 84 LocalDate.of(2021,9,5), LocalDate.of(2024,4,15), "WRITTEN_OFF"), 85 new CreditEntry("AC005","ICICI Bank", "AUTO_LOAN", 800000, 320000, 86 LocalDate.of(2022,4,20), LocalDate.of(2024,6,2), "REGULAR") 87 ); 88 89 new CreditReportService().generateReport(entries); 90 } 91}
Output:
==========================================================================================
  TIMELINE (natural order — most recent first)
==========================================================================================
  [AC005] ICICI Bank   AUTO_LOAN      Limit: 800000  Owed: 320000  Util:40%  REGULAR  2024-06-02
  [AC002] SBI          HOME_LOAN      Limit:3500000  Owed:2100000  Util:60%  REGULAR  2024-06-01
  [AC003] Bajaj Fin    PERSONAL_LOAN  Limit: 200000  Owed:  45000  Util:22%  REGULAR  2024-05-31
  [AC001] HDFC Bank    CREDIT_CARD    Limit: 150000  Owed:  82000  Util:54%  OVERDUE  2024-05-28
  [AC004] Axis Bank    CREDIT_CARD    Limit: 100000  Owed:  98000  Util:98%  WRITTEN_OFF  2024-04-15
==========================================================================================

==========================================================================================
  RISK ANALYSIS (WRITTEN_OFF → OVERDUE → REGULAR, then outstanding DESC)
==========================================================================================
  [AC004] Axis Bank    CREDIT_CARD    Limit: 100000  Owed:  98000  Util:98%  WRITTEN_OFF  2024-04-15
  [AC001] HDFC Bank    CREDIT_CARD    Limit: 150000  Owed:  82000  Util:54%  OVERDUE  2024-05-28
  [AC002] SBI          HOME_LOAN      Limit:3500000  Owed:2100000  Util:60%  REGULAR  2024-06-01
  [AC005] ICICI Bank   AUTO_LOAN      Limit: 800000  Owed: 320000  Util:40%  REGULAR  2024-06-02
  [AC003] Bajaj Fin    PERSONAL_LOAN  Limit: 200000  Owed:  45000  Util:22%  REGULAR  2024-05-31
==========================================================================================

==========================================================================================
  UTILISATION (highest utilisation % first)
==========================================================================================
  [AC004] Axis Bank    CREDIT_CARD    Limit: 100000  Owed:  98000  Util:98%  WRITTEN_OFF  2024-04-15
  [AC002] SBI          HOME_LOAN      Limit:3500000  Owed:2100000  Util:60%  REGULAR  2024-06-01
  [AC001] HDFC Bank    CREDIT_CARD    Limit: 150000  Owed:  82000  Util:54%  OVERDUE  2024-05-28
  [AC005] ICICI Bank   AUTO_LOAN      Limit: 800000  Owed: 320000  Util:40%  REGULAR  2024-06-02
  [AC003] Bajaj Fin    PERSONAL_LOAN  Limit: 200000  Owed:  45000  Util:22%  REGULAR  2024-05-31
==========================================================================================

==========================================================================================
  ACCOUNT SUMMARY (alphabetical by lender)
==========================================================================================
  [AC004] Axis Bank    CREDIT_CARD    Limit: 100000  Owed:  98000  Util:98%  WRITTEN_OFF  2024-04-15
  [AC003] Bajaj Fin    PERSONAL_LOAN  Limit: 200000  Owed:  45000  Util:22%  REGULAR  2024-05-31
  [AC001] HDFC Bank    CREDIT_CARD    Limit: 150000  Owed:  82000  Util:54%  OVERDUE  2024-05-28
  [AC005] ICICI Bank   AUTO_LOAN      Limit: 800000  Owed: 320000  Util:40%  REGULAR  2024-06-02
  [AC002] SBI          HOME_LOAN      Limit:3500000  Owed:2100000  Util:60%  REGULAR  2024-06-01
==========================================================================================

==========================================================================================
  CREDIT HISTORY (oldest account first)
==========================================================================================
  [AC002] SBI          HOME_LOAN      Limit:3500000  Owed:2100000  Util:60%  REGULAR  2024-06-01
  [AC001] HDFC Bank    CREDIT_CARD    Limit: 150000  Owed:  82000  Util:54%  OVERDUE  2024-05-28
  [AC004] Axis Bank    CREDIT_CARD    Limit: 100000  Owed:  98000  Util:98%  WRITTEN_OFF  2024-04-15
  [AC005] ICICI Bank   AUTO_LOAN      Limit: 800000  Owed: 320000  Util:40%  REGULAR  2024-06-02
  [AC003] Bajaj Fin    PERSONAL_LOAN  Limit: 200000  Owed:  45000  Util:22%  REGULAR  2024-05-31
==========================================================================================

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()

1// WRONG — compareTo sorts by salary, equals uses employeeId 2// Same salary → TreeSet rejects second insert (compareTo = 0) 3// Different ID → HashMap keeps both (equals = false) 4// Mixed use produces contradictory results 5public int compareTo(Employee other) { 6 return Double.compare(this.salary, other.salary); // mismatched with equals 7} 8public boolean equals(Object o) { 9 return this.employeeId == ((Employee) o).employeeId; // uses ID 10} 11 12// CORRECT — compareTo uses salary as primary, ID as tiebreaker 13// compareTo returns 0 only when ID also matches → consistent with equals 14public int compareTo(Employee other) { 15 int salaryOrder = Double.compare(this.salary, other.salary); 16 if (salaryOrder != 0) return salaryOrder; 17 return Integer.compare(this.employeeId, other.employeeId); // tiebreaker = equals field 18}

Mistake 2 — Using a Comparator Without a Tiebreaker in TreeSet

1// WRONG — two employees in the same department → compare() = 0 → second dropped 2TreeSet<Employee> byDept = new TreeSet<>( 3 Comparator.comparing(Employee::getDepartment) 4); 5byDept.add(new Employee(1, "Priya", "Engineering", 92000)); 6byDept.add(new Employee(2, "Rohan", "Engineering", 78000)); // silently dropped! 7System.out.println(byDept.size()); // 1, not 2 8 9// CORRECT — chain to unique ID field as tiebreaker 10TreeSet<Employee> byDeptSafe = new TreeSet<>( 11 Comparator.comparing(Employee::getDepartment) 12 .thenComparingInt(Employee::getEmployeeId) 13);

Mistake 3 — Modifying the Class for Sort Flexibility When Comparator Fits Better

1// WRONG — changing natural order or adding multiple compareTo strategies 2// because some feature needs a different sort. Comparable is for ONE natural order. 3public int compareTo(Order other) { 4 if (sortByAmount) { // WRONG — state in compareTo! 5 return Double.compare(this.amount, other.amount); 6 } else { 7 return this.createdAt.compareTo(other.createdAt); 8 } 9} 10 11// CORRECT — natural order is one thing (chronological); all other orderings 12// are Comparators defined in a dedicated class 13public int compareTo(Order other) { 14 return this.createdAt.compareTo(other.createdAt); // one natural order 15} 16// Different sorts are separate Comparator constants

Mistake 4 — Forgetting That Comparator.reverseOrder() Requires Comparable

1// WRONG — Comparator.reverseOrder() on a class that does NOT implement Comparable 2// throws ClassCastException at runtime 3List<Employee> employees = ...; 4employees.sort(Comparator.reverseOrder()); // ClassCastException: Employee not Comparable 5 6// CORRECT for reversing a custom Comparator: 7Comparator<Employee> byId = Comparator.comparingInt(Employee::getEmployeeId); 8Comparator<Employee> byIdDesc = byId.reversed(); // reverses byId — no Comparable needed 9 10// Comparator.reverseOrder() only works for types that already implement Comparable: 11List<String> names = ...; 12names.sort(Comparator.reverseOrder()); // fine — String implements Comparable

Interview 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