Files
java-core-examples/sorting
..

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/06 is the full transcript: (a,b) -> a.val - b.val sorts [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 of D(MIN), and C(-20) landing dead last. Integer.compare sorts the exact same list correctly. Both are real List.sort() calls against the same seven elements - see output/06 and SortingTest#subtractionComparatorProducesWrongOrderOnOverflow.
  • A record's generated equals() uses every component. Your compareTo() doesn't have to. If it doesn't, compareTo(x) == 0 stops implying equals(x) is true, and anything keyed on compareTo - most importantly TreeSet/TreeMap - starts silently treating distinct records as duplicates. See output/05, section 4.
  • Collections.sort / List.sort never threw here, even with Integer.MAX_VALUE and Integer.MIN_VALUE in a seven-element list. Java's sort implementation only runs its contract-violation detector once a run is long enough to need merging (TimSort's MIN_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.