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
@@ -0,0 +1,11 @@
=== 1. Named class: new EmployeeNameComparator() ===
Employee{id=102, name='Alex', age=45, salary=120000.0}
Employee{id=104, name='Alex', age=35, salary=110000.0}
Employee{id=103, name='Bob', age=29, salary=85000.0}
Employee{id=101, name='Zoe', age=29, salary=80000.0}
=== 2. Anonymous inner class, sorting by age ===
Employee{id=101, name='Zoe', age=29, salary=80000.0}
Employee{id=103, name='Bob', age=29, salary=85000.0}
Employee{id=104, name='Alex', age=35, salary=110000.0}
Employee{id=102, name='Alex', age=45, salary=120000.0}
@@ -0,0 +1,2 @@
Before sort: [Keyboard($49.99), Monitor($299.0), Mouse($29.5), Webcam($79.99)]
After Collections.sort(products): [Mouse($29.5), Keyboard($49.99), Webcam($79.99), Monitor($299.0)]
@@ -0,0 +1,38 @@
=== 1. Lambda (type inferred) ===
Employee{id=101, name='Zoe', age=29, salary=80000.0}
Employee{id=103, name='Bob', age=29, salary=85000.0}
Employee{id=104, name='Alex', age=35, salary=110000.0}
Employee{id=102, name='Alex', age=45, salary=120000.0}
=== 2. Comparator.comparing(Employee::getName) - method reference ===
Employee{id=102, name='Alex', age=45, salary=120000.0}
Employee{id=104, name='Alex', age=35, salary=110000.0}
Employee{id=103, name='Bob', age=29, salary=85000.0}
Employee{id=101, name='Zoe', age=29, salary=80000.0}
=== 3. comparingInt/comparingDouble - avoid boxing for primitives ===
Employee{id=102, name='Alex', age=45, salary=120000.0}
Employee{id=104, name='Alex', age=35, salary=110000.0}
Employee{id=103, name='Bob', age=29, salary=85000.0}
Employee{id=101, name='Zoe', age=29, salary=80000.0}
=== 4. thenComparing - break ties left to right ===
(Two employees are both named "Alex" - watch thenComparing break that tie by age)
Employee{id=104, name='Alex', age=35, salary=110000.0}
Employee{id=102, name='Alex', age=45, salary=120000.0}
Employee{id=103, name='Bob', age=29, salary=85000.0}
Employee{id=101, name='Zoe', age=29, salary=80000.0}
=== 5. nullsFirst - an Employee whose name is null, sorted to the front ===
Employee{id=105, name='null', age=40, salary=90000.0}
Employee{id=102, name='Alex', age=45, salary=120000.0}
Employee{id=104, name='Alex', age=35, salary=110000.0}
Employee{id=103, name='Bob', age=29, salary=85000.0}
Employee{id=101, name='Zoe', age=29, salary=80000.0}
=== 6. nullsLast + CASE_INSENSITIVE_ORDER, chained with thenComparing ===
Employee{id=104, name='Alex', age=35, salary=110000.0}
Employee{id=102, name='Alex', age=45, salary=120000.0}
Employee{id=103, name='Bob', age=29, salary=85000.0}
Employee{id=101, name='Zoe', age=29, salary=80000.0}
Employee{id=106, name='null', age=22, salary=60000.0}
+2
View File
@@ -0,0 +1,2 @@
Natural (no-arg TreeSet ctor, uses Product.compareTo): [Mouse($29.5), Keyboard($49.99), Monitor($299.0)]
Custom (Comparator ctor, descending price) : [Monitor($299.0), Keyboard($49.99), Mouse($29.5)]
+23
View File
@@ -0,0 +1,23 @@
=== 1. Comparator.comparing(StockPrice::ticker) - record accessor, no "get" prefix ===
StockPrice[ticker=AAPL, price=189.5, volume=42000000]
StockPrice[ticker=MSFT, price=410.2, volume=18000000]
StockPrice[ticker=NVDA, price=118.75, volume=210000000]
=== 2. Natural order via StockPrice.compareTo (price only) ===
StockPrice[ticker=NVDA, price=118.75, volume=210000000]
StockPrice[ticker=AAPL, price=189.5, volume=42000000]
StockPrice[ticker=MSFT, price=410.2, volume=18000000]
=== 3. Two DIFFERENT records, same price: compareTo==0 but equals()==false ===
StockPrice[ticker=AAPL, price=150.0, volume=1000000].compareTo(StockPrice[ticker=MSFT, price=150.0, volume=2000000]) = 0
StockPrice[ticker=AAPL, price=150.0, volume=1000000].equals(StockPrice[ticker=MSFT, price=150.0, volume=2000000]) = false
=== 4. That inconsistency reaches a TreeSet: natural order (price-only compareTo) ===
treeSet.add(appleAt150) -> true (first element)
treeSet.add(msftAt150) -> false (TreeSet used compareTo==0 to call this a duplicate)
treeSet now contains 1 element(s): [StockPrice[ticker=AAPL, price=150.0, volume=1000000]]
-> MSFT silently never entered the set, even though msftAt150.equals(appleAt150) is false
=== 5. Fix: a Comparator that is consistent with equals (ticker, then price, then volume) ===
treeSet.add(msftAt150) -> true (now distinguishes the two records)
treeSet now contains 2 element(s): [StockPrice[ticker=AAPL, price=150.0, volume=1000000], StockPrice[ticker=MSFT, price=150.0, volume=2000000]]
@@ -0,0 +1,14 @@
=== 1. The contract violation in isolation ===
buggyCompare(BIG=2147483647, NEG=-2) = -2147483647 (claims BIG < NEG: true)
correctCompare(BIG=2147483647, NEG=-2) = 1 (correctly says BIG > NEG: true)
BIG.val - NEG.val overflows int range because 2147483647 - (-2) = 2147483647 + 2 > Integer.MAX_VALUE, and int arithmetic wraps silently.
=== 2. Sorting a real list with each comparator ===
Input (unsorted): [A(10), B(2147483647), C(-20), D(-2147483648), E(0), F(5), G(-1)]
Sorted with (a,b) -> a.val - b.val : [G(-1), E(0), F(5), A(10), B(2147483647), D(-2147483648), C(-20)]
Sorted with Integer.compare(a.val, b.val) : [D(-2147483648), C(-20), G(-1), E(0), F(5), A(10), B(2147483647)]
=== 3. Checking each result WITHOUT trusting either comparator ===
buggyResult is actually ascending by .val? false
correctResult is actually ascending by .val? true
First inversion: B(2147483647) appears immediately before D(-2147483648), but 2147483647 > -2147483648
+4
View File
@@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.ankurm.sorting.SortingTest
-------------------------------------------------------------------------------
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.122 s -- in com.ankurm.sorting.SortingTest