Skip to content
Java collections 5 min read

Comparator

When you need to sort objects in more than one way — by name here, by salary there — Comparator is your go-to tool. Unlike Comparable, which bakes a single “natural” order into the class itself, Comparator lives outside the class and lets you define as many different orderings as you like without ever touching the original source.

What Is Comparator?

Comparator<T> is a functional interface in java.util. It defines one abstract method:

int compare(T o1, T o2);

The contract is simple:

  • Return a negative number if o1 should come before o2.
  • Return zero if they are considered equal.
  • Return a positive number if o1 should come after o2.

You pass a Comparator to Collections.sort(), List.sort(), Arrays.sort(), or sorted data structures like TreeSet and TreeMap.

A First Example — Sorting by Name

import java.util.*;

class Employee {
    String name;
    int salary;

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

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

public class ComparatorDemo {
    public static void main(String[] args) {
        List<Employee> employees = new ArrayList<>(List.of(
            new Employee("Zara",  90000),
            new Employee("Alice", 75000),
            new Employee("Bob",   85000)
        ));

        // Sort alphabetically by name
        employees.sort(Comparator.comparing(e -> e.name));

        System.out.println(employees);
    }
}

Output:

[Alice($75000), Bob($85000), Zara($90000)]

Comparator.comparing() (added in Java 8) accepts a key extractor — a function that pulls the field you want to compare — and builds the full Comparator for you.

Writing a Comparator Three Ways

1. Anonymous Class (pre-Java 8)

Comparator<Employee> bySalary = new Comparator<Employee>() {
    @Override
    public int compare(Employee a, Employee b) {
        return Integer.compare(a.salary, b.salary);
    }
};

Always use Integer.compare() (or Double.compare(), etc.) instead of plain subtraction — subtraction can overflow for extreme values.

2. Lambda Expression

Comparator<Employee> bySalary = (a, b) -> Integer.compare(a.salary, b.salary);

Because Comparator is a @FunctionalInterface, it works perfectly with lambda expressions.

3. Comparator.comparingInt() Factory

Comparator<Employee> bySalary = Comparator.comparingInt(e -> e.salary);

The primitive-specialised variants (comparingInt, comparingLong, comparingDouble) avoid boxing overhead.

Reversing Order

Comparator<Employee> byHighestSalary = Comparator
    .comparingInt((Employee e) -> e.salary)
    .reversed();

employees.sort(byHighestSalary);
System.out.println(employees);

Output:

[Zara($90000), Bob($85000), Alice($75000)]

reversed() wraps the comparator and flips the sign of every compare() call.

Chaining Multiple Sort Criteria with thenComparing()

Real-world sorting often needs a secondary (or tertiary) criterion to break ties. thenComparing() makes this elegant:

List<Employee> team = new ArrayList<>(List.of(
    new Employee("Alice", 75000),
    new Employee("Alice", 90000),   // same name, different salary
    new Employee("Bob",   75000)
));

Comparator<Employee> byNameThenSalary = Comparator
    .comparing((Employee e) -> e.name)
    .thenComparingInt(e -> e.salary);

team.sort(byNameThenSalary);
System.out.println(team);

Output:

[Alice($75000), Alice($90000), Bob($75000)]

The second comparator only fires when the first returns zero — exactly like a SQL ORDER BY name, salary.

Handling null Values

Sorting a list that might contain null elements crashes with a NullPointerException by default. Use Comparator.nullsFirst() or Comparator.nullsLast() to handle them gracefully:

List<String> names = Arrays.asList("Charlie", null, "Alice", null, "Bob");

names.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(names);

Output:

[null, null, Alice, Bob, Charlie]

Tip: Comparator.naturalOrder() returns a comparator that uses the element’s own compareTo() — it works for any class that implements Comparable.

Using Comparator with TreeSet and TreeMap

Sorted collections accept a Comparator in their constructors to override the default ordering:

TreeSet<Employee> byName = new TreeSet<>(Comparator.comparing(e -> e.name));
byName.add(new Employee("Zara",  90000));
byName.add(new Employee("Alice", 75000));
byName.add(new Employee("Bob",   85000));

byName.forEach(System.out::println);

Output:

Alice($75000)
Bob($85000)
Zara($90000)

Warning: When you provide a custom Comparator to a TreeSet, it replaces the natural ordering entirely. Two objects that your comparator considers equal (compare returns 0) are treated as duplicates and only one is stored — even if equals() says they are different.

Comparator vs Comparable at a Glance

FeatureComparableComparator
Packagejava.langjava.util
MethodcompareTo(T o)compare(T o1, T o2)
Implemented byThe class being sortedA separate class or lambda
Number of orderingsOne (natural order)Unlimited
Modifying the classRequiredNot required
Typical useStrings, numbers, datesCustom / multiple sort criteria

See Comparable vs Comparator for a deeper side-by-side analysis.

Under the Hood

Comparator has been a @FunctionalInterface since Java 8, which means the JVM can represent it as a single invokedynamic call site rather than allocating a named anonymous-class object every time. At runtime, the JIT can inline small lambdas directly into the sort loop, eliminating the virtual-dispatch overhead that older hand-written Comparator anonymous classes incurred.

Comparator.comparing() returns a Comparators.NaturalOrderComparator or an KeyExtractorComparator internally. When you chain .thenComparing(), it wraps the first comparator in a CompoundComparator that calls the first, checks for zero, and only then delegates to the second. The chain is built lazily as a linked structure, not eagerly evaluated, so building a long chain has O(n) composition cost but adds only a tiny constant overhead per element during the actual sort.

List.sort() uses a TimSort algorithm (O(n log n) worst-case) which is adaptive — nearly-sorted input converges in near O(n) time. The stability of TimSort means equal elements (those where compare() returns 0) keep their original relative order, which matters when you chain comparators.

Note: For primitive arrays (int[], long[], etc.) Arrays.sort() uses a dual-pivot quicksort, not TimSort, and does not accept a Comparator. To use custom comparators you must work with boxed arrays or List.

Practical Pattern — Reusable Comparators as Constants

Centralise your comparators as public static final fields or factory methods so the rest of the codebase stays consistent:

public class Employee {
    public static final Comparator<Employee> BY_NAME   =
        Comparator.comparing(e -> e.name);
    public static final Comparator<Employee> BY_SALARY =
        Comparator.comparingInt(e -> e.salary);
    public static final Comparator<Employee> BY_SALARY_DESC =
        BY_SALARY.reversed();

    String name;
    int salary;

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

// Elsewhere:
employees.sort(Employee.BY_SALARY_DESC);

This pairs well with Enums if the set of orderings is fixed and known at compile time.

  • Comparable — understand natural ordering before adding external comparators
  • Comparable vs Comparator — pick the right tool for each sorting job
  • Sorting Collections — how Collections.sort() and List.sort() use comparators under the hood
  • TreeSet — a SortedSet that relies on a comparator (or natural order) to stay ordered
  • TreeMap — a SortedMap where the key ordering is driven by a comparator
  • Lambda Expressions — write concise, readable comparators without anonymous classes
Last updated June 13, 2026
Was this helpful?