Comparable & Comparator

Reviewed & published by Brayan K

Java can sort numbers and strings out of the box — but how does it sort your objects? You teach it. Comparable gives a class one built-in order; Comparator gives you unlimited orders on the side. Together they power every sort, TreeMap, and priority queue in Java.

Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

Before You Start

You'll want interfaces (Comparable is one you implement), lists to sort, and a glance at lambdas — modern comparators read best as lambdas and method references.

What You'll Learn

The Big Idea — Built-in Order vs Order on Demand

💡 Analogy: Think of a deck of playing cards. Comparable is the order printed on the cards themselves — the natural rank, 2 through Ace. Everyone agrees on it because it's baked in. A Comparator is a house rule you bring to a specific game: "tonight, sort by suit colour, then by rank." The cards don't change — you just supply a different way to compare them whenever you sort.

So a type has at most one Comparable order (its compareTo), but you can write as many Comparators as you like. And crucially, a Comparator lets you sort classes you can't even edit — like library types.

1️⃣ Comparable — the Natural Order

A class implements Comparable<T> and overrides one method, compareTo. That defines the type's natural order — the order used by Collections.sort(list), list.sort(null), TreeSet, and TreeMap when you don't supply anything else.

The compareTo contract: return a negative number if this comes before other, zero if they tie, and a positive number if this comes after. Implement it with helpers like Integer.compare or Double.compare — never raw subtraction.

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

public class Main {
    // Comparable defines the ONE natural order for a type.
    static class Student implements Comparable<Student> {
        String name;
        double gpa;
        Student(String name, double gpa) { this.name = name; this.gpa = gpa; }

        // compareTo: negative if THIS < other, 0 if equal, positive if THIS > other.
        // Here the natural order is "by GPA, ascending".
        @Override
        public int compareTo(Student other) {
            return Double.compare(this.gpa, other.gpa);
        }

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

    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("Ada", 3.9));
        students.add(new Student("Ben", 3.2));
        students.add(new Student("Cleo", 3.7));

        Collections.sort(students);    // uses compareTo automatically
        System.out.println("By GPA ascending:");
        for (Student s : students) System.out.println("  " + s);
    }
}

2️⃣ Comparator — Order on Demand

A Comparator defines an order outside the class, so you can have many and switch between them per sort. The modern way to build one is the Comparator.comparing(...) family, which extracts a sort key from each object:

Pass the comparator to list.sort(comparator) and you're done — no change to the class itself.

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

public class Main {
    static class Student {
        String name; double gpa;
        Student(String name, double gpa) { this.name = name; this.gpa = gpa; }
        public String toString() { return name + " (" + gpa + ")"; }
    }

    public static void main(String[] args) {
        List<Student> students = new ArrayList<>(List.of(
            new Student("Ada", 3.9),
            new Student("Ben", 3.2),
            new Student("Cleo", 3.7)
        ));

        // A Comparator defines an order WITHOUT touching the class.
        // Sort by name (alphabetical) instead of the natural order.
        students.sort(Comparator.comparing(s -> s.name));
        System.out.println("By name:");
        for (Student s : students) System.out.println("  " + s);

        // Now by GPA descending — just change the comparator.
        students.sort(Comparator.comparingDouble((Student s) -> s.gpa).reversed());
        System.out.println("By GPA descending:");
        for (Student s : students) System.out.println("  " + s);
    }
}

3️⃣ Multi-Level Sorting with thenComparing

Real-world sorts have tie-breakers: sort by last name, then first name, then age. Chain comparators with thenComparing — it only takes effect when the previous keys are equal, so it cleanly resolves ties.

Comparator.comparing((Person p) -> p.last)
          .thenComparing(p -> p.first)
          .thenComparingInt(p -> p.age)

Read it top to bottom as "primary key, then secondary, then tertiary." Each level is consulted only when all the levels above it tie.

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

