Skip to content
Ameen Abozaid
PublishedBEG-032-L02

Comparators

  • Lesson 2 of 4
  • 9 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

In the last lesson you gave Player one natural order: by score. But the people running a game want more. The scoreboard shows the best first, the roster page shows names A to Z, and a support tool shows the newest players. One class, many orders.

A class can have only one compareTo, so that cannot work. Instead, Java lets you keep the rule outside the class, in a small object called a comparator. In the race picture from lesson 1, the natural ordering is the race's official ranking. A comparator is a different ruler: today the judges rank by shirt number, tomorrow by age, and the runners themselves never change.

Why this matters: nearly every API sort option ("sort by price", "sort by date") ends up as a comparator in the code. You also often cannot edit the class you need to sort, for example a class from a library, and a comparator is the only way.

This lesson builds on lesson 1 (Comparable, compareTo), on interfaces (BEG-024), and on collections and generics (BEG-030, BEG-031).

Preview notice: the compact comparator builders below use method references such as Player::getName. The course teaches method references and lambdas properly in BEG-035. For now read Player::getName as "the getName method of Player": "take a player, get its name". We first write comparators the long way, so every step uses only what you know.

Learning Objectives

By the end of this lesson you will be able to:

  • Explain the difference between Comparable and Comparator.
  • Write a class that implements Comparator<T> and its compare method.
  • Sort a list with list.sort(comparator) and create a sorted set or map with a comparator.
  • Build comparators with Comparator.comparing and Comparator.comparingInt.
  • Reverse an order with reversed() and use ready-made ones such as String.CASE_INSENSITIVE_ORDER.
  • Choose a comparator instead of a natural ordering when you need another order.

Written Explanation

The Comparator interface

java.util.Comparator<T> has one abstract method, compare:

java
public interface Comparator<T> {
    int compare(T first, T second);
    // plus many default and static helper methods
}

It follows the same sign rules as compareTo: negative means first goes before second, zero means a tie, positive means first goes after. The difference is that the comparison lives in a separate object that takes both elements as arguments. The class being sorted needs no changes.

Comparable<T>Comparator<T>
Where it livesInside the classIn a separate object
Methoda.compareTo(b)c.compare(a, b)
How many per classOne (the natural order)As many as you like
Needs access to edit the classYesNo

Writing one by hand

Using the interface skills from BEG-024, a comparator is just a class that implements the interface. For a Player with getName() and getScore():

java
import java.util.Comparator;

public class ByName implements Comparator<Player> {
    @Override
    public int compare(Player a, Player b) {
        return a.getName().compareTo(b.getName());
    }
}

Use it by passing an instance to sort:

java
players.sort(new ByName());

List.sort(Comparator) sorts the list in place. Passing null instead means "use the natural order". According to the Java documentation, this sort is stable: elements that compare as equal keep their original relative order.

Building comparators from a field

Writing a class per order is long. Comparator has static builders. Comparator.comparing takes a "key extractor", a method that pulls out the value to compare, and uses that key's natural order:

java
players.sort(Comparator.comparing(Player::getName));          // by name, A to Z
players.sort(Comparator.comparingInt(Player::getScore));      // by score, low to high

For a primitive int key use comparingInt (there are comparingLong and comparingDouble too): it avoids converting every value to an object, and it uses Integer.compare inside, so there is no subtraction trap. The key of comparing must itself be Comparable; String is.

Reversing

reversed() returns a comparator with the opposite order:

java
players.sort(Comparator.comparingInt(Player::getScore).reversed());   // high to low

Written by hand, "descending" means swapping the arguments: Integer.compare(b.getScore(), a.getScore()). Do not negate the result of compareTo, with a minus sign, to reverse it. For Integer.MIN_VALUE, negation overflows (it stays negative), so the swap or reversed() is the safe way.

Handy ready-made comparators

  • Comparator.naturalOrder() and Comparator.reverseOrder(): the natural order of a Comparable type, and the opposite.
  • String.CASE_INSENSITIVE_ORDER: compares strings ignoring case.
  • Comparator.nullsFirst(c) and Comparator.nullsLast(c): wrap another comparator so that null elements do not crash it, placing them first or last.

Using a comparator with sorted collections

TreeSet and TreeMap have constructors that accept a comparator:

java
Set<Player> roster = new TreeSet<>(Comparator.comparing(Player::getName));
Map<String, Integer> reversed = new TreeMap<>(Comparator.reverseOrder());

