Keyboard Shortcuts N Next post
P Previous post
S Save / unsave
R Read aloud
T Toggle theme
/ Focus search
Esc Close panels
🔥
Ready to read...
Interview Questions Java Java and Cryptography Interview Series Java Programming Java Tutorial Module 27 - Java Advanced and JVM Internals

Java Comparable vs Comparator with Sorting Examples

Reviewed & accurate
AI Summary

Introduction

The comparable vs comparator java question dates to Java 1.2 (December 1998) when the Collections Framework introduced both the Comparable and Comparator interfaces. Comparable was the natural-ordering interface, implemented by a class itself to define how its instances should be sorted by default; Comparator was the external-ordering interface, implemented by a separate class to define an alternative ordering. Both have been part of every JDK release since, and Comparator was substantially enhanced in Java 8 (March 2014) with default and static methods like comparing(), reversed(), and thenComparing() that turned it into a fluent builder.

For a Java learner, the comparable vs comparator java choice is one you will make every week. Comparable is the answer when there is one obvious natural order (a Person sorts by id, an Employee sorts by employee number, a LocalDate sorts chronologically). Comparator is the answer when there are multiple useful orders (an Employee sorts by name for the directory, by salary for the payroll report, by hire date for the tenure report). Java's sorting algorithm has been TimSort (originally from Python, by Tim Peters, 2002) since Java 7 (July 2011), which is stable and O(n log n). This post walks through both interfaces with a runnable demo.

What is Comparable vs Comparator in Java?

Comparable vs Comparator in Java is a comparison of two interfaces that both define an ordering relation but differ in where the ordering logic lives. Comparable (java.lang.Comparable) is implemented by the class being sorted and defines the natural ordering of its instances through a single compareTo(T o) method. Comparator (java.util.Comparator) is implemented by a separate class and defines an external ordering through a compare(T a, T b) method. The two interfaces are not interchangeable: Comparable is "the class knows how to order itself", Comparator is "an external piece of code knows how to order two instances".

The category breakdown is: Comparable for natural ordering (used by Collections.sort(list), list.sort(null), Arrays.sort(array), and the sorted streams without an explicit comparator), versus Comparator for custom ordering (used by list.sort(comparator), Stream.sorted(comparator), TreeMap and TreeSet with a custom comparator, and Collections.binarySearch with a comparator). Comparable's compareTo takes one argument (the other instance), Comparator's compare takes two arguments (the two instances to compare). Comparable is implemented once per class, Comparator can be implemented many times for the same class (one per ordering criterion).

How Comparable vs Comparator Works - Step by Step

Step 1: Implement Comparable. A class implements Comparable<T> with a single method int compareTo(T o) that returns a negative int if this is less than o, zero if equal, positive if greater. The contract requires antisymmetry (a.compareTo(b) and b.compareTo(a) have opposite signs), transitivity (a.compareTo(b) > 0 and b.compareTo(c) > 0 implies a.compareTo(c) > 0), and consistency with equals (a.compareTo(b) == 0 should imply a.equals(b), though this is recommended not required). Use Integer.compare(x, y) or Double.compare(x, y) rather than subtraction to avoid integer overflow on subtraction.

Step 2: Use Comparable for natural order. Collections.sort(list) and list.sort(null) both invoke the elements' compareTo method. Arrays.sort(array) does the same for arrays. TreeMap and TreeSet with no explicit comparator use compareTo for ordering. Stream.sorted() with no comparator uses compareTo. Collections.binarySearch(list, key) requires the list to be sorted by compareTo and uses compareTo for the search.

Step 3: Implement Comparator. A separate class (often a lambda) implements Comparator<T> with a method int compare(T a, T b) that returns a negative int if a is less than b, zero if equal, positive if greater. In Java 8+, the idiomatic way is Comparator.comparing(Employee::getName) which extracts a Comparable key from each element and delegates to that key's compareTo. comparingInt, comparingLong, and comparingDouble are primitive specialisations that avoid boxing.

Step 4: Use Comparator for custom order. list.sort(comparator), Stream.sorted(comparator), TreeMap(comparator), TreeSet(comparator), and Collections.binarySearch(list, key, comparator) all take a Comparator. Collections.sort(list, comparator) is the older equivalent. The same list can be sorted differently by passing different comparators: by name, by salary, by hire date, by name then by salary descending, and so on.

