arithmetic: companion code for the Add Two Numbers Without Overflow post
Silent int/long overflow reproduced with real numbers, the Math *Exact() family (addExact/subtractExact/multiplyExact/incrementExact/decrementExact/ negateExact/toIntExact/absExact) with the abs(MIN_VALUE) trap absExact exists to catch, widening to long vs BigInteger, and the XOR-based overflow bit trick Math.addExact uses internally, cross-checked against BigInteger ground truth over 100k random int pairs per JUnit repeat. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FhzLY5p6okFva3qsnsRyvM
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package com.ankurm.arithmetic;
|
||||
|
||||
/**
|
||||
* The {@code Math} class carries a whole family of "exact" arithmetic methods: every one of them
|
||||
* does the normal operation when the true mathematical result fits the return type, and throws
|
||||
* {@link ArithmeticException} instead of silently wrapping when it doesn't. This demo runs the
|
||||
* addition-relevant members of that family - both the "it just works" case and the "it throws"
|
||||
* case - plus one genuinely surprising edge case: {@code Math.abs(Integer.MIN_VALUE)} does NOT
|
||||
* throw and does NOT return a positive number, because positive {@code Integer.MIN_VALUE} has no
|
||||
* representation in a 32-bit int. {@code Math.absExact} exists specifically to catch that.
|
||||
*/
|
||||
public class ExactArithmeticDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== addExact / subtractExact / multiplyExact: normal case ===");
|
||||
System.out.println("Math.addExact(20, 22) = " + Math.addExact(20, 22));
|
||||
System.out.println("Math.subtractExact(50, 8) = " + Math.subtractExact(50, 8));
|
||||
System.out.println("Math.multiplyExact(6, 7) = " + Math.multiplyExact(6, 7));
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== addExact / multiplyExact: overflow case - throws instead of wrapping ===");
|
||||
try {
|
||||
Math.addExact(Integer.MAX_VALUE, 1);
|
||||
} catch (ArithmeticException e) {
|
||||
System.out.println("Math.addExact(Integer.MAX_VALUE, 1) threw: " + e.getMessage());
|
||||
}
|
||||
try {
|
||||
Math.multiplyExact(100_000, 100_000);
|
||||
} catch (ArithmeticException e) {
|
||||
System.out.println("Math.multiplyExact(100000, 100000) threw: " + e.getMessage());
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== incrementExact / decrementExact / negateExact ===");
|
||||
System.out.println("Math.incrementExact(41) = " + Math.incrementExact(41));
|
||||
System.out.println("Math.decrementExact(43) = " + Math.decrementExact(43));
|
||||
System.out.println("Math.negateExact(-42) = " + Math.negateExact(-42));
|
||||
try {
|
||||
Math.incrementExact(Integer.MAX_VALUE);
|
||||
} catch (ArithmeticException e) {
|
||||
System.out.println("Math.incrementExact(Integer.MAX_VALUE) threw: " + e.getMessage());
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== toIntExact: the safe narrowing cast from long back to int ===");
|
||||
long fitsInInt = 2_000_000_000L;
|
||||
System.out.println("Math.toIntExact(2000000000L) = " + Math.toIntExact(fitsInInt));
|
||||
long tooBigForInt = 3_000_000_000L;
|
||||
try {
|
||||
Math.toIntExact(tooBigForInt);
|
||||
} catch (ArithmeticException e) {
|
||||
System.out.println("Math.toIntExact(3000000000L) threw: " + e.getMessage());
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== The classic trap: Math.abs(Integer.MIN_VALUE) does NOT throw, and is wrong ===");
|
||||
int minValueAbs = Math.abs(Integer.MIN_VALUE);
|
||||
System.out.println("Math.abs(Integer.MIN_VALUE) = " + minValueAbs
|
||||
+ " (still negative! +2147483648 doesn't fit in an int)");
|
||||
try {
|
||||
Math.absExact(Integer.MIN_VALUE);
|
||||
} catch (ArithmeticException e) {
|
||||
System.out.println("Math.absExact(Integer.MIN_VALUE) threw: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.ankurm.arithmetic;
|
||||
|
||||
/**
|
||||
* How does {@code Math.addExact} actually know overflow happened, without doing the addition
|
||||
* twice at different widths? A bit trick: for {@code int x + int y = result}, overflow occurred
|
||||
* if and only if {@code x} and {@code y} have the same sign AND that sign differs from
|
||||
* {@code result}'s sign. Two numbers with the same sign can only overflow into the opposite
|
||||
* sign; two numbers with different signs can never overflow (their sum is always between them).
|
||||
*
|
||||
* <p>Expressed without any branching on sign:
|
||||
* {@code overflow = ((x ^ result) & (y ^ result)) < 0}. XOR-ing two ints and checking the sign
|
||||
* bit of the result is a cheap way to ask "do these differ in sign?" - the expression is true
|
||||
* (negative, i.e. the top bit is set) exactly when both {@code x} and {@code y} differ in sign
|
||||
* from {@code result}, which is exactly the overflow condition above. This is not a novel trick -
|
||||
* it is functionally the same check the JDK's own {@code Math.addExact} source uses internally.
|
||||
*/
|
||||
public class OverflowBitTrickDemo {
|
||||
|
||||
/** Returns true if x + y overflows the int range, computed without widening to long. */
|
||||
static boolean additionOverflows(int x, int y) {
|
||||
int result = x + y; // allowed to wrap; we only use it to test the sign relationship
|
||||
return ((x ^ result) & (y ^ result)) < 0;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Hand-rolled overflow detector vs. known cases ===");
|
||||
report(2_000_000_000, 2_000_000_000); // same sign, overflows
|
||||
report(2_000_000_000, -1_000_000_000); // different signs, never overflows
|
||||
report(Integer.MAX_VALUE, 1); // classic edge case
|
||||
report(Integer.MIN_VALUE, -1); // negative-side overflow
|
||||
report(100, 200); // ordinary case, no overflow
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== Cross-checked against Math.addExact() on the same inputs ===");
|
||||
int[][] cases = {
|
||||
{2_000_000_000, 2_000_000_000},
|
||||
{2_000_000_000, -1_000_000_000},
|
||||
{Integer.MAX_VALUE, 1},
|
||||
{Integer.MIN_VALUE, -1},
|
||||
{100, 200}
|
||||
};
|
||||
for (int[] c : cases) {
|
||||
boolean detected = additionOverflows(c[0], c[1]);
|
||||
boolean addExactThrew;
|
||||
try {
|
||||
Math.addExact(c[0], c[1]);
|
||||
addExactThrew = false;
|
||||
} catch (ArithmeticException e) {
|
||||
addExactThrew = true;
|
||||
}
|
||||
System.out.println(c[0] + " + " + c[1] + ": bit-trick says overflow=" + detected
|
||||
+ ", Math.addExact() agrees=" + (detected == addExactThrew));
|
||||
}
|
||||
}
|
||||
|
||||
private static void report(int x, int y) {
|
||||
boolean overflow = additionOverflows(x, y);
|
||||
System.out.println(x + " + " + y + " -> raw int result " + (x + y) + ", overflow detected: " + overflow);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.ankurm.arithmetic;
|
||||
|
||||
/**
|
||||
* Java's {@code int} and {@code long} arithmetic never throws on overflow. It wraps around
|
||||
* silently, two's-complement style, and the compiler and JVM both consider {@code a + b} on
|
||||
* two ints to be perfectly well-defined behavior even when the mathematically correct result
|
||||
* doesn't fit. This demo reproduces that wraparound with real numbers instead of just quoting
|
||||
* {@code Integer.MAX_VALUE}.
|
||||
*/
|
||||
public class OverflowWrapDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== int addition wraps silently past Integer.MAX_VALUE ===");
|
||||
int max = Integer.MAX_VALUE;
|
||||
int wrapped = max + 1;
|
||||
System.out.println("Integer.MAX_VALUE = " + max);
|
||||
System.out.println("Integer.MAX_VALUE + 1 = " + wrapped + " (wrapped to Integer.MIN_VALUE, no exception)");
|
||||
System.out.println("Integer.MIN_VALUE = " + Integer.MIN_VALUE);
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== A realistic case: summing item prices in cents overflows int ===");
|
||||
int priceCents = 1_800_000_000; // $18,000,000.00, a plausible large invoice line in cents
|
||||
int quantity = 2;
|
||||
int totalCents = priceCents * quantity;
|
||||
System.out.println("priceCents * quantity = " + priceCents + " * " + quantity + " = " + totalCents
|
||||
+ " (should be 3,600,000,000 - this is wrong, and no exception was thrown)");
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== Multiplication overflows just as silently as addition ===");
|
||||
int a = 100_000;
|
||||
int b = 100_000;
|
||||
int product = a * b; // mathematically 10,000,000,000 - doesn't fit in an int
|
||||
System.out.println(a + " * " + b + " = " + product + " (true value is 10,000,000,000)");
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== long has the same problem, just a much higher ceiling ===");
|
||||
long longMax = Long.MAX_VALUE;
|
||||
long longWrapped = longMax + 1;
|
||||
System.out.println("Long.MAX_VALUE = " + longMax);
|
||||
System.out.println("Long.MAX_VALUE + 1 = " + longWrapped + " (wrapped to Long.MIN_VALUE)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.ankurm.arithmetic;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Two more ways to avoid the overflow in {@link OverflowWrapDemo}: widen to {@code long} before
|
||||
* adding (cheap, but only pushes the ceiling higher - it can still overflow), or switch to
|
||||
* {@link BigInteger} for genuinely unbounded precision (more expensive, never overflows).
|
||||
*/
|
||||
public class WideningAndBigIntegerDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Widening to long fixes the int-range overflow from OverflowWrapDemo ===");
|
||||
int a = Integer.MAX_VALUE;
|
||||
int bad = a + 1; // wraps, wrong
|
||||
long good = (long) a + 1; // widened before the add, correct
|
||||
System.out.println("int: Integer.MAX_VALUE + 1 = " + bad + " (wrong)");
|
||||
System.out.println("long: (long) Integer.MAX_VALUE + 1 = " + good + " (correct)");
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== Widening only raises the ceiling - long still overflows eventually ===");
|
||||
long lmax = Long.MAX_VALUE;
|
||||
long stillWraps = lmax + 1;
|
||||
System.out.println("Long.MAX_VALUE + 1 = " + stillWraps + " (wrapped to Long.MIN_VALUE - same bug, bigger numbers)");
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== BigInteger never overflows - it grows to fit the true value ===");
|
||||
BigInteger big = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE);
|
||||
System.out.println("BigInteger.valueOf(Long.MAX_VALUE).add(ONE) = " + big);
|
||||
|
||||
BigInteger huge = BigInteger.valueOf(2).pow(100);
|
||||
System.out.println("BigInteger.valueOf(2).pow(100) = " + huge);
|
||||
System.out.println("(that value has no exact long or int representation at all)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.ankurm.arithmetic;
|
||||
|
||||
import org.junit.jupiter.api.RepeatedTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ArithmeticClaimsTest {
|
||||
|
||||
@Test
|
||||
void plainIntAdditionWrapsPastMaxValueInsteadOfThrowing() {
|
||||
int wrapped = Integer.MAX_VALUE + 1;
|
||||
assertEquals(Integer.MIN_VALUE, wrapped);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addExactThrowsExactlyWhenTheTrueSumDoesNotFitAnInt() {
|
||||
assertEquals(42, Math.addExact(20, 22));
|
||||
assertThrows(ArithmeticException.class, () -> Math.addExact(Integer.MAX_VALUE, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiplyExactThrowsExactlyWhenTheTrueProductDoesNotFitAnInt() {
|
||||
assertEquals(42, Math.multiplyExact(6, 7));
|
||||
assertThrows(ArithmeticException.class, () -> Math.multiplyExact(100_000, 100_000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void absDoesNotThrowOnIntegerMinValueAndReturnsAWrongNegativeNumber() {
|
||||
// Math.abs is NOT part of the exact family - this is the trap absExact exists to catch.
|
||||
assertEquals(Integer.MIN_VALUE, Math.abs(Integer.MIN_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void absExactThrowsOnIntegerMinValueInsteadOfReturningAWrongAnswer() {
|
||||
assertThrows(ArithmeticException.class, () -> Math.absExact(Integer.MIN_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toIntExactThrowsWhenALongDoesNotFitInAnInt() {
|
||||
assertEquals(2_000_000_000, Math.toIntExact(2_000_000_000L));
|
||||
assertThrows(ArithmeticException.class, () -> Math.toIntExact(3_000_000_000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wideningToLongBeforeAddingAvoidsTheIntRangeOverflow() {
|
||||
int a = Integer.MAX_VALUE;
|
||||
long widened = (long) a + 1;
|
||||
assertEquals(2_147_483_648L, widened);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bigIntegerNeverOverflowsRegardlessOfMagnitude() {
|
||||
BigInteger huge = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE);
|
||||
assertEquals(new BigInteger("9223372036854775808"), huge);
|
||||
}
|
||||
|
||||
@RepeatedTest(5)
|
||||
void bitTrickOverflowDetectorAgreesWithBigIntegerGroundTruthOnRandomPairs() {
|
||||
Random random = new Random();
|
||||
for (int i = 0; i < 100_000; i++) {
|
||||
int x = random.nextInt();
|
||||
int y = random.nextInt();
|
||||
|
||||
boolean detected = OverflowBitTrickDemo.additionOverflows(x, y);
|
||||
|
||||
BigInteger trueSum = BigInteger.valueOf(x).add(BigInteger.valueOf(y));
|
||||
boolean actuallyOverflows = trueSum.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0
|
||||
|| trueSum.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0;
|
||||
|
||||
assertEquals(actuallyOverflows, detected,
|
||||
() -> x + " + " + y + ": bit trick said overflow=" + detected
|
||||
+ " but true sum is " + trueSum);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void bitTrickAgreesWithMathAddExactOnKnownEdgeCases() {
|
||||
int[][] cases = {
|
||||
{2_000_000_000, 2_000_000_000},
|
||||
{2_000_000_000, -1_000_000_000},
|
||||
{Integer.MAX_VALUE, 1},
|
||||
{Integer.MIN_VALUE, -1},
|
||||
{Integer.MIN_VALUE, Integer.MIN_VALUE},
|
||||
{100, 200},
|
||||
{0, 0}
|
||||
};
|
||||
for (int[] c : cases) {
|
||||
boolean detected = OverflowBitTrickDemo.additionOverflows(c[0], c[1]);
|
||||
boolean addExactThrew = true;
|
||||
try {
|
||||
Math.addExact(c[0], c[1]);
|
||||
addExactThrew = false;
|
||||
} catch (ArithmeticException ignored) {
|
||||
// expected on overflow
|
||||
}
|
||||
assertTrue(detected == addExactThrew,
|
||||
c[0] + " + " + c[1] + ": bit trick=" + detected + " vs addExact threw=" + addExactThrew);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user