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:
@@ -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).
|
||||
Reference in New Issue
Block a user