Step 5: Chain and reverse. Comparator is a fluent builder since Java 8. comparator.reversed() returns a comparator that sorts in the opposite order. comparator.thenComparing(other) chains a tie-breaker: if the first comparator returns zero (equal), the second is consulted. Comparator.nullsFirst(comparator) and nullsLast(comparator) handle nulls gracefully. Comparator.comparing(keyExtractor).reversed().thenComparing(keyExtractor2) is the standard pattern for a multi-key sort with mixed directions.

Java Program for Comparable vs Comparator

import java.util.*;

public class ComparableVsComparatorDemo {

    // Comparable: natural ordering, defined inside the class
    static class Employee implements Comparable<Employee> {
        private final int id;
        private final String name;
        private final double salary;

        public Employee(int id, String name, double salary) {
            this.id = id;
            this.name = name;
            this.salary = salary;
        }

        public int getId() { return id; }
        public String getName() { return name; }
        public double getSalary() { return salary; }

        @Override
        public int compareTo(Employee o) {
            // Natural order: by id ascending
            return Integer.compare(this.id, o.id);
        }

        @Override
        public String toString() {
            return id + ":" + name + "($" + salary + ")";
        }
    }

    // Comparator: external ordering strategies
    static final Comparator<Employee> BY_NAME =
        Comparator.comparing(Employee::getName);

    static final Comparator<Employee> BY_SALARY_DESC =
        Comparator.comparing(Employee::getSalary).reversed();

    static final Comparator<Employee> BY_NAME_THEN_SALARY =
        Comparator.comparing(Employee::getName)
                  .thenComparing(Comparator.comparing(Employee::getSalary).reversed());

    public static void main(String[] args) {
        List<Employee> emps = new ArrayList<>(Arrays.asList(
            new Employee(3, "Charlie", 75000),
            new Employee(1, "Alice", 95000),
            new Employee(2, "Bob", 85000),
            new Employee(4, "Alice", 70000)
        ));

        // Natural order via Comparable
        System.out.println("=== Natural order (Comparable by id) ===");
        Collections.sort(emps);
        for (Employee e : emps) System.out.println("  " + e);

        // Sort by name with Comparator
        System.out.println("
=== By name (Comparator) ===");
        emps.sort(BY_NAME);
        for (Employee e : emps) System.out.println("  " + e);

        // Sort by salary descending
        System.out.println("
=== By salary desc (Comparator.reversed) ===");
        emps.sort(BY_SALARY_DESC);
        for (Employee e : emps) System.out.println("  " + e);

        // Chained: name asc then salary desc
        System.out.println("
=== By name asc, then salary desc (chained) ===");
        emps.sort(BY_NAME_THEN_SALARY);
        for (Employee e : emps) System.out.println("  " + e);

        // Binary search needs Comparable consistency
        List<Employee> byId = new ArrayList<>(Arrays.asList(
            new Employee(1, "X", 0),
            new Employee(2, "Y", 0),
            new Employee(3, "Z", 0)
        ));
        int idx = Collections.binarySearch(byId, new Employee(2, "??", 0));
        System.out.println("
Binary search found id=2 at index " + idx);

        // nullsFirst / nullsLast
        List<Employee> withNull = new ArrayList<>(emps);
        withNull.add(null);
        withNull.add(null);
        System.out.println("
=== With nulls, nullsLast ===");
        withNull.sort(Comparator.nullsLast(BY_NAME));
        for (Employee e : withNull) System.out.println("  " + e);
    }
}

Code Explanation - Line by Line

The Employee class implements Comparable<Employee> with a compareTo method that orders by id ascending using Integer.compare(this.id, o.id). Using Integer.compare rather than subtraction (this.id - o.id) avoids integer overflow when the difference exceeds Integer.MAX_VALUE, which is rare for ids but a real bug for general Comparable implementations. The class also overrides toString to make the printed output readable.

The three static Comparator fields show the three common patterns. BY_NAME uses Comparator.comparing(Employee::getName) which extracts the name String from each Employee and delegates to String's compareTo (alphabetical). BY_SALARY_DESC chains .reversed() on top of a double-key comparator to sort salary from high to low. BY_NAME_THEN_SALARY chains .thenComparing(...) to sort by name first (ascending) and by salary second (descending) for ties on name.

The first sort uses Collections.sort(emps) which calls Employee.compareTo, so the list ends sorted by id ascending: 1:Alice, 2:Bob, 3:Charlie, 4:Alice. The second sort uses list.sort(BY_NAME), which delegates to String.compareTo, so the list ends alphabetical: Alice(95000), Alice(70000), Bob, Charlie. The order of the two Alices depends on the original order because TimSort is stable.

The third sort uses BY_SALARY_DESC, which sorts by salary descending: Alice 95000, Bob 85000, Charlie 75000, Alice 70000. The fourth sort uses the chained BY_NAME_THEN_SALARY, which sorts by name ascending and by salary descending within each name group: Alice 95000, Alice 70000, Bob 85000, Charlie 75000.

The binarySearch block demonstrates that Collections.binarySearch uses compareTo to find the index of an element in a Comparable-sorted list. We search for an Employee with id=2 in a list sorted by id, and we get back index 1. The element used for the search has name "??" and salary 0, but those fields are irrelevant because compareTo only looks at id.

The nullsLast block shows the null-handling helper. The list contains two nulls, and sorting it without nullsLast would throw NullPointerException. Comparator.nullsLast(BY_NAME) wraps BY_NAME so that nulls sort to the end (after all non-null elements, which are sorted by name).

Interview Questions and Answers

Q1. What is the difference between Comparable and Comparator in Java?

Comparable (java.lang.Comparable) is implemented by the class being sorted and defines its natural ordering through a single compareTo(T o) method. Comparator (java.util.Comparator) is implemented by a separate class and defines an external ordering through a compare(T a, T b) method. Comparable is for "the class knows its own order"; Comparator is for "an external piece of code knows how to order two instances of the class". A class can have only one Comparable implementation but can be sorted by many different Comparators.

Q2. When would you use Comparable and when would you use Comparator?

Use Comparable when there is a single obvious natural order for the class -- for example, a Person sorts by id, a LocalDate sorts chronologically, a Money sorts by amount. Use Comparator when there are multiple useful orders -- for example, an Employee sorts by name for the directory, by salary for the payroll report, and by hire date for the tenure report. Comparator is also the answer when you cannot modify the class (e.g., sorting instances of a third-party library class) or when the natural order is "wrong" for your use case.

Q3. What does the compareTo method return and what is its contract?

compareTo returns a negative int if this is less than the argument, zero if equal, and a positive int if greater. The contract requires antisymmetry (sign of a.compareTo(b) is opposite to sign of b.compareTo(a)), transitivity (a.compareTo(b) > 0 and b.compareTo(c) > 0 implies a.compareTo(c) > 0), and consistency with equals (a.compareTo(b) == 0 should imply a.equals(b), though this is recommended not required). Violating the contract can cause sort to throw IllegalArgumentException or produce a wrong order.

Q4. Why should you use Integer.compare(x, y) rather than x - y in compareTo?

Subtraction x - y can overflow when the difference exceeds Integer.MAX_VALUE -- for example, Integer.MAX_VALUE - (-1) wraps around to Integer.MIN_VALUE, which is negative, so the comparison returns the wrong sign. Integer.compare(x, y) uses a relational comparison internally (one of less-than, equals, greater-than) and is therefore safe for all int values. The same applies to Long.compare and to subtraction on any primitive type -- always use the compare helper.

Q5. What does Comparator.comparing do?

Comparator.comparing(keyExtractor) is a static factory that takes a function (typically a method reference like Employee::getName) and returns a Comparator that extracts the key from each element and delegates to that key's compareTo. The result is a Comparator that sorts by the extracted key in its natural order. Variants include comparingInt, comparingLong, and comparingDouble for primitive keys (which avoid boxing), and comparing(keyExtractor, keyComparator) for keys that have no natural order or for which you want a custom order.

Q6. How do you sort by multiple criteria with mixed directions?

Use Comparator.comparing(firstKey).thenComparing(secondKey) for a two-key sort, and chain more thenComparing calls for additional keys. To reverse the direction of a single key, append .reversed() to that key's comparator before chaining: Comparator.comparing(Employee::getName).thenComparing(Comparator.comparing(Employee::getSalary).reversed()) sorts by name ascending and by salary descending within each name group. The order of the chained comparators is the order of precedence: the first comparator decides, the second is consulted only on ties, and so on.

Q7. You have a List of a million records to sort by a string field. Should you use Comparable on the record class or a Comparator passed to list.sort?

It depends on whether the string field is the natural order of the record. If yes (e.g., a Country class whose natural order is by ISO country code), implement Comparable and call list.sort(null). If no (e.g., a Person sorted by name for one report, by age for another), use a Comparator: list.sort(Comparator.comparing(Person::getName)). Performance is identical because both paths call the same TimSort algorithm; the difference is purely architectural. For a million records, consider list.sort(Comparator.comparing(Person::getName, Comparator.nullsLast(Comparator.naturalOrder()))) to handle nulls gracefully without throwing.

Sample Input and Output

Input:  Run ComparableVsComparatorDemo on JDK 11.
Output:
=== Natural order (Comparable by id) ===
  1:Alice($95000.0)
  2:Bob($85000.0)
  3:Charlie($75000.0)
  4:Alice($70000.0)

=== By name (Comparator) ===
  1:Alice($95000.0)
  4:Alice($70000.0)
  2:Bob($85000.0)
  3:Charlie($75000.0)

=== By salary desc (Comparator.reversed) ===
  1:Alice($95000.0)
  2:Bob($85000.0)
  3:Charlie($75000.0)
  4:Alice($70000.0)

=== By name asc, then salary desc (chained) ===
  1:Alice($95000.0)
  4:Alice($70000.0)
  2:Bob($85000.0)
  3:Charlie($75000.0)

Binary search found id=2 at index 1

=== With nulls, nullsLast ===
  1:Alice($95000.0)
  4:Alice($70000.0)
  2:Bob($85000.0)
  3:Charlie($75000.0)
  null
  null

The output shows the four sort orders applied to the same list. The natural order (Comparable) sorts by id ascending. The by-name Comparator sorts alphabetically, and TimSort is stable so the two Alices keep their original relative order. The by-salary-desc Comparator sorts by salary from highest to lowest. The chained Comparator sorts by name ascending then by salary descending within each name group. Binary search finds the Employee with id=2 at index 1 in the id-sorted list. The nullsLast Comparator places the two nulls at the end of the by-name-sorted list.

Common Mistakes to Avoid

First, do not use x - y to compare ints or longs in compareTo; the subtraction can overflow and return the wrong sign. Use Integer.compare(x, y) or Long.compare(x, y) instead. Second, do not implement Comparable without overriding equals to be consistent -- the Comparable contract recommends that compareTo == 0 implies equals, otherwise TreeSet and TreeMap (which use compareTo for equality) will behave inconsistently with HashMap and HashSet (which use equals).

Third, do not assume that sort is stable without checking -- TimSort (Java 7+) is stable, but the older merge sort (Java 6 and earlier) was also stable, and the legacy Collections.sort contract guarantees stability. Fourth, do not throw NullPointerException from compareTo or compare when given null; use Comparator.nullsFirst or nullsLast to handle nulls explicitly, or document that nulls are not supported. Finally, do not mix Comparable and Comparator inconsistently -- if you pass a Comparator to a TreeMap, all subsequent operations use that Comparator, not compareTo, so the map's view of equality differs from the class's natural order.

Real-World Applications

The comparable vs comparator java choice is exercised everywhere in the JDK and in real applications. String implements Comparable (alphabetical order), Integer and friends implement Comparable (numerical order), LocalDate and the other java.time types implement Comparable (chronological order), and Enum implements Comparable by ordinal. TreeMap and TreeSet use Comparable by default and accept a Comparator for a custom order. PriorityQueue uses Comparable by default and accepts a Comparator for a custom heap order.

Beyond the JDK, Spring's @Order annotation and Ordered interface use Comparable under the hood to sort beans by precedence. JPA entities often implement Comparable for in-memory sorting when the database sort is not desired. Stream.collect(Collectors.groupingBy(..., TreeMap::new, ...)) uses Comparable on the keys. Android's RecyclerView adapters often accept a Comparator for sorting items in place. Anywhere you see a list, set, or map that needs an order, you are using Comparable or Comparator.

Conclusion

The comparable vs comparator java choice is straightforward: Comparable is the natural ordering defined inside the class, Comparator is the external ordering defined by a separate piece of code. Use Comparable for one obvious order, Comparator for multiple or for classes you cannot modify, and chain Comparator.comparing with thenComparing for multi-key sorts. Next, study the TimSort algorithm (Java 7+, originally from Python by Tim Peters), the primitive-specialised Comparators (comparingInt, comparingLong, comparingDouble), and the records pattern (Java 16+) which auto-generates Comparable for record components.

Test Your Knowledge
How did you find this?

Comments

Join the discussion! Sign in with your Google or Blogger account, or comment as Anonymous - no account needed. For quick questions, also reach me on Telegram @cytestch.

Comments