Comparators
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
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
ComparableandComparator. - Write a class that implements
Comparator<T>and itscomparemethod. - Sort a list with
list.sort(comparator)and create a sorted set or map with a comparator. - Build comparators with
Comparator.comparingandComparator.comparingInt. - Reverse an order with
reversed()and use ready-made ones such asString.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:
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 lives | Inside the class | In a separate object |
| Method | a.compareTo(b) | c.compare(a, b) |
| How many per class | One (the natural order) | As many as you like |
| Needs access to edit the class | Yes | No |
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():
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:
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:
players.sort(Comparator.comparing(Player::getName)); // by name, A to Z
players.sort(Comparator.comparingInt(Player::getScore)); // by score, low to highFor 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:
players.sort(Comparator.comparingInt(Player::getScore).reversed()); // high to lowWritten 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()andComparator.reverseOrder(): the natural order of aComparabletype, and the opposite.String.CASE_INSENSITIVE_ORDER: compares strings ignoring case.Comparator.nullsFirst(c)andComparator.nullsLast(c): wrap another comparator so thatnullelements do not crash it, placing them first or last.
Using a comparator with sorted collections
TreeSet and TreeMap have constructors that accept a comparator:
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)
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):
[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)
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:
[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:
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());orComparator.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?
أظهر الحل 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?
أظهر الحل 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?
أظهر الحل 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.
أظهر الحل B1
B1.
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.
أظهر الحل 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?
أظهر الحل B3
B3.
Set<String> s = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
s.add("Bob"); s.add("bob"); s.add("Amy"); // [Amy, Bob], size 2The 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.
أظهر الحل C1
C1.
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.
أظهر الحل 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" withcompare(a, b). You can have many for one class, and you do not need to edit the class. - Build them with
Comparator.comparing(...)andcomparingInt(...), flip them withreversed(), and sort withlist.sort(comparator)ornew TreeSet<>(comparator). - The sign rules match
compareTo. Reverse by swapping orreversed(), never by negation. Never subtract. - A sorted set or map uses the comparator to decide sameness, so a weak comparator drops elements.
Player::getNamestyle 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
- What is the main difference between
ComparableandComparator? - Which method must a
Comparatorimplement? - How do you sort in descending order without subtracting?
- What does
list.sort(comparator)guarantee about elements that compare as equal? - Why might
new TreeSet<>(Comparator.comparing(Player::getName))have fewer elements than expected?
أظهر الإجابات
Comparableis the class's single built-in order; aComparatoris a separate object, and a class can have many.int compare(T first, T second).- Swap the arguments, or call
.reversed()on an ascending comparator. - Their original relative order is kept (the sort is stable).
- 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 thecomparecontract,comparing,comparingInt,reversed,naturalOrder,reverseOrder,nullsFirst, and the statement that sorted sets and maps use the comparator rather thanequals. - Oracle, Java SE 21 API,
List. Supportssort(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.sortfails at compile time for non-Comparable elements and thatList.oflists rejectsort(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).
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.