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,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}
|
||||
@@ -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)]
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user