Java Collections Sorting and Comparators

Java comparators define ordering separately from the objects being sorted.

Lesson 54 of 127Intermediate14 min read

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.

Example 1

Java Collections Sorting and Comparators in a complete program

Java
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);
    }
}
Output
[Ava, Zia, Leonie]
Line-by-line explanation
  1. Line 5Evaluates names and prints [Ava, Zia, Leonie].
Example 2

A tie-breaker resolves equal lengths

Java
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);
    }
}
Output
[Al, Bo, Cara]
Line-by-line explanation
  1. Line 5Evaluates names and prints [Al, Bo, Cara].
Example 3

Reverse only the primary rule

Java
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);
    }
}
Output
[Cara, Al, Bo]
Line-by-line explanation
  1. Line 6Evaluates names and 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.

Multiple choiceeasy

When does thenComparing run its secondary rule?

Multiple choicemedium

What does List.sort change?

Java Quiz

2 questions to check what you learned.

  1. 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
  2. What does comparingInt(...).reversed().thenComparing(...) reverse?

    • The primary numeric ordering only
    • Every later tie-breaker automatically
    • The original source list before comparison