public class Main {
    static class Person {
        String last; String first; int age;
        Person(String last, String first, int age) {
            this.last = last; this.first = first; this.age = age;
        }
        public String toString() { return first + " " + last + ", " + age; }
    }

    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(List.of(
            new Person("Smith", "Ana", 30),
            new Person("Smith", "Abe", 25),
            new Person("Jones", "Zoe", 40)
        ));

        // Multi-level sort: by last name, then first name, then age.
        // thenComparing kicks in only when the previous keys tie.
        people.sort(
            Comparator.comparing((Person p) -> p.last)
                      .thenComparing(p -> p.first)
                      .thenComparingInt(p -> p.age)
        );

        for (Person p : people) System.out.println(p);
    }
}

4️⃣ Built-in Comparators & Null Safety

Java ships with handy ready-made comparators so you don't reinvent them:

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

public class Main {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("Cleo", "ada", "Ben"));

        // Case-INSENSITIVE order using a built-in comparator
        names.sort(String.CASE_INSENSITIVE_ORDER);
        System.out.println("Case-insensitive: " + names);

        // Reverse the natural order
        names.sort(Comparator.reverseOrder());
        System.out.println("Reverse natural : " + names);

        // Handle nulls safely: nulls sorted first, then natural order
        List<String> withNulls = new ArrayList<>();
        withNulls.add("beta"); withNulls.add(null); withNulls.add("alpha");
        withNulls.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
        System.out.println("Nulls first     : " + withNulls);
    }
}

🧩 Reorder Challenge

These lines sort a list of employees by department, then by salary descending within each department. Put them in order.

A)         .thenComparing(Comparator.comparingInt(Employee::salary).reversed())
B) emps.sort(
C) );
D)     Comparator.comparing(Employee::dept)
E) for (Employee e : emps) System.out.println(e);

Correct order: B, D, A, C, E

emps.sort(
    Comparator.comparing(Employee::dept)
        .thenComparing(Comparator.comparingInt(Employee::salary).reversed())
);
for (Employee e : emps) System.out.println(e);

Start the sort call, give the primary key (department), chain the tie-breaker (salary, reversed for descending), close the call, then print. Reversing only the inner comparator keeps department ascending.

🧠 Quick Recall

Predict the output before opening each answer.

List<Integer> nums = new ArrayList<>(List.of(5, 2, 8, 1));
nums.sort(Comparator.reverseOrder());
System.out.println(nums);

Answer: [8, 5, 2, 1]. reverseOrder() sorts by the natural order (ascending) and then flips it, giving largest-first.

List<String> n = new ArrayList<>(List.of("bob", "Alice", "carol"));
n.sort(Comparator.naturalOrder());
System.out.println(n);

Answer: [Alice, bob, carol]. The natural order for String is case-sensitive and compares by Unicode value, where uppercase letters (A=65) come before lowercase (b=98). That's why "Alice" sorts first. Use CASE_INSENSITIVE_ORDER for human-friendly sorting.

Integer a = 3, b = 9;
int r = a.compareTo(b);
System.out.println(r < 0 ? "negative" : r == 0 ? "zero" : "positive");

Answer: negative. 3.compareTo(9) means "is 3 before 9?" — yes, so it returns a negative number (this < other). That negative result is exactly what tells a sort to keep 3 ahead of 9.

🎯 Your Turn #1 — Natural order for Book

Implement compareTo so books sort shortest-first by page count.

import java.util.*;

public class Main {
    static class Book implements Comparable<Book> {
        String title; int pages;
        Book(String title, int pages) { this.title = title; this.pages = pages; }
        public String toString() { return title + " (" + pages + "p)"; }

        // YOUR TURN: make the natural order "shortest book first" (by pages).
        // Use Integer.compare. Expected sorted output:
        //   Short (50p)
        //   Medium (180p)
        //   Long (420p)
        @Override
        public int compareTo(Book other) {
            return 0;   // fix this
        }
    }