When a comparator is given, the collection uses it instead of the natural order, both for ordering and for deciding that two elements are duplicates. This is why the key you choose matters: a TreeSet ordered by name alone keeps only one player per name. (Oracle's Comparator documentation describes this relationship.)

Comparator or Comparable?

  • Use Comparable when the class has one obvious, permanent order (dates, versions, amounts).
  • Use a Comparator when you need several orders, when the order depends on the screen or request, or when you cannot change the class.

A class can have both: a natural order for default use and comparators for alternatives.

Examples

Example 1: sort players by name (simple)

java
List<Player> ps = new ArrayList<>(List.of(
        new Player("Mona", 90), new Player("Ali", 70), new Player("Zed", 85)));
ps.sort(new ByName());
System.out.println(ps);

Output (compiled and run with JDK 21, with toString printing name:score):

text
[Ali:70, Mona:90, Zed:85]

The Player class has no compareTo at all. The order came entirely from outside.

Example 2: three orders for one list (practical)

java
ps.sort(Comparator.comparingInt(Player::getScore));
System.out.println(ps);
ps.sort(Comparator.comparingInt(Player::getScore).reversed());
System.out.println(ps);
Set<Player> set = new TreeSet<>(Comparator.comparing(Player::getName));
set.addAll(ps);
System.out.println(set);

Real output:

text
[Ali:70, Zed:85, Mona:90]
[Mona:90, Zed:85, Ali:70]
[Ali:70, Mona:90, Zed:85]

The same three objects appear in three arrangements: score ascending, score descending, and by name in a sorted set.

Example 3: text, case, and nulls (realistic)

A user list typed by hand has mixed capitals, and sometimes a missing value:

java
List<String> words = new ArrayList<>(List.of("banana", "Apple", "cherry"));
Collections.sort(words);                      // natural order
words.sort(String.CASE_INSENSITIVE_ORDER);
words.sort(Comparator.reverseOrder());
words.sort(Comparator.comparingInt(String::length));
List<String> withNull = new ArrayList<>(Arrays.asList("b", null, "a"));
withNull.sort(Comparator.nullsFirst(Comparator.naturalOrder()));

After the steps above I printed: natural order [Apple, banana, cherry] (uppercase letters sort before lowercase in character-code order, so "Zebra" would come before "apple" in natural order), reverse [cherry, banana, Apple], by length [Apple, cherry, banana], and nulls first [null, a, b].

Look at the by-length result: cherry and banana both have length 6, and they stayed in the order they had before, because the sort is stable. Lesson 3 shows how to break such ties on purpose.

Backend Insight

A typical API accepts ?sort=price or ?sort=name. A common approach is a small lookup from the request word to a Comparator, so the controller picks the right "ruler" without if chains inside the sorting code. Because comparators are separate objects, the same list of results can be sorted for each request, and the domain class stays free of display decisions.

Comparators also help with classes you do not own. If a library gives you a list of objects that are not Comparable, Collections.sort(list) will not even compile, while list.sort(comparator) will. (Passing the list raw, without generics, compiles but fails at run time with a ClassCastException, which is why generics are valuable.)

A named comparator is also easy to test: build three small objects, compare them, and assert the sign of the result. That is much cheaper than finding an ordering bug later in a screen full of data.

When the data lives in a database, sorting there with ORDER BY is usually better, especially with paging. Java-side comparators are for data already in memory.

Common Mistakes

Mistake 1: Reversing by negating

  • Incorrect: return -a.compareTo(b);
  • Correct: return b.compareTo(a); or .reversed()

Mistake 2: Subtracting inside compare

  • Incorrect: return a.getScore() - b.getScore();
  • Correct: return Integer.compare(a.getScore(), b.getScore()); or Comparator.comparingInt(...).

Mistake 3: Sorting a list that is not modifiable

List.of(...) creates an unmodifiable list, and sort on it throws UnsupportedOperationException. Copy first: new ArrayList<>(List.of(...)).

Mistake 4: A comparator that makes a TreeSet lose elements

new TreeSet<>(Comparator.comparing(Player::getName)) silently drops a second player with the same name. The comparator defines sameness for the set. If names can repeat, include more in the comparison (next lesson).

Mistake 5: Forgetting that String order is case-sensitive

Use a case-insensitive comparator if capitalization must not matter. Natural String order puts "Zebra" before "apple".

Exercises

Level A: Foundation

A1. Which interface is used when you want an order that is not the class's natural order: Comparable or Comparator?

Show solution A1

A1. Comparator. A class has only one compareTo, but many comparators can exist outside it.

A2. What does Comparator.comparingInt(Task::getPriority).reversed() do to a list of tasks?

Show solution A2

A2. It sorts the tasks by priority, highest number first, because comparingInt builds the low-to-high order and reversed() flips it.

A3. Why would Collections.sort(listOfTasks) fail to compile if Task is not Comparable, while listOfTasks.sort(someComparator) is fine?

Show solution A3

A3. Collections.sort(list) requires the element type to be Comparable, and the compiler enforces it. list.sort(comparator) supplies the rule itself, so the element type does not have to be Comparable.

Level B: Application

B1. For a Task with title and int priority, write a class ByPriorityDesc implements Comparator<Task> that puts the highest priority first, without using subtraction.

Show solution B1

B1.

java
class ByPriorityDesc implements Comparator<Task> {
    @Override public int compare(Task a, Task b) {
        return Integer.compare(b.priority, a.priority);   // swapped arguments
    }
}

Run on write/3, Test/1, ship/2, it gave [write/3, ship/2, Test/1].

B2. Sort a list of tasks with the titles write, Test, ship (a) by title with the natural String order, and (b) ignoring case. Show both results.

Show solution B2

B2. (a) ts.sort(Comparator.comparing(Task::getTitle)) gives [Test, ship, write], because uppercase T sorts before lowercase letters. (b) Comparator.comparing(Task::getTitle, String.CASE_INSENSITIVE_ORDER) gives [ship, Test, write]. (Both results are from a run in JDK 21.)

B3. Create a TreeSet<String> that treats Bob and bob as the same element and add Bob, bob, Amy. What is its size, and which spelling remains?

Show solution B3

B3.

java
Set<String> s = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
s.add("Bob"); s.add("bob"); s.add("Amy");   // [Amy, Bob], size 2

The size is 2. "bob" compares as 0 against "Bob", so it is rejected, and the first spelling, Bob, stays.

Level C: Challenge

C1. Create a TreeMap<String, Integer> that lists keys from Z to A. Put a, c, b and print the map.

Show solution C1

C1.

java
Map<String, Integer> m = new TreeMap<>(Comparator.reverseOrder());
m.put("a", 1); m.put("c", 3); m.put("b", 2);
System.out.println(m);   // {c=3, b=2, a=1}

C2. A teammate wrote return -a.getScore().compareTo(b.getScore()); to sort descending. Explain what can go wrong and rewrite it two ways.

Show solution C2

C2. Negating can overflow: -Integer.MIN_VALUE is still Integer.MIN_VALUE, so the sign is wrong in that one case. (Also, getScore() returning an int has no compareTo method, so that line would not compile as written. The same idea with an Integer would.) Safe versions: return Integer.compare(b.getScore(), a.getScore()); or Comparator.comparingInt(Player::getScore).reversed().

Mini Challenge

Take the Movie class from lesson 1, or create a Movie with title and releaseYear fields and getters. Build three comparators: by title, by year ascending, and by year descending. Sort a list of five movies with each and print the three results. Then put the movies into a TreeSet that uses the title comparator and add two movies that share a title.

Check your work: you sorted without changing Movie, you used no subtraction, and you can say why the set kept only one of the two same-titled movies.

Summary & Next Lesson

Key takeaways

  • A Comparator<T> is a separate "ruler" with compare(a, b). You can have many for one class, and you do not need to edit the class.
  • Build them with Comparator.comparing(...) and comparingInt(...), flip them with reversed(), and sort with list.sort(comparator) or new TreeSet<>(comparator).
  • The sign rules match compareTo. Reverse by swapping or reversed(), never by negation. Never subtract.
  • A sorted set or map uses the comparator to decide sameness, so a weak comparator drops elements.
  • Player::getName style references are a preview; BEG-035 explains them.

Next lesson

A single key often leaves ties, such as two players with the same score. The next lesson, "Multi-key sorting", chains comparators with thenComparing to break ties.

Self-check

  1. What is the main difference between Comparable and Comparator?
  2. Which method must a Comparator implement?
  3. How do you sort in descending order without subtracting?
  4. What does list.sort(comparator) guarantee about elements that compare as equal?
  5. Why might new TreeSet<>(Comparator.comparing(Player::getName)) have fewer elements than expected?
Show answers
  1. Comparable is the class's single built-in order; a Comparator is a separate object, and a class can have many.
  2. int compare(T first, T second).
  3. Swap the arguments, or call .reversed() on an ascending comparator.
  4. Their original relative order is kept (the sort is stable).
  5. Players with the same name compare as 0, so the set treats them as the same element and keeps only one.

References

  • Oracle, Java SE 21 API, Comparator. Supports the compare contract, comparing, comparingInt, reversed, naturalOrder, reverseOrder, nullsFirst, and the statement that sorted sets and maps use the comparator rather than equals.
  • Oracle, Java SE 21 API, List. Supports sort(Comparator), null meaning natural ordering, and the stability guarantee.
  • Oracle, Java SE 21 API, TreeMap. Supports the constructor choice between natural ordering and a comparator.
  • All outputs come from code compiled and run on JDK 21. Not separately fetched: that Collections.sort fails at compile time for non-Comparable elements and that List.of lists reject sort (the first I confirmed with a compile; the second I confirmed by running it, but I did not fetch the documentation page that states it).

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.