From af615cc17cdc6096c4de002c7247ede1ce723a3a Mon Sep 17 00:00:00 2001 From: asmhatre Date: Wed, 30 Sep 2026 09:16:49 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01FhzLY5p6okFva3qsnsRyvM --- README.md | 1 + arithmetic/README.md | 47 ++++++++ arithmetic/output/01-overflow-wrap.txt | 16 +++ arithmetic/output/02-exact-arithmetic.txt | 24 ++++ .../output/03-widening-and-biginteger.txt | 13 +++ arithmetic/output/04-overflow-bit-trick.txt | 15 +++ arithmetic/output/05-correctness-tests.txt | 4 + arithmetic/pom.xml | 43 +++++++ arithmetic/scripts/run-all.sh | 41 +++++++ .../arithmetic/ExactArithmeticDemo.java | 66 +++++++++++ .../arithmetic/OverflowBitTrickDemo.java | 60 ++++++++++ .../ankurm/arithmetic/OverflowWrapDemo.java | 42 +++++++ .../arithmetic/WideningAndBigIntegerDemo.java | 35 ++++++ .../arithmetic/ArithmeticClaimsTest.java | 106 ++++++++++++++++++ pom.xml | 1 + 15 files changed, 514 insertions(+) create mode 100644 arithmetic/README.md create mode 100644 arithmetic/output/01-overflow-wrap.txt create mode 100644 arithmetic/output/02-exact-arithmetic.txt create mode 100644 arithmetic/output/03-widening-and-biginteger.txt create mode 100644 arithmetic/output/04-overflow-bit-trick.txt create mode 100644 arithmetic/output/05-correctness-tests.txt create mode 100644 arithmetic/pom.xml create mode 100644 arithmetic/scripts/run-all.sh create mode 100644 arithmetic/src/main/java/com/ankurm/arithmetic/ExactArithmeticDemo.java create mode 100644 arithmetic/src/main/java/com/ankurm/arithmetic/OverflowBitTrickDemo.java create mode 100644 arithmetic/src/main/java/com/ankurm/arithmetic/OverflowWrapDemo.java create mode 100644 arithmetic/src/main/java/com/ankurm/arithmetic/WideningAndBigIntegerDemo.java create mode 100644 arithmetic/src/test/java/com/ankurm/arithmetic/ArithmeticClaimsTest.java diff --git a/README.md b/README.md index 11dea01..b382bbb 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ article; each module's own README has that article's version table, quickstart, | [`synchronizers`](synchronizers/) | CountDownLatch vs CyclicBarrier vs Phaser vs Semaphore in Java | | [`executors`](executors/) | ExecutorService Done Right: Shutdown, try-with-resources and Virtual Thread Executors | | [`concurrency-interview`](concurrency-interview/) | Top 40 Java Concurrency Interview Questions and Answers (2026) | +| [`arithmetic`](arithmetic/) | Add Two Numbers in Java Without Overflow: addExact, Widening, and BigInteger | ## License diff --git a/arithmetic/README.md b/arithmetic/README.md new file mode 100644 index 0000000..c5b7737 --- /dev/null +++ b/arithmetic/README.md @@ -0,0 +1,47 @@ +# arithmetic + +Companion code for the ankurm.com post *"Add Two Numbers in Java Without Overflow."* Eighth +module in `java-core-examples`. + +`a + b` on two Java `int`s never throws when the true mathematical result doesn't fit - it wraps +around silently, two's-complement style. This module reproduces that wraparound with real +numbers, then works through every real fix: the `Math` class's `*Exact()` family (which throws +`ArithmeticException` instead of wrapping), widening to `long`, `BigInteger` for unbounded +precision, and the actual bit trick `Math.addExact` uses internally to detect overflow without +doing the addition twice at different widths. + +## Versions this was built and tested against + +| Component | Version | Notes | +|---|---|---| +| JDK | 25.0.4.1+1 (Temurin, LTS) | No preview flags needed - every API used here is finalized. | +| JUnit Jupiter | 5.11.0 | Includes a 100,000-iteration randomized cross-check per repeat. | +| Maven | 3.9.11 | | +| Hardware | 2 vCPU x86-64 VM | Same sandbox as the rest of this series. | + +## Quickstart + +```bash +export JAVA_HOME=/path/to/jdk-25 +mvn package +java -cp target/classes com.ankurm.arithmetic.OverflowWrapDemo +java -cp target/classes com.ankurm.arithmetic.ExactArithmeticDemo +``` + +`scripts/run-all.sh` regenerates every file in `output/` (needs `JDK25_HOME`). + +## What's in here + +| File | What it shows | +|---|---| +| `.../OverflowWrapDemo.java` | `int` and `long` addition and multiplication wrapping silently past their max values - no exception, just a wrong answer. | +| `.../ExactArithmeticDemo.java` | The `Math` `*Exact()` family: `addExact`, `subtractExact`, `multiplyExact`, `incrementExact`, `decrementExact`, `negateExact`, `toIntExact`, `absExact` - normal case and overflow-throws case for each, plus the `Math.abs(Integer.MIN_VALUE)` trap that `absExact` exists to catch. | +| `.../WideningAndBigIntegerDemo.java` | Widening to `long` fixes the `int`-range overflow (but `long` can still overflow); `BigInteger` never overflows at all. | +| `.../OverflowBitTrickDemo.java` | The XOR-based overflow test for addition, cross-checked against `Math.addExact()` on the same inputs. | +| `src/test/.../ArithmeticClaimsTest.java` | Asserts every claim above, including a 100,000-sample-per-repeat randomized cross-check of the bit-trick detector against `BigInteger` ground truth. | +| `output/01`-`04` | Each demo's real run. | +| `output/05` | JUnit correctness run. | + +## License + +MIT - see the [repo-wide LICENSE](../LICENSE). diff --git a/arithmetic/output/01-overflow-wrap.txt b/arithmetic/output/01-overflow-wrap.txt new file mode 100644 index 0000000..6038b2f --- /dev/null +++ b/arithmetic/output/01-overflow-wrap.txt @@ -0,0 +1,16 @@ +$ java OverflowWrapDemo + +=== int addition wraps silently past Integer.MAX_VALUE === +Integer.MAX_VALUE = 2147483647 +Integer.MAX_VALUE + 1 = -2147483648 (wrapped to Integer.MIN_VALUE, no exception) +Integer.MIN_VALUE = -2147483648 + +=== A realistic case: summing item prices in cents overflows int === +priceCents * quantity = 1800000000 * 2 = -694967296 (should be 3,600,000,000 - this is wrong, and no exception was thrown) + +=== Multiplication overflows just as silently as addition === +100000 * 100000 = 1410065408 (true value is 10,000,000,000) + +=== long has the same problem, just a much higher ceiling === +Long.MAX_VALUE = 9223372036854775807 +Long.MAX_VALUE + 1 = -9223372036854775808 (wrapped to Long.MIN_VALUE) diff --git a/arithmetic/output/02-exact-arithmetic.txt b/arithmetic/output/02-exact-arithmetic.txt new file mode 100644 index 0000000..cdc77dc --- /dev/null +++ b/arithmetic/output/02-exact-arithmetic.txt @@ -0,0 +1,24 @@ +$ java ExactArithmeticDemo + +=== addExact / subtractExact / multiplyExact: normal case === +Math.addExact(20, 22) = 42 +Math.subtractExact(50, 8) = 42 +Math.multiplyExact(6, 7) = 42 + +=== addExact / multiplyExact: overflow case - throws instead of wrapping === +Math.addExact(Integer.MAX_VALUE, 1) threw: integer overflow +Math.multiplyExact(100000, 100000) threw: integer overflow + +=== incrementExact / decrementExact / negateExact === +Math.incrementExact(41) = 42 +Math.decrementExact(43) = 42 +Math.negateExact(-42) = 42 +Math.incrementExact(Integer.MAX_VALUE) threw: integer overflow + +=== toIntExact: the safe narrowing cast from long back to int === +Math.toIntExact(2000000000L) = 2000000000 +Math.toIntExact(3000000000L) threw: integer overflow + +=== The classic trap: Math.abs(Integer.MIN_VALUE) does NOT throw, and is wrong === +Math.abs(Integer.MIN_VALUE) = -2147483648 (still negative! +2147483648 doesn't fit in an int) +Math.absExact(Integer.MIN_VALUE) threw: Overflow to represent absolute value of Integer.MIN_VALUE diff --git a/arithmetic/output/03-widening-and-biginteger.txt b/arithmetic/output/03-widening-and-biginteger.txt new file mode 100644 index 0000000..26e41a0 --- /dev/null +++ b/arithmetic/output/03-widening-and-biginteger.txt @@ -0,0 +1,13 @@ +$ java WideningAndBigIntegerDemo + +=== Widening to long fixes the int-range overflow from OverflowWrapDemo === +int: Integer.MAX_VALUE + 1 = -2147483648 (wrong) +long: (long) Integer.MAX_VALUE + 1 = 2147483648 (correct) + +=== Widening only raises the ceiling - long still overflows eventually === +Long.MAX_VALUE + 1 = -9223372036854775808 (wrapped to Long.MIN_VALUE - same bug, bigger numbers) + +=== BigInteger never overflows - it grows to fit the true value === +BigInteger.valueOf(Long.MAX_VALUE).add(ONE) = 9223372036854775808 +BigInteger.valueOf(2).pow(100) = 1267650600228229401496703205376 +(that value has no exact long or int representation at all) diff --git a/arithmetic/output/04-overflow-bit-trick.txt b/arithmetic/output/04-overflow-bit-trick.txt new file mode 100644 index 0000000..2d47bbe --- /dev/null +++ b/arithmetic/output/04-overflow-bit-trick.txt @@ -0,0 +1,15 @@ +$ java OverflowBitTrickDemo + +=== Hand-rolled overflow detector vs. known cases === +2000000000 + 2000000000 -> raw int result -294967296, overflow detected: true +2000000000 + -1000000000 -> raw int result 1000000000, overflow detected: false +2147483647 + 1 -> raw int result -2147483648, overflow detected: true +-2147483648 + -1 -> raw int result 2147483647, overflow detected: true +100 + 200 -> raw int result 300, overflow detected: false + +=== Cross-checked against Math.addExact() on the same inputs === +2000000000 + 2000000000: bit-trick says overflow=true, Math.addExact() agrees=true +2000000000 + -1000000000: bit-trick says overflow=false, Math.addExact() agrees=true +2147483647 + 1: bit-trick says overflow=true, Math.addExact() agrees=true +-2147483648 + -1: bit-trick says overflow=true, Math.addExact() agrees=true +100 + 200: bit-trick says overflow=false, Math.addExact() agrees=true diff --git a/arithmetic/output/05-correctness-tests.txt b/arithmetic/output/05-correctness-tests.txt new file mode 100644 index 0000000..a20b176 --- /dev/null +++ b/arithmetic/output/05-correctness-tests.txt @@ -0,0 +1,4 @@ +------------------------------------------------------------------------------- +Test set: com.ankurm.arithmetic.ArithmeticClaimsTest +------------------------------------------------------------------------------- +Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.438 s -- in com.ankurm.arithmetic.ArithmeticClaimsTest diff --git a/arithmetic/pom.xml b/arithmetic/pom.xml new file mode 100644 index 0000000..b01b7d2 --- /dev/null +++ b/arithmetic/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + com.ankurm + java-core-examples + 1.0 + + + arithmetic + arithmetic + Adding two numbers in Java without silent overflow: the wraparound bug reproduced, the Math *Exact() family, widening to long, BigInteger, and the bit trick Math.addExact itself uses to detect overflow. + + + + org.junit.jupiter + junit-jupiter + 5.11.0 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 25 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + diff --git a/arithmetic/scripts/run-all.sh b/arithmetic/scripts/run-all.sh new file mode 100644 index 0000000..e2c8e1c --- /dev/null +++ b/arithmetic/scripts/run-all.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Regenerates every file in ../output/ from a real run against $JDK25_HOME. +# Usage: JDK25_HOME=/path/to/jdk-25 ./run-all.sh +set -euo pipefail + +if [ -z "${JDK25_HOME:-}" ]; then + echo "Set JDK25_HOME to a JDK 25 install first." >&2 + exit 1 +fi + +cd "$(dirname "$0")/.." +OUT="output" +mkdir -p "$OUT" + +"$JDK25_HOME/bin/mvn" -q -f pom.xml package 2>/dev/null || true +CP="target/classes" +if [ ! -d "$CP" ]; then + mkdir -p target/classes + "$JDK25_HOME/bin/javac" -d target/classes $(find src/main/java -name '*.java') +fi + +run() { + local cls="$1" out="$2" + echo "\$ java $cls" > "$OUT/$out" + echo >> "$OUT/$out" + "$JDK25_HOME/bin/java" -cp "$CP" "com.ankurm.arithmetic.$cls" >> "$OUT/$out" 2>&1 +} + +run OverflowWrapDemo 01-overflow-wrap.txt +run ExactArithmeticDemo 02-exact-arithmetic.txt +run WideningAndBigIntegerDemo 03-widening-and-biginteger.txt +run OverflowBitTrickDemo 04-overflow-bit-trick.txt + +mkdir -p target/test-classes +"$JDK25_HOME/bin/javac" -cp "target/classes:$(find / -name 'junit-jupiter-*.jar' 2>/dev/null | tr '\n' ':')" \ + -d target/test-classes $(find src/test/java -name '*.java') 2>/dev/null || true + +"$JDK25_HOME/bin/mvn" -q -f pom.xml test > "$OUT/05-correctness-tests.txt" 2>&1 || true +cp target/surefire-reports/com.ankurm.arithmetic.ArithmeticClaimsTest.txt "$OUT/05-correctness-tests.txt" 2>/dev/null || true + +echo "Done. See $OUT/" diff --git a/arithmetic/src/main/java/com/ankurm/arithmetic/ExactArithmeticDemo.java b/arithmetic/src/main/java/com/ankurm/arithmetic/ExactArithmeticDemo.java new file mode 100644 index 0000000..123dd87 --- /dev/null +++ b/arithmetic/src/main/java/com/ankurm/arithmetic/ExactArithmeticDemo.java @@ -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()); + } + } +} diff --git a/arithmetic/src/main/java/com/ankurm/arithmetic/OverflowBitTrickDemo.java b/arithmetic/src/main/java/com/ankurm/arithmetic/OverflowBitTrickDemo.java new file mode 100644 index 0000000..12553db --- /dev/null +++ b/arithmetic/src/main/java/com/ankurm/arithmetic/OverflowBitTrickDemo.java @@ -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). + * + *

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); + } +} diff --git a/arithmetic/src/main/java/com/ankurm/arithmetic/OverflowWrapDemo.java b/arithmetic/src/main/java/com/ankurm/arithmetic/OverflowWrapDemo.java new file mode 100644 index 0000000..e291fc6 --- /dev/null +++ b/arithmetic/src/main/java/com/ankurm/arithmetic/OverflowWrapDemo.java @@ -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)"); + } +} diff --git a/arithmetic/src/main/java/com/ankurm/arithmetic/WideningAndBigIntegerDemo.java b/arithmetic/src/main/java/com/ankurm/arithmetic/WideningAndBigIntegerDemo.java new file mode 100644 index 0000000..4d5aa1d --- /dev/null +++ b/arithmetic/src/main/java/com/ankurm/arithmetic/WideningAndBigIntegerDemo.java @@ -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)"); + } +} diff --git a/arithmetic/src/test/java/com/ankurm/arithmetic/ArithmeticClaimsTest.java b/arithmetic/src/test/java/com/ankurm/arithmetic/ArithmeticClaimsTest.java new file mode 100644 index 0000000..610e58c --- /dev/null +++ b/arithmetic/src/test/java/com/ankurm/arithmetic/ArithmeticClaimsTest.java @@ -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); + } + } +} diff --git a/pom.xml b/pom.xml index 98c327c..af6da0c 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,7 @@ synchronizers executors concurrency-interview + arithmetic