    public static void main(String[] args) {
        List<Book> books = new ArrayList<>(List.of(
            new Book("Long", 420), new Book("Short", 50), new Book("Medium", 180)));
        Collections.sort(books);
        for (Book b : books) System.out.println(b);
    }
}

🎯 Your Turn #2 — Price then name

Build a comparator that sorts by price ascending and breaks ties by name.

import java.util.*;

public class Main {
    record Product(String name, double price) {}

    public static void main(String[] args) {
        List<Product> items = new ArrayList<>(List.of(
            new Product("Pen", 1.50),
            new Product("Pad", 1.50),
            new Product("Bag", 9.99)
        ));

        // YOUR TURN: sort by price ASCENDING, breaking ties by name ASCENDING.
        // Use Comparator.comparingDouble(...).thenComparing(...).
        // Expected output:
        //   Pad (1.5)
        //   Pen (1.5)
        //   Bag (9.99)
        items.sort(/* your comparator */ (a, b) -> 0);

        for (Product p : items) System.out.println(p.name() + " (" + p.price() + ")");
    }
}

🧩 Mini-Challenge — Leaderboard

Sort players by score descending, break ties by name ascending, and print with rank numbers.

import java.util.*;

public class Main {
    // GOAL: sort a leaderboard. Primary: score DESCENDING (highest first).
    // Tie-breaker: name ASCENDING (alphabetical).
    //
    // Expected output:
    //   1. Bea  — 95
    //   2. Cal  — 95
    //   3. Ada  — 88

    record Player(String name, int score) {}

    public static void main(String[] args) {
        List<Player> board = new ArrayList<>(List.of(
            new Player("Ada", 88),
            new Player("Cal", 95),
            new Player("Bea", 95)
        ));

        // 1. build a Comparator: score descending, then name ascending
        // 2. sort the list
        // 3. print with rank numbers (1-based)

        // your code here
    }
}

Common Errors

Pro Tips

📋 Quick Reference

TaskSyntaxNotes
Natural orderimplements Comparable<T>Override compareTo
Safe compareInteger.compare(a, b)Never a - b
By a keyComparator.comparing(x::getKey)comparingInt/Double for primitives
Tie-breaker.thenComparing(...)Used only on ties
Descending.reversed()Flips everything before it
Null-safeComparator.nullsFirst(...)Or nullsLast
Sort a listlist.sort(comparator)Or Collections.sort

🎉 Lesson Complete!

You can now give a class its natural order with Comparable, build any number of ad-hoc orders with Comparator, chain tie-breakers with thenComparing, reverse and null-proof them — and you'll never write a buggy a - b comparator again.

Practice quiz

What is the core difference between Comparable and Comparator?

  • Comparable is external; Comparator is implemented by the class
  • They are identical
  • Comparable defines one natural order inside the class; Comparator is an external order you can have many of
  • Comparator can only sort numbers

Answer: Comparable defines one natural order inside the class; Comparator is an external order you can have many of. Comparable's compareTo is the single natural order; Comparators live outside the class so you can write as many as you like.

compareTo (or a comparator) returns a NEGATIVE number when:

  • this comes before (is less than) other
  • this is greater than other
  • this equals other
  • Always, on error

Answer: this comes before (is less than) other. Negative means this < other, zero means equal, positive means this > other.

Why should you NEVER write 'return a - b' in a comparator?

  • It is slower
  • It does not compile
  • It only works for doubles
  • Integer subtraction can overflow and silently reverse the order

Answer: Integer subtraction can overflow and silently reverse the order. For large/opposite-sign values a - b wraps around. Use Integer.compare(a, b), which handles the full range.

List<Integer> nums = List.of(5,2,8,1); after nums.sort(Comparator.reverseOrder()) the list is:

  • [1, 2, 5, 8]
  • [8, 5, 2, 1]
  • [5, 2, 8, 1]
  • [2, 5, 8, 1]

