Comparable vs Comparator in Java: Where Each Ordering Lives

Comparable and Comparator both answer the same question — which of two objects comes first — but they answer it from different places. Comparable is implemented by the class itself, giving it exactly one fixed ordering. Comparator is a separate object built outside the class, so the same class can be sorted several different ways without changing it. This post runs two small Java programs, each compiled and run for real, one showing Comparable’s single built-in order and the other showing Comparator building several orders — including a composed one — over a class that has no ordering of its own.

Comparable: one fixed order living on the class

ComparableDemo defines an Employee that implements Comparable<Employee>, with compareTo ordering by salary ascending via Integer.compare. The JDK 21 Comparable Javadoc puts it plainly: “This interface imposes a total ordering on the objects of each class that implements it,” and describes compareTo’s result as “a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object” — the ordering it produces is what the doc calls the class’s natural ordering.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ComparableDemo {

    static class Employee implements Comparable<Employee> {
        final String name;
        final int salary;

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

        @Override
        public int compareTo(Employee other) {
            return Integer.compare(this.salary, other.salary);
        }

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

    public static void main(String[] args) {
        List<Employee> employees = new ArrayList<>(List.of(
                new Employee("Dara", 72000),
                new Employee("Mo", 51000),
                new Employee("Wren", 98000),
                new Employee("Ivo", 51000),
                new Employee("Ken", 64000)
        ));

        System.out.println("before: " + employees);
        Collections.sort(employees);
        System.out.println("after Collections.sort(employees): " + employees);
    }
}

Real javac+java output, JDK 21 (Temurin):

before: [Dara(72000), Mo(51000), Wren(98000), Ivo(51000), Ken(64000)]
after Collections.sort(employees): [Mo(51000), Ivo(51000), Ken(64000), Dara(72000), Wren(98000)]

Collections.sort(list) with no second argument sorts by that natural ordering, and its own Javadoc requires every element to implement Comparable. Look at Mo and Ivo in the output — both on a 51000 salary, and they come out in the order Mo then Ivo, the same relative order they had going in. That’s the sort’s own documented guarantee at work: “This sort is guaranteed to be stable: equal elements will not be reordered as a result of the sort.”

Comparator: as many orders as you want, defined outside the class

ComparatorDemo uses a plain Employee with no ordering of its own — it implements nothing. The orderings live in separate Comparator<Employee> objects instead: byName, built with Comparator.comparing(e -> e.name), and bySalaryDescThenName, built by chaining Comparator.comparingInt(...).reversed().thenComparing(...). The Comparator Javadoc describes the interface the same way Comparable’s does — “a comparison function, which imposes a total ordering on some collection of objects” — the difference is where that ordering lives.

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

public class ComparatorDemo {

    static class Employee {
        final String name;
        final int salary;

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

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

    public static void main(String[] args) {
        List<Employee> employees = new ArrayList<>(List.of(
                new Employee("Dara", 72000),
                new Employee("Mo", 51000),
                new Employee("Wren", 98000),
                new Employee("Ivo", 51000),
                new Employee("Ken", 64000)
        ));

        System.out.println("original: " + employees);

        Comparator<Employee> byName = Comparator.comparing(e -> e.name);
        List<Employee> byNameSorted = new ArrayList<>(employees);
        byNameSorted.sort(byName);
        System.out.println("sorted by name: " + byNameSorted);

        Comparator<Employee> bySalaryDescThenName =
                Comparator.<Employee>comparingInt(e -> e.salary).reversed()
                        .thenComparing(e -> e.name);
        List<Employee> bySalaryDescSorted = new ArrayList<>(employees);
        bySalaryDescSorted.sort(bySalaryDescThenName);
        System.out.println("sorted by salary desc, then name: " + bySalaryDescSorted);
    }
}

Real javac+java output, JDK 21 (Temurin):

original: [Dara(72000), Mo(51000), Wren(98000), Ivo(51000), Ken(64000)]
sorted by name: [Dara(72000), Ivo(51000), Ken(64000), Mo(51000), Wren(98000)]
sorted by salary desc, then name: [Wren(98000), Dara(72000), Ken(64000), Ivo(51000), Mo(51000)]

Sorting by byName alone produces Dara, Ivo, Ken, Mo, Wren — plain alphabetical order over the same Employee objects, without touching the class. The composed comparator is the more interesting run: reversed salary order puts the two 51000 earners, Ivo and Mo, next to each other, and thenComparing breaks that tie by name — Ivo before Mo in the output, matching what its Javadoc says it does: “If this Comparator considers two elements equal, … other is used to determine the order.”

Takeaway

Comparable bakes a single ordering into the class itself, which fits a type that has one obvious way to be compared. Comparator sits outside the class, so the same objects can be sorted by name, by salary, or by a composed multi-key order like salary-desc-then-name — all without adding or changing a compareTo method.