Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YXCrLgRKFgCh9RHKW8xaqJ
sorting
Companion code for the ankurm.com post "Java Comparator & Comparable: The Complete Guide."
Module in java-core-examples, the Java-core series.
Versions this was built and tested against
| Component | Version | Notes |
|---|---|---|
| JDK | 25.0.4.1+1 (Temurin, LTS) | Comparator.comparing/thenComparing/nullsFirst/nullsLast shipped in Java 8 (2014), unchanged since. Records (used here as sort keys) shipped in Java 16 (2021). |
| JUnit Jupiter | 5.11.0 | |
| Maven | 3.9.11 |
Quickstart
export JAVA_HOME=/path/to/jdk-17-or-newer
mvn compile
java -cp target/classes com.ankurm.sorting.IntegerOverflowBugDemo
scripts/run-all.sh regenerates every file in output/. scripts/run.sh <ClassName> runs one
demo ad hoc.
What's in here
| File | What it shows |
|---|---|
Employee.java |
A plain POJO that deliberately does NOT implement Comparable - it has no one obvious order, which is the case Comparator exists for. |
Product.java |
A POJO that DOES implement Comparable - price is its one obvious natural order - using Double.compare, never subtraction. |
EmployeeNameComparator.java + OldSchoolComparatorDemo.java |
Pre-Java-8 Comparator construction: a named class and an anonymous inner class, run side by side. |
ComparableNaturalOrderDemo.java |
Collections.sort(products) calling Product.compareTo() automatically. |
ComparatorFactoryMethodsDemo.java |
Lambdas, Comparator.comparing/comparingInt/comparingDouble, thenComparing, reversed, nullsFirst/nullsLast, chained together. |
TreeSetOrderingDemo.java |
The same element type sorted two ways in a TreeSet - natural order from the no-arg constructor, a reversed custom order from the Comparator constructor. |
StockPrice.java + RecordOrderingDemo.java |
A record implementing Comparable; its accessor is price() not getPrice(); and the record-specific trap where a compareTo that does not use every component stops being consistent with the record's generated equals() - demonstrated by a TreeSet silently dropping an element. |
Ticket.java + IntegerOverflowBugDemo.java |
The main exhibit. (a, b) -> a.val - b.val reproduced exactly, sorted against Integer.compare, with the actual wrong order it produces for Integer.MAX_VALUE / Integer.MIN_VALUE input, checked independently of either comparator. |
SortingTest.java |
Pins all of the above as assertions, 9/9 passing - including the overflow bug and the record/equals inconsistency as regression tests, not just printouts. |
output/01-07 |
Captured runs of all six demos plus the test suite. |
Notes worth knowing before reading the post
- The subtraction comparator does not crash. It just lies.
output/06is the full transcript:(a,b) -> a.val - b.valsorts[A(10), B(MAX), C(-20), D(MIN), E(0), F(5), G(-1)]into[G(-1), E(0), F(5), A(10), B(MAX), D(MIN), C(-20)]-B(MAX)sitting directly in front ofD(MIN), andC(-20)landing dead last.Integer.comparesorts the exact same list correctly. Both are realList.sort()calls against the same seven elements - seeoutput/06andSortingTest#subtractionComparatorProducesWrongOrderOnOverflow. - A record's generated
equals()uses every component. YourcompareTo()doesn't have to. If it doesn't,compareTo(x) == 0stops implyingequals(x)is true, and anything keyed oncompareTo- most importantlyTreeSet/TreeMap- starts silently treating distinct records as duplicates. Seeoutput/05, section 4. Collections.sort/List.sortnever threw here, even withInteger.MAX_VALUEandInteger.MIN_VALUEin a seven-element list. Java's sort implementation only runs its contract-violation detector once a run is long enough to need merging (TimSort'sMIN_MERGE, 32 elements by default); below that it is a plain insertion sort that will happily produce a wrong-but-silent answer. A small reproduction case like this one will not save you from the bug in production - it will just fail to warn you in a unit test with a short list.
License
MIT - see the repo-wide LICENSE.