Answer: [8, 5, 2, 1]. reverseOrder() flips the natural ascending order, producing largest-first: [8, 5, 2, 1].

Sorting ["bob", "Alice", "carol"] with Comparator.naturalOrder() gives:

  • [Alice, bob, carol]
  • [alice, bob, carol]
  • [bob, Alice, carol]
  • [carol, bob, Alice]

Answer: [Alice, bob, carol]. String natural order is case-sensitive by Unicode value; uppercase 'A' (65) sorts before lowercase 'b' (98).

How do you add a tie-breaker key to a comparator?

  • .andThen(...)
  • .orElse(...)
  • .thenComparing(...)
  • .plus(...)

Answer: .thenComparing(...). thenComparing chains another key that is consulted only when the previous keys are equal.

Calling .reversed() on a chained comparator comparing(a).thenComparing(b).reversed() reverses:

  • Only key b
  • Both keys (everything before it)
  • Only key a
  • Nothing

Answer: Both keys (everything before it). reversed() flips the whole comparator built so far. To reverse only one key, reverse that key's comparator alone.

Which built-in comparator sorts strings alphabetically while ignoring case?

  • Comparator.naturalOrder()
  • Comparator.reverseOrder()
  • Comparator.nullsFirst()
  • String.CASE_INSENSITIVE_ORDER

Answer: String.CASE_INSENSITIVE_ORDER. String.CASE_INSENSITIVE_ORDER compares ignoring case, giving human-friendly ordering.

Sorting a list that contains a null element with the natural order will:

  • Put nulls last automatically
  • Throw a NullPointerException
  • Skip the nulls
  • Sort fine

Answer: Throw a NullPointerException. Natural-order sorting can't compare null. Wrap with Comparator.nullsFirst(...) or nullsLast(...) to handle nulls.

If compareTo returns 0 for objects that are NOT equals(), a TreeSet will:

  • Throw an exception
  • Keep both anyway
  • Treat them as duplicates and drop one
  • Sort them randomly

Answer: Treat them as duplicates and drop one. Sorted collections use compareTo for identity; a 0 result means 'same key', so the second object is treated as a duplicate.

Continue this course

Frequently asked questions

What is the difference between Comparable and Comparator?

Comparable is implemented BY the class itself and defines its single 'natural ordering' via compareTo — for example a Student sorted by GPA. Comparator is a SEPARATE object that defines an ordering from the outside, so you can have many of them (by name, by GPA, by date) without changing the class. Rule of thumb: use Comparable for the one obvious order a type should have; use Comparator for everything else, including sorting classes you can't edit.

What should compareTo return?

A negative int if this object is 'less than' the other, zero if they're equal in order, and a positive int if this is 'greater than' the other. You almost never compute this by hand — use the built-in helpers Integer.compare(a, b), Double.compare(a, b), or this.field.compareTo(other.field) for objects. Never subtract ints (a - b) because that can overflow for large values.

How do I sort by one field, then break ties with another?

Chain comparators with thenComparing. Comparator.comparing(Person::getLast).thenComparing(Person::getFirst) sorts by last name, and only when two last names are equal does it fall back to comparing first names. You can chain as many levels as you need, and add .reversed() to flip any of them.

How do I sort in descending order?

Call .reversed() on a comparator: Comparator.comparingInt(Player::getScore).reversed() sorts highest score first. For natural-order types you can use Comparator.reverseOrder(), or Collections.sort(list, Comparator.reverseOrder()). Be careful when combining reversed() with thenComparing — reversed() flips everything before it, so order your calls deliberately.

Why shouldn't I write 'return a - b' in a comparator?

Subtraction can overflow. If a is a large positive int and b is a large negative int, a - b wraps around to a negative number and your sort silently produces the wrong order. Integer.compare(a, b) handles the full range correctly and reads more clearly. The same applies to Long.compare and Double.compare — always prefer the compare helpers over arithmetic.

Related lessons