Skip to content
Java collections 5 min read

Comparable vs Comparator

Sorting numbers and strings is easy — Java already knows how to order them. But what about your own custom objects? That’s where Comparable and Comparator come in. Both interfaces let you define ordering rules, but they serve different purposes and live in different places in your design.

The Core Difference

FeatureComparableComparator
Packagejava.langjava.util
MethodcompareTo(T o)compare(T o1, T o2)
Defined inThe class being sortedA separate class / lambda
Sorts byOne natural orderAny number of custom orders
Modifies original class?YesNo
Used byCollections.sort(list), Arrays.sort()Collections.sort(list, cmp), Arrays.sort(arr, cmp)

Think of Comparable as the object saying “I know how to compare myself.” Comparator is an external judge that says “I’ll decide the order for you.”

Comparable — Natural Ordering

Implement Comparable<T> directly in your class when there is one obvious, logical ordering — like sorting employees by ID or products by price.

import java.util.*;

public class Employee implements Comparable<Employee> {
    int id;
    String name;

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

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

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

    public static void main(String[] args) {
        List<Employee> list = new ArrayList<>();
        list.add(new Employee(3, "Alice"));
        list.add(new Employee(1, "Bob"));
        list.add(new Employee(2, "Carol"));

        Collections.sort(list);   // uses compareTo
        System.out.println(list);
    }
}

Output:

[1:Bob, 2:Carol, 3:Alice]

The compareTo Contract

Your compareTo method must return:

  • negativethis comes before other
  • zero — they are considered equal
  • positivethis comes after other

Warning: Never return hardcoded 1, 0, -1 by subtracting integers (e.g. this.id - other.id). Integer subtraction can overflow for large or negative values. Use Integer.compare(), Double.compare(), or String.compareTo() instead.

Comparator — Custom Ordering

Comparator<T> lives outside your class. Use it when you need multiple sort orders, when you cannot modify the class (e.g. a library type), or when the ordering is context-dependent.

import java.util.*;

public class SortByName {
    public static void main(String[] args) {
        List<Employee> list = new ArrayList<>();
        list.add(new Employee(3, "Alice"));
        list.add(new Employee(1, "Bob"));
        list.add(new Employee(2, "Carol"));

        // Sort by name using a Comparator lambda
        list.sort(Comparator.comparing(e -> e.name));
        System.out.println(list);
    }
}

Output:

[3:Alice, 1:Bob, 2:Carol]

Tip: list.sort(comparator) (added in Java 8) is equivalent to Collections.sort(list, comparator) and is generally preferred today.

Classic Comparator with an Anonymous Class

Before Java 8, you wrote a full anonymous class. You may still see this in older codebases:

Comparator<Employee> byName = new Comparator<Employee>() {
    @Override
    public int compare(Employee a, Employee b) {
        return a.name.compareTo(b.name);
    }
};
Collections.sort(list, byName);

Since Java 8, Comparator is a functional interface, so a lambda expression is far cleaner.

Chaining Comparators — Multiple Sort Keys

One of Comparator’s superpowers is easy chaining with thenComparing:

import java.util.*;

public class ChainedSort {
    public static void main(String[] args) {
        List<Employee> list = new ArrayList<>();
        list.add(new Employee(2, "Carol"));
        list.add(new Employee(1, "Bob"));
        list.add(new Employee(2, "Alice"));   // same id as Carol

        // Primary: id ascending; Secondary: name ascending
        list.sort(Comparator.comparingInt((Employee e) -> e.id)
                             .thenComparing(e -> e.name));
        System.out.println(list);
    }
}

Output:

[1:Bob, 2:Alice, 2:Carol]

Reverse Order

// Descending by ID
list.sort(Comparator.comparingInt((Employee e) -> e.id).reversed());

Null-safe Sorting

// Nulls first, then sort by name
list.sort(Comparator.nullsFirst(Comparator.comparing(e -> e.name)));

Sorting with Arrays

Both interfaces work identically with arrays via Arrays.sort():

import java.util.Arrays;
import java.util.Comparator;

public class ArraySort {
    public static void main(String[] args) {
        String[] words = {"banana", "apple", "cherry"};

        // Natural order (String already implements Comparable)
        Arrays.sort(words);
        System.out.println(Arrays.toString(words));

        // Custom: sort by string length
        Arrays.sort(words, Comparator.comparingInt(String::length));
        System.out.println(Arrays.toString(words));
    }
}

Output:

[apple, banana, cherry]
[apple, banana, cherry]

Comparable and Comparator Together

You can implement Comparable for the default order and still pass a Comparator when you need something different — they do not conflict.

// Natural order: sort by ID (Comparable)
Collections.sort(list);

// Custom order: sort by name (Comparator) — original class unchanged
list.sort(Comparator.comparing(e -> e.name));

This pattern is common in the Collections Framework itself. TreeSet and TreeMap use natural ordering (via Comparable) unless you supply a Comparator at construction time.

When to Use Which

  • Use Comparable when there is one clear, universally agreed-upon natural order for your objects (e.g. dates ordered by time, employees ordered by ID).
  • Use Comparator when you need multiple sort strategies, when you cannot touch the class source code, or when sorting by different fields in different contexts (e.g. sort by salary in one report, by name in another).

Note: If your class will be used as a key in a TreeMap or stored in a TreeSet, it must implement Comparable (or you must provide a Comparator at construction time). Forgetting this causes a ClassCastException at runtime.

Under the Hood

Java’s Collections.sort() and Arrays.sort() both use TimSort (a hybrid of merge sort and insertion sort) since Java 7. TimSort is stable — equal elements stay in their original relative order — which makes chained comparisons work correctly.

When you call Collections.sort(list), the JVM casts each element to Comparable internally. If your class does not implement it, you get a ClassCastException, not a compile-time error (unless you use generics properly). This is why parameterizing your list (List<Employee>) and implementing Comparable<Employee> (not raw Comparable) matters: the compiler can catch type mismatches early.

Under the hood, the Comparator.comparing() static factory methods return lambda-backed Comparator instances. The JVM represents these as invokedynamic call sites, so there is no object allocation overhead for the lambda capture in the common case where no variables are captured — the JDK generates a static field-cached instance.

For primitive-heavy sorting, Comparator.comparingInt/comparingLong/comparingDouble avoid boxing overhead by accepting ToIntFunction, ToLongFunction, or ToDoubleFunction references, which are worth preferring over Comparator.comparing() when sorting large collections of objects with primitive fields.

  • Comparable — deep dive into implementing the Comparable interface
  • Comparator — deep dive into Comparator, lambdas, and method references with sorting
  • Sorting Collections — practical guide to sorting lists, maps, and arrays in Java
  • Collections Utility ClassCollections.sort(), reverse(), shuffle(), and more
  • TreeSet — a sorted Set that relies on natural ordering or a Comparator
  • TreeMap — a sorted Map that uses Comparable or Comparator for key ordering
  • Lambda Expressions — write concise Comparators with lambdas and method references
Last updated June 13, 2026
Was this helpful?