Comparator

Su Suriya Ravichandran Updated 14 Nov 2026
6 min read ·Lesson 86 of 133

A java.util.Comparator<T> is an object that defines an ordering for values of type T from outside the class. Where Comparable gives a class one built-in natural order, comparators let you sort the same objects in any number of ways (by name, by price, by date, descending, with tie-breakers) and sort classes you cannot modify. Since Java 8, the Comparator interface provides factory and combinator methods that build almost any ordering in a single readable expression.

The Interface

@FunctionalInterface
public interface Comparator<T> {
    int compare(T a, T b);
    // plus many default and static helper methods
}

compare(a, b) follows the same sign convention as compareTo: negative if a comes first, zero if they are equal in order, positive if a comes after b.

Because Comparator has a single abstract method, it is a functional interface, so it can be written as a lambda:

Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());

List<String> words = new ArrayList<>(List.of("pear", "Fig", "apple", "kiwi"));
words.sort(byLength);
System.out.println(words);   // [Fig, pear, kiwi, apple]

The sort is stable, so pear and kiwi, both length 4, keep their original relative order.

Where Comparators Are Used

APIExample
Sorting listslist.sort(cmp), Collections.sort(list, cmp)
Sorting arrays of objectsArrays.sort(array, cmp)
Sorted collectionsnew TreeSet<>(cmp), new TreeMap<>(cmp)
Priority queuesnew PriorityQueue<>(cmp)
Minimum / maximumCollections.min(coll, cmp), Collections.max(coll, cmp)
Streamsstream.sorted(cmp), stream.max(cmp), stream.min(cmp)
Binary searchCollections.binarySearch(list, key, cmp)

Building Comparators with Factory Methods

Writing compare by hand is rarely necessary. Static factory methods create a comparator from a key extractor, a function that pulls out the value to compare:

record Product(String name, double price, int stock) { }

Comparator<Product> byName = Comparator.comparing(Product::name);
Comparator<Product> byPrice = Comparator.comparingDouble(Product::price);
Comparator<Product> byStock = Comparator.comparingInt(Product::stock);
FactoryKey type
Comparator.comparing(keyExtractor)Any Comparable key (String, LocalDate, BigDecimal, ...)
Comparator.comparing(keyExtractor, keyComparator)Any key, compared with the given comparator
comparingInt, comparingLong, comparingDoublePrimitive keys, without boxing
Comparator.naturalOrder() / reverseOrder()Natural ordering of Comparable elements, or its reverse
Comparator.nullsFirst(cmp) / nullsLast(cmp)Wraps a comparator so null elements sort first or last

Product::name is a method reference meaning "call name() on each product." The comparator calls it on both objects and compares the results.

Combining Comparators

Default methods transform and chain comparators:

MethodEffect
reversed()Reverse the entire ordering so far
thenComparing(other)Tie-breaker: used only when the current comparator returns 0
thenComparing(keyExtractor)Tie-breaker by another key
thenComparing(keyExtractor, keyComparator)Tie-breaker by another key with its own ordering
thenComparingInt / Long / Double(...)Primitive tie-breakers
Comparator<Product> listing = Comparator
        .comparingInt(Product::stock).reversed()     // most stock first
        .thenComparing(Product::price)               // then cheapest
        .thenComparing(Product::name);               // then alphabetical

reversed() applies to everything before it

reversed() reverses the whole comparator built up to that point, not just the last key:

// Price descending, then name ascending:
Comparator.comparingDouble(Product::price).reversed()
          .thenComparing(Product::name);

// Name ascending, then price DESCENDING (reverse only the tie-breaker):
Comparator.comparing(Product::name)
          .thenComparing(Product::price, Comparator.reverseOrder());

// Careful: this reverses BOTH keys:
Comparator.comparing(Product::name)
          .thenComparing(Product::price)
          .reversed();

To reverse a single key in a chain, pass Comparator.reverseOrder() as that key's comparator.

Type inference with lambdas

When a chain starts with a lambda rather than a method reference, the compiler sometimes cannot infer the element type before reversed() or thenComparing is applied. Declaring the lambda parameter's type fixes it:

Comparator.comparingInt((String s) -> s.length()).reversed();   // compiles
// Comparator.comparingInt(s -> s.length()).reversed();          // s is inferred as Object: compile error

Method references such as String::length usually avoid the problem.

Common Orderings

List<String> names = new ArrayList<>(List.of("ravi", "Arjun", "kavya", "Divya"));

names.sort(Comparator.naturalOrder());               // [Arjun, Divya, kavya, ravi]  uppercase first
names.sort(String.CASE_INSENSITIVE_ORDER);           // [Arjun, Divya, kavya, ravi]
names.sort(Comparator.reverseOrder());               // [ravi, kavya, Divya, Arjun]
names.sort(Comparator.comparing(String::length)
        .thenComparing(String.CASE_INSENSITIVE_ORDER));   // [ravi, Arjun, Divya, kavya]

Sorting with nulls

Natural-order comparators throw NullPointerException when they meet null. Wrap them to decide where nulls go, either for elements or for keys:

List<String> cities = new ArrayList<>(Arrays.asList("Pune", null, "Agra"));
cities.sort(Comparator.nullsLast(Comparator.naturalOrder()));   // [Agra, Pune, null]

