Java List Sorting and Comparators
Choose an ordering rule before choosing sorting syntax. Start with numbers and text, build comparators one key at a time, then create a stable inventory preview with clear tie and missing-name policies.
Before you start: review collection algorithms, list copying, and anonymous classes. Every runner is a complete Java 21 Main.java program with no input required. Execution requires the isolated sandbox; no fallback runs code on the website host.
What you will be able to do
- Distinguish natural order from a supplied Comparator.
- Interpret negative, zero, and positive comparison results.
- Compare integers without overflow and respect ordering contracts.
- Chain primary and secondary keys with deliberate reversal scope.
- Handle null elements separately from null sort-key fields.
- Return a validated sorted copy while retaining duplicates and stable ties.
1. Sorting changes positions, not membership
Imagine displaying study durations from shortest to longest. Sorting rearranges the entries according to a rule; it does not discard repeated values. Our two threes remain two entries. The original List.of belongs to the caller, so we create an editable ArrayList and sort that copy instead of trying to change the unmodifiable source.
Collections.sort(sorted) uses the elements’ natural ordering. Integer natural order is numeric ascending, so 3 comes before 10 and 20. List.sort takes a supplied Comparator; Comparator.reverseOrder gives descending natural order here. Both calls edit their supplied list rather than returning a new automatically detached collection.
Comparable is a rule implemented by a type itself through compareTo. Comparator is a separate ordering object that can describe one of several display policies. Built-in Integer and String already implement Comparable. Our inventory objects will not need a single global natural order: a supplied comparator can choose the fields relevant to this report.
Natural numeric order on a copy
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Ascending: [3, 3, 10, 20]
Descending: [20, 10, 3, 3]
Original: [20, 3, 10, 3]
2. Natural String order is not numeric or human-language sorting
The labels 2, 10, and 30 are Strings, not Integer values. In natural String order, the initial 1 in 10 comes before the initial 2, so the output is 10, 2, 30. If those labels represent numbers in your domain, parsing and validating numeric data is a separate decision; merely sorting text does not interpret it as quantities.
Natural String order compares UTF-16 code units and distinguishes case. Java therefore precedes the lowercase css and sql in the second example. This is predictable programming-language behavior, not a guarantee of human dictionary ordering across languages. A locale-aware presentation can require a Collator or another explicit policy beyond this introductory lesson.
Write down the intended comparison before picking syntax. Do you want text order, numeric order, name length, or a business field? Avoid silently normalizing source data merely to change its display. You can compare a selected key without rewriting the stored object, while separately documenting any normalization your application genuinely requires.
Numeric text is still text
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Text order: [10, 2, 30]
Case-sensitive order: [Java, css, sql]
3. A comparator answers before, tied, or after
compare(left, right) returns a negative number when left belongs before right, zero when they tie under this rule, and a positive number when left belongs after right. The exact magnitude is not important. Our explicit Comparator compares String lengths with Integer.compare, so go precedes the three-letter words and java follows them.
The anonymous class is an ordinary object implementing Comparator<String>. Its compare method makes the rule visible without requiring lambda syntax. Equal lengths return zero, so sql and css remain in their original relative order in this stable list sort. A comparison tie does not necessarily mean two objects are equal, and List sorting does not deduplicate them.
A valid ordering must be consistent in direction and transitive: if A precedes B and B precedes C, A must precede C. Do not use random values, changing external state, or boolean-like 0/1 results as a comparator. Use Integer.compare rather than subtracting arbitrary ints, because subtraction can overflow and reverse the intended sign. The repair exercise isolates that bug.
Write an explicit comparison rule first
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
[go, sql, css, java]
4. Compare the next key only when the earlier key ties
For a glossary ordered by word length, equal-length words may need alphabetical order instead of their original arrival order. thenComparing adds that secondary rule. In our input, sql and css both have length three; the primary comparison ties, so natural String order puts css first. The two-letter go still comes before either.
Comparator.comparingInt(String::length) is shorter notation for extracting each String’s int length and comparing it safely. String::length is a method reference: for each compared String, call its length method. It does not call a static method or compute one shared length in advance. Broader lambda-expression syntax is outside this course; the key extractor is explained here so you can read this comparator directly.
The chain is lexicographic: first length, then text only for a length tie. It is not a combined score and it does not sort the entire list again independently by the secondary key. This distinction becomes especially useful for inventory: quantity determines priority, while name explains how to order entries with the same quantity.
Break length ties using natural String order
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
[go, css, sql, java]
Trace the three-letter tie
- Compare sql and css by length: both are three, so the result is zero.
- Consult natural String order only for this tie.
- css comes before sql, while go remains ahead of both by length.
Remove thenComparing from the runner and predict the stable source order of the equal-length words again.
5. Where reversed appears changes the whole requirement
Suppose longer words should come first but equal-length words should still appear in ascending text order. Reverse the length comparator before adding the secondary comparator. The first report places java first, then css and sql, then go. Only the primary key is descending; its tie-breaker remains ascending.
Calling reversed after building the complete length-then-text chain reverses that whole rule. Equal-length ties now run in descending text order, so sql appears before css. Read chained code in groups: build the primary rule, possibly reverse it, then add the next rule. Parentheses and line breaks should help readers see that scope.
Do not substitute Collections.reverse on an ascending result without checking the tie requirement. Reversing the sequence also reverses the order inside ties, whereas a stable descending sort can preserve input order for entries tied by its comparator. Decide the full ordering policy first instead of assuming every route to ‘descending’ is equivalent.
Reverse one key or the whole rule?
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Length down, ties up: [java, css, sql, go]
Both down: [java, sql, css, go]
6. Handle null elements and missing key fields separately
A natural comparator expects real values. Comparator.nullsLast wraps another comparator so null elements follow non-null ones, while the wrapped rule orders actual Strings. The example uses Arrays.asList only to create null-containing input, then copies it into an editable ArrayList. List.of would reject null at construction before any sort begins.
To keep null last while reversing the real names, wrap reverseOrder inside nullsLast. Reversing the entire nullsLast comparator would also reverse the null placement. That is the same scope issue as the prior example, now affecting missing-data presentation rather than just a secondary alphabetic rule.
For objects, a null Item is different from an Item whose name field is null. A name-key comparator can handle the missing name but still cannot call quantity on a null object. Our lab rejects null Item entries, allows a null name as an explicitly missing label, and applies nullsLast only to the name comparison within equal-quantity groups.
Keep null last in both ascending and descending displays
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Ascending, null last: [java, sql, null, null]
Descending, null last: [sql, java, null, null]
7. A report can sort objects without changing their fields
Each Item has an SKU, name, and quantity. Quantity ascending puts low-stock entries first; name ascending organizes ties. Item::quantity and Item::name read the relevant fields through accessor methods. The comparator never edits an Item and does not require Item to implement Comparable for every possible future business report.
Trace the sample: Book and Pen with quantity two both precede the quantity-five Pen. Between the two quantity-two entries, Book precedes Pen by name. Sorting does not group all Pens together regardless of quantity, because quantity is the primary key. The compact toString format exposes SKU:name(quantity) so you can check every part of the output.
This sample’s fields are final and its Strings are immutable, so comparison observes stable state. A shallow copied list still refers to the same Item objects; sorting changes only references’ positions in the copied container. Do not mutate sort keys during comparison or assume a sorted list will automatically maintain its order after later object changes.
Read sort keys from inventory objects
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
[B:Book(2), C:Pen(2), A:Pen(5)]
8. Stable ties preserve history; they do not invent a universal order
List sorting is stable: entries that compare as zero retain their relative source order. A and B both have name Pen and quantity two, so their order remains A then B after Book moves ahead. The SKU makes that preservation visible. We intentionally do not add SKU as a third key, because the requirement is to retain source order for complete comparison ties.
Stable does not mean canonical across differently ordered inputs. If B appears before A in the source, their exact tie stays B then A. Add an explicit third key only when the business requirement calls for it. A List also retains duplicate entries even when comparison returns zero; a TreeSet using that same comparator has different uniqueness semantics.
Validate data before sorting a private copy. Reject invalid objects and quantities deliberately, then apply the agreed name-null policy. Sorting can fail for invalid data or a broken comparator and is not a transactional rollback service for a caller’s list. Copying protects the source’s sequence, while stable fields keep the comparisons meaningful throughout the operation.
Exact comparison ties keep their source order
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
[C:Book(2), A:Pen(2), B:Pen(2)]
Entries retained: 3
Repair exercise: subtraction reversed the numeric order
This ascending comparator claims the largest int belongs before the smallest. Run the isolated comparison and explain why subtraction is unsafe. Repair the compare method without changing the requested ascending order.
Main.java · Inspect the comparison sign
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Current wrong result:
Comparison sign: -1
Largest before smallest: true
Reveal the explained repair
MAX_VALUE minus MIN_VALUE exceeds the int range and wraps to -1, producing the wrong direction. Integer.compare determines the sign without overflowing through subtraction. The test deliberately checks one pair rather than teaching an unreliable expected sort order for an invalid comparator. Null operands remain outside this numeric comparator’s policy.
Main.java · Compare without subtraction overflow
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Comparison sign: 1
Largest before smallest: false
Independent lab: a stable low-stock inventory preview
Create a low-stock inventory preview. Sort quantity ascending, then name ascending with missing names last within each quantity group. Keep exact ties in source order, retain duplicates, and never edit the source container or Item fields.
- Implement sortedInventory(List<Item> source), returning a new editable List<Item>.
- Reject a null list, null Item, missing/blank SKU, negative quantity, or non-null blank name with IllegalArgumentException.
- Allow null names as missing labels; compare non-null names by case-sensitive natural String order without normalization.
- Use quantity ascending as the primary key and name ascending/nulls-last as the secondary key.
- Preserve source order for ties in both keys; do not add SKU as a tie-breaker or remove duplicates.
- Accept empty input, leave source unchanged on success and rejection, and return a shallow container copy.
The starter includes the complete Item model and compiles without printing. Implement and call the helper to reach this output; each displayed entry is SKU:name(quantity):
Preview: [F:Eraser(0), B:Book(2), E:Book(2), C:Pen(2), D:null(2), A:Pen(5)]
Original: [A:Pen(5), B:Book(2), C:Pen(2), D:null(2), E:Book(2), F:Eraser(0)]
Entries retained: 6
Main.java · Build the inventory ordering
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Stage 1 hints
Stage 1: own a separate container and validate it. Copy source, reject invalid Item references before reading fields, and distinguish an allowed null name from a rejected blank name.
Stage 2 hints
Stage 2: build the rule in priority order. comparingInt(Item::quantity) is primary. thenComparing reads Item::name with nullsLast(naturalOrder()) for equal quantities.
Stage 3 hints
Stage 3: sort the copy and verify stability. B and E both compare as Book with quantity two, so they stay B then E. Return the editable result, retain all entries, and check the original sequence afterward.
Reveal the complete explained solution
Main.java · Validate, copy, and sort with stable ties
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Eraser has quantity zero and appears first. Quantity-two Book entries B and E precede Pen, keeping B then E because their compared keys tie. The missing name D follows named entries of that same quantity group, not every named entry across all quantities. Quantity-five A comes last. SKU is displayed for identity but is not a sort key. All fields stay unchanged and every result entry is the same Item reference taken from the source; only the container order is new.
Test your contract
- Empty and singleton inputs produce independently editable containers.
- Equal quantities use names; missing names follow named items only within that quantity group.
- Exact key ties preserve input order, including repeated references and duplicate names.
- Zero and Integer.MAX_VALUE quantities sort correctly without subtraction; reject negative quantities and invalid fields without editing source.
- Use ArrayList, LinkedList, and List.of sources; compare object identity and order, then edit the returned list to confirm container independence.
For null Item tests, add null to an ArrayList explicitly; List.of rejects it at construction. Null names inside real Items are different and allowed. Add a second Item with identical name and quantity but another SKU, then reverse those two in the source to observe stability rather than inventing an unrequested SKU ordering.
Common mistakes and how to diagnose them
Subtracting arbitrary numbers to compare them
Subtraction can overflow and return the opposite sign. Prefer Integer.compare, Long.compare, or comparingInt/comparingLong for the appropriate key type.
Reversing more keys than intended
reversed applies to the comparator built so far. Reverse a primary rule before thenComparing when only the primary direction should change; reversing the whole chain also reverses its tie-breakers.
A null-safe field comparator still crashes on a null object
Handling Item.name does not make Item::quantity callable on a null Item. Reject null objects or define a separate outer object-null policy.
Using sorting as deduplication
List sorting retains entries, including those comparing as zero. Set uniqueness is a different operation and may discard data under the same comparison rule.
Expecting stability to invent a tie-breaker
Stability preserves the original order of comparison ties. If you need a consistent SKU order across different source sequences, add that key explicitly instead of describing source-order preservation as canonical.
The source changed while creating a preview
List.sort and Collections.sort edit their supplied list. Copy first, validate privately, and remember that a copied container still shares mutable element references.
Frequently asked questions
What is the difference between Comparable and Comparator?
Comparable defines a type’s natural ordering through compareTo. Comparator is a separate rule supplied for a particular operation, allowing several report orders without changing the class itself.
Does compare have to return exactly -1, 0, or 1?
No. The sign matters, not the magnitude. Use a valid, transitive rule with consistent direction. Do not assume a String comparison or every custom comparator returns only those three literal values.
What does Item::quantity mean?
It is a method reference used as a key extractor: read quantity() from each Item being compared. comparingInt handles the integer comparison. The lesson first shows an explicit Comparator class so this shorter syntax has a concrete meaning.
Are all null names placed at the very end in the lab?
No. Quantity is primary. A missing name comes after real names within its own quantity group, so a null name at quantity two still precedes a named item at quantity five.
Can I sort a List.of directly?
It is unmodifiable, so use an editable copy for a sorted preview. Sort operations require supported element replacement, not merely a List variable declaration. Do not rely on a no-op sort to succeed on an unmodifiable list.
Is sorting a shallow copy enough to isolate mutable objects?
It isolates changes to list positions and membership, not mutations to shared objects. This lesson uses stable final fields and never edits the Items. For mutable models, define copying and concurrent-change policies separately.
A comparator is a precise ordering policy
Natural order belongs to Comparable; a Comparator supplies another rule. Use comparison signs correctly, chain keys in priority order, scope reversal deliberately, and handle nulls at the right level. Stable List sorting preserves complete ties and duplicates, while a copied container protects the original sequence.
Before moving on, explain why subtraction overflow broke the repair, why B stays before E in the lab, and why the missing name at quantity two still precedes a named item at quantity five. Next: generic classes and methods.
Can you explain the full ordering rule?
Answer six questions and read the explanations. Quiz answers are temporary and do not save course progress.
References and related learning
This original SovranCode lesson uses official Java 21 APIs for comparison contracts, stable sorting, and supported list operations.
- Java 21 Comparator API
- Java 21 List API: stable sort
- Java 21 Comparable API
- Java 21 Collections algorithms
Review algorithms · Review list copies · Java course roadmap.
Last updated: