Q19 of 40 · Core Java

What is the difference between Comparable and Comparator?

Core JavaMidcomparablecomparatorsortingjava-collectionsjava-fundamentals

Short answer

Short answer: Comparable defines a class's natural ordering via compareTo() — the class itself implements it and can only have one. Comparator is an external comparison strategy passed to sort operations — you can define many and swap them at runtime. Comparable is 'the default sort'; Comparator is 'a custom sort'.

Detail

Comparable<T> is implemented by the class being compared. Its single method compareTo(T other) returns a negative int (this < other), zero (equal), or positive int (this > other). Standard library types like String, Integer, LocalDate, and BigDecimal implement Comparable. This enables Collections.sort(list) and TreeMap with no arguments — they use the natural order.

A class has only one compareTo implementation — you can't have both "sort users by name" and "sort users by ID" built in. That's where Comparator comes in.

Comparator<T> is an external strategy object. You can create as many as you need and compose them. Java 8 added fluent factory methods: Comparator.comparing(), thenComparing(), reversed(), nullsFirst(), nullsLast(). These make complex multi-field sorts readable.

compareTo contract: must be consistent with equals — if a.compareTo(b) == 0 then a.equals(b) should be true. Violating this causes bizarre behaviour with TreeSet and TreeMap (they use compareTo not equals for membership).

In test automation: sort test cases for reporting (Comparator.comparing(TestCase::severity).thenComparing(TestCase::id)), produce stable orderings for snapshot assertions, or use TreeMap with a custom comparator to group results.

// EXAMPLE

ComparableVsComparator.java

import java.util.Comparator;
import java.util.List;

// Comparable — natural ordering built into the class
record Severity(String name, int level) implements Comparable<Severity> {
    @Override
    public int compareTo(Severity other) {
        return Integer.compare(this.level, other.level);
    }
}

Severity critical = new Severity("CRITICAL", 4);
Severity minor    = new Severity("MINOR",    1);
System.out.println(critical.compareTo(minor)); // positive — critical > minor

// Comparator — external, multiple strategies
record TestCase(String id, Severity severity, long durationMs) {}

// Sort by severity descending, then by duration ascending
Comparator<TestCase> reportOrder = Comparator
    .comparing(TestCase::severity).reversed()          // high severity first
    .thenComparingLong(TestCase::durationMs);          // shortest first within level

List<TestCase> cases = getTestCases();
cases.sort(reportOrder);          // sorts in-place
var sorted = cases.stream()
    .sorted(reportOrder)          // stream version, returns new sorted stream
    .toList();

// Null-safe comparator for optional fields
Comparator<TestCase> nullSafe = Comparator
    .comparing(TestCase::id, Comparator.nullsLast(Comparator.naturalOrder()));

// WHAT INTERVIEWERS LOOK FOR

The 'built-in vs external' framing, the compareTo contract (consistent with equals), Java 8 fluent Comparator factory methods, and at least one composed comparator. Test automation use case (stable sort for assertions) is a strong applied angle.

// COMMON PITFALL

Using subtraction for numeric compareTo implementations: `return this.level - other.level`. This causes integer overflow for values near Integer.MIN/MAX_VALUE. Always use Integer.compare() or Long.compare() instead.