// null KEYS: employees without a manager listed last
Comparator.comparing(Employee::manager, Comparator.nullsLast(Comparator.naturalOrder()));

Sorting map entries

Map<String, Integer> votes = Map.of("java", 42, "python", 57, "go", 42);

List<Map.Entry<String, Integer>> ranking = new ArrayList<>(votes.entrySet());
ranking.sort(Map.Entry.<String, Integer>comparingByValue().reversed()
        .thenComparing(Map.Entry.comparingByKey()));
// [python=57, go=42, java=42]

The explicit type witness Map.Entry.<String, Integer>comparingByValue() supplies the generic types that inference cannot determine through .reversed().

Writing compare by Hand

For orderings that do not depend on a simple key, implement compare directly:

// Custom rank: CRITICAL, then HIGH, then everything else alphabetically
Comparator<String> severity = (a, b) -> {
    List<String> rank = List.of("CRITICAL", "HIGH");
    int ra = rank.indexOf(a), rb = rank.indexOf(b);
    ra = ra < 0 ? rank.size() : ra;
    rb = rb < 0 ? rank.size() : rb;
    int c = Integer.compare(ra, rb);
    return c != 0 ? c : a.compareTo(b);
};

The same contract as compareTo applies: antisymmetric, transitive, and consistent. Use Integer.compare and friends rather than subtraction to avoid overflow.

Comparators and Sorted Collections

A comparator given to TreeSet, TreeMap, or PriorityQueue also decides what counts as a duplicate: two elements comparing as 0 are treated as the same. A comparator that compares only one field can therefore make a TreeSet silently drop distinct objects. Add tie-breakers that make the ordering distinguish every non-equal pair:

new TreeSet<>(Comparator.comparingDouble(Product::price)
                        .thenComparing(Product::name));

Reusable Comparators as Constants

Comparators are stateless and immutable, so they are ideal constants. Defining them once gives orderings meaningful names:

public record Product(String name, double price, int stock) {
    public static final Comparator<Product> CHEAPEST_FIRST =
            Comparator.comparingDouble(Product::price).thenComparing(Product::name);
    public static final Comparator<Product> BEST_STOCKED =
            Comparator.comparingInt(Product::stock).reversed();
}

products.sort(Product.CHEAPEST_FIRST);

Practical Example: Sortable Staff Directory

An HR dashboard lets the user choose how to sort the staff list. Each option is a named comparator in a map.

import java.time.LocalDate;
import java.util.*;

public class Directory {
    record Employee(String name, String dept, double salary, LocalDate joined, String manager) { }

    static final Map<String, Comparator<Employee>> SORTS = Map.of(
        "name", Comparator.comparing(Employee::name, String.CASE_INSENSITIVE_ORDER),
        "salary", Comparator.comparingDouble(Employee::salary).reversed(),
        "seniority", Comparator.comparing(Employee::joined),
        "dept", Comparator.comparing(Employee::dept)
                          .thenComparing(Employee::salary, Comparator.reverseOrder()),
        "manager", Comparator.comparing(Employee::manager,
                          Comparator.nullsLast(Comparator.naturalOrder()))
                          .thenComparing(Employee::name)
    );

    public static void main(String[] args) {
        List<Employee> staff = new ArrayList<>(List.of(
            new Employee("meena", "Engineering", 145000, LocalDate.of(2019, 4, 1), null),
            new Employee("Ravi", "Engineering", 98000, LocalDate.of(2022, 7, 15), "meena"),
            new Employee("arjun", "Sales", 76000, LocalDate.of(2021, 1, 10), "Kavya"),
            new Employee("Kavya", "Sales", 120000, LocalDate.of(2018, 9, 3), null),
            new Employee("Divya", "Engineering", 112000, LocalDate.of(2020, 2, 20), "meena")
        ));

        for (String key : List.of("name", "salary", "dept", "manager")) {
            staff.sort(SORTS.get(key));
            StringJoiner line = new StringJoiner(", ", key + ": ", "");
            for (Employee e : staff) line.add(e.name());
            System.out.println(line);
        }

        Employee longest = Collections.min(staff, SORTS.get("seniority"));
        System.out.println("Longest serving: " + longest.name() + " since " + longest.joined());
    }
}

Output:

name: arjun, Divya, Kavya, meena, Ravi
salary: meena, Kavya, Divya, Ravi, arjun
dept: meena, Divya, Ravi, Kavya, arjun
manager: arjun, Divya, Ravi, Kavya, meena
Longest serving: Kavya since 2018-09-03

Each sort key is a comparator built from factory methods:

  • name compares case-insensitively, so arjun and meena sort among the capitalized names instead of after them.
  • salary reverses a primitive comparator for highest-first.
  • dept sorts departments ascending and, within each department, salary descending, by giving the tie-breaker its own reverseOrder().
  • manager uses nullsLast on the key, so employees without a manager (meena, Kavya) appear at the end instead of causing a NullPointerException. Its natural string order puts Kavya before meena because uppercase letters sort first.

Collections.min accepts a comparator too, finding the earliest joining date without sorting. The Employee record has no natural ordering at all; every ordering lives outside the class, which is exactly what comparators are for.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Comparator and get answers drawn from it.

Signed-in readers only.