sorting: Comparable vs Comparator from first principles, comparing/thenComparing/nullsFirst chaining, records as sort keys (and the record/equals compareTo trap), and a real reproduction of the int-subtraction-overflow comparator bug

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YXCrLgRKFgCh9RHKW8xaqJ
This commit is contained in:
Claude
2026-10-01 16:19:14 +00:00
parent 8eb601aa0e
commit dfd3a830cd
25 changed files with 883 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
# 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
```bash
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](../LICENSE).