Java comparators define ordering separately from the objects being sorted. They can sort by a field, reverse one rule and add tie-breakers. This lesson shows how to make an order predictable and why a comparator's return value is a sign, not a requirement to return exactly minus one or one.
Review the related foundation lesson when you need a prerequisite. The complete programs below use Java 17-compatible syntax.
Natural and external ordering
Comparable defines a natural ordering on the type itself. Comparator supplies an external ordering without changing the element class.
Comparable puts a natural order on a type through compareTo. Comparator supplies an external ordering, allowing the same data to be sorted in several ways. Its compare method returns a negative number, zero or a positive number to indicate the relation. The magnitude is not important. Use Integer.compare instead of subtracting int fields, because subtraction can overflow and reverse the intended sign.
Sorting and key-based tie-breakers
List.sort sorts a mutable list in place. sorted in a stream produces a sorted stream instead of changing the source list.
Comparator.comparing extracts a comparable key, while comparingInt and related primitive forms avoid unnecessary boxing for numeric keys. thenComparing adds a rule used only when the earlier comparison returns zero. This is how a sort by name length can become deterministic for different names with equal lengths: the tie-breaker compares their actual text.
Key-based ordering and reversing components
Comparator.comparing and comparingInt build readable key-based comparisons. thenComparing adds a tie-breaker, and reversed changes an ordering direction.
reversed reverses the comparator it is called on. Reversing the whole combined comparator changes every ordering rule, while reversing only a first component can retain ascending tie-breakers. Write the composition carefully and test tied values, not just clearly different values. nullsFirst and nullsLast handle null according to an explicit policy instead of letting a key extraction unexpectedly throw.
The comparator contract
A comparator must satisfy its ordering contract. Subtracting arbitrary integer keys can overflow, so use Integer.compare or comparator helpers instead.
List.sort changes the list's order in place and is stable for equal comparisons: elements that compare zero retain their prior relative order. An unmodifiable list cannot be sorted directly. A TreeSet or TreeMap uses comparator equality to decide uniqueness, so an ordering that compares only one nonunique field can collapse distinct objects in those structures even when it is acceptable for a list sort.
Running the examples
Save name.java, compile it with javac --release 17 name.java, and run java name. Use each program's own public class name and filename.
How NameComparatorSort produces its output
The names start as Zia, Leonie and Ava. The comparator orders by length first, making Zia and Ava tie at three characters while Leonie has six. The natural-order tie-breaker puts Ava before Zia. The sorted list therefore displays Ava, Zia, Leonie. Because sort mutates names, later reads from that list use the new sequence.
How ComparatorTies produces its output
Al and Bo have the same length, so the first comparison cannot order them. thenComparing uses their natural String order and puts Al first. Cara has length four and follows the two shorter names. Sorting mutates the ArrayList; the comparator supplies comparison logic rather than constructing another collection.
How DescendingLengths produces its output
reversed is applied to the length comparator before the natural-order tie-breaker is attached. Therefore longer names come first, but names with equal length remain in ascending text order. Cara leads, followed by Al and Bo. Reversing the entire finished comparator would also reverse that tie-breaker and change the order of those two names.
A comparator must describe a consistent order
If a comparison says A precedes B and B precedes C, it must also place A before C. A comparator based on changing random values or mutable external state can violate that requirement while sorting. Return zero when values are equal under the chosen ordering, and use a stable tie-breaker when different values need a deterministic sequence. For practice, sort names by descending length with ascending alphabetical ties, then test three equal-length names. Reverse only the primary comparator and compare that result with reversing the entire combined comparator. The exercise exposes exactly which relation each reversed call changes.
Handle null values at the correct level
Comparator.nullsLast(Comparator.naturalOrder()) handles null elements themselves, placing them after non-null comparable values. Sorting objects by a nullable field is a different case: the extracted key needs its own null-aware comparator. A null object can still fail before extraction unless the entire object comparator also handles null. Distinguish those two boundaries instead of wrapping one comparator and assuming every null is covered. For instance, a non-null member with a null surname requires a policy for surname ordering, while a null member entry requires a policy for whether the entry is accepted at all. Test both cases separately when external data permits them.
Java Summary
- Comparable defines a natural ordering on the type itself. Comparator supplies an external ordering without changing the element class.
- List.sort sorts a mutable list in place. sorted in a stream produces a sorted stream instead of changing the source list.
- Comparator.comparing and comparingInt build readable key-based comparisons. thenComparing adds a tie-breaker, and reversed changes an ordering direction.
- A comparator must satisfy its ordering contract. Subtracting arbitrary integer keys can overflow, so use Integer.compare or comparator helpers instead.
- Continue with Java exercises, Java quizzes and the Java study roadmap.
Java Examples
Each example shows the code, the output it produces and what each part does.
Java Collections Sorting and Comparators in a complete program
public class NameComparatorSort {
public static void main(String[] args) {
java.util.List<String> names = new java.util.ArrayList<>(java.util.List.of("Zia", "Leonie", "Ava"));
names.sort(java.util.Comparator.comparingInt(String::length).thenComparing(java.util.Comparator.naturalOrder()));
System.out.println(names);
}
}[Ava, Zia, Leonie]Line-by-line explanation
- Line 5Evaluates
namesand prints[Ava, Zia, Leonie].
A tie-breaker resolves equal lengths
public class ComparatorTies {
public static void main(String[] args) {
java.util.List<String> names = new java.util.ArrayList<>(java.util.List.of("Bo", "Al", "Cara"));
names.sort(java.util.Comparator.comparingInt(String::length).thenComparing(java.util.Comparator.naturalOrder()));
System.out.println(names);
}
}[Al, Bo, Cara]Line-by-line explanation
- Line 5Evaluates
namesand prints[Al, Bo, Cara].
Reverse only the primary rule
public class DescendingLengths {
public static void main(String[] args) {
java.util.List<String> names = new java.util.ArrayList<>(java.util.List.of("Bo", "Al", "Cara"));
java.util.Comparator<String> order = java.util.Comparator.comparingInt(String::length).reversed().thenComparing(java.util.Comparator.naturalOrder());
names.sort(order);
System.out.println(names);
}
}[Cara, Al, Bo]Line-by-line explanation
- Line 6Evaluates
namesand prints[Cara, Al, Bo].
Java FAQ
Must compare return exactly minus one or one?
No. Only the sign matters, and zero denotes comparison equality. Implement a consistent ordering that respects symmetry and transitivity. Returning arbitrary inconsistent results can make sorting fail or produce unusable ordered collections.
Why avoid subtracting integer sort keys?
The subtraction can overflow, causing its sign to describe the opposite relation. Integer.compare handles the full int range correctly. Use comparingInt when extracting a numeric field, and test large positive and negative values if the field can contain them.
Should I implement Comparable or write a Comparator?
Implement Comparable when a class has one obvious natural order, such as dates by time or versions by number. Write a Comparator for every other order, such as sorting people by city in one screen and by age in another, or when you can't change the class.
What does "consistent with equals" mean for a comparator?
It means compare(a, b) returns 0 exactly when a.equals(b) is true. Sorted collections such as TreeSet and TreeMap treat a comparison result of 0 as a duplicate, so a comparator that compares only names will silently drop a second person with the same name. Add tie-breakers to keep distinct items.
Can I sort a map with a comparator?
A TreeMap keeps its keys sorted and accepts a comparator for the key order, for example new TreeMap<>(String.CASE_INSENSITIVE_ORDER). To order by values, sort the map's entries with Map.Entry.comparingByValue() and collect them into a LinkedHashMap to keep that order.
Java Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
When does thenComparing run its secondary rule?
What does List.sort change?
Java Quiz
2 questions to check what you learned.
Why use Integer.compare instead of a-b?
- Subtraction can overflow and give a wrong sign
- compare always returns an array
- Subtraction is forbidden in methods
What does comparingInt(...).reversed().thenComparing(...) reverse?
- The primary numeric ordering only
- Every later tie-breaker automatically
- The original source list before comparison