Skip to main content

Add Two Numbers in Java Without Overflow: addExact, Widening, and BigInteger

Adding two integers looks trivial until the sum overflows silently. This guide covers five quick ways to add, then goes deep: the wraparound reproduced with real numbers, Math.addExact and the exact-arithmetic family, widening to long, BigInteger, and the bit trick addExact itself uses.

Adding two integers is the “Hello, World” of Java arithmetic — but a truly useful guide has to cover what happens when the answer doesn’t fit. a + b on two ints never throws when the true sum overflows. It wraps around silently, two’s-complement style, and your program keeps running with a wrong number and no warning. This post covers five quick ways to add two integers, then goes deep on the part that actually causes production bugs: the wraparound reproduced with real numbers, the full Math “exact” family that throws instead of wrapping, widening to long, BigInteger for numbers with no ceiling at all, and the exact bit trick the JDK’s own Math.addExact uses to detect overflow.

Versions. JDK 25.0.4.1+1 (Temurin, LTS). Every API used here is fully finalized — no preview flags needed. JUnit 5.11.0 for the correctness tests, including a 100,000-sample-per-repeat randomized cross-check. On a 2-vCPU x86-64 virtual machine.

Five quick ways to add two integers

The mechanics first, briefly — then the part that actually matters.

// 1. Hardcoded literals
int sum = 15 + 27;                        // 42

// 2. Scanner console input
Scanner scanner = new Scanner(System.in);
int sum2 = scanner.nextInt() + scanner.nextInt();

// 3. Command-line arguments
int sum3 = Integer.parseInt(args[0]) + Integer.parseInt(args[1]);

// 4. A reusable method
static int add(int a, int b) { return a + b; }

// 5. Streams
int total = List.of(10, 20, 30).stream().mapToInt(Integer::intValue).sum();

All five compile, all five run, and all five have the exact same blind spot: none of them check whether the true sum actually fits in an int.

The overflow nobody checks for

Java’s int is a signed 32-bit type: −2,147,483,648 to 2,147,483,647. Add past the top of that range and the result wraps around to a negative number — silently, with no exception, no warning, nothing in the return type that distinguishes a correct sum from a wrapped one. This isn’t a contrived edge case; it’s what happens the moment two large, perfectly ordinary numbers get added:

$ 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)

Output: 01-overflow-wrap.txt, source: OverflowWrapDemo.java. Notice the third block: 100000 * 100000 is a completely unremarkable multiplication, and it’s already wrong by nine digits. long pushes the ceiling much higher but doesn’t remove the problem — it just makes it rarer, which arguably makes it worse, because the bug shows up years later against a number nobody thought to test.

Math’s exact-arithmetic family — the real fix

Since Java 8, java.lang.Math has carried a parallel “exact” version of every basic arithmetic operation: addExact, subtractExact, multiplyExact, incrementExact, decrementExact, negateExact, and toIntExact (the safe narrowing cast from long back to int). Each one does the ordinary operation when the true result fits the return type, and throws ArithmeticException instead of wrapping when it doesn’t — turning a silent wrong answer into a loud, immediate failure at the exact line that caused it.

$ 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

Output: 02-exact-arithmetic.txt, source: ExactArithmeticDemo.java. The last block is the one worth remembering: Math.abs(Integer.MIN_VALUE) is not part of the exact family, does not throw, and returns Integer.MIN_VALUE right back — still negative, because positive 2,147,483,648 has no representation in a 32-bit int. Math.absExact exists specifically to catch that one case where plain abs() silently lies.

This is the same territory Java 8 opened up with the Math class’s other precision-focused additions — getExponent, nextUp/nextDown, ulp, and scalb for floating-point work, alongside floorDiv/floorMod for integer division that rounds toward negative infinity instead of truncating toward zero. Later JDKs kept extending the same idea: ceilDiv/ceilMod for the rounding-toward-positive-infinity counterpart, and clamp for bounding a value into a range in one call. The throughline across all of them is the same principle demonstrated above — make the edge case impossible to miss instead of silently wrong.

Widening to long, and where that stops helping

The cheapest fix for an int-range overflow is to widen to long before adding, not after:

$ 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)

Output: 03-widening-and-biginteger.txt, source: WideningAndBigIntegerDemo.java. Note the ordering in the first block: (long) a + 1 casts a to long before the addition runs, so the addition itself happens at 64-bit width. Writing (long) (a + 1) instead would compute the wrong int result first and only widen the already-broken answer — a genuinely common mistake.

BigInteger: when you need no ceiling at all

long only raises the ceiling — it’s still a fixed 64-bit width with its own overflow point, shown in the second block above. BigInteger removes the ceiling entirely: it’s backed by a growable internal representation, so 2100 (a 34-digit number with no exact int or long representation) comes out exactly right, every time, with no overflow condition to check for in the first place. The cost is real — every BigInteger operation allocates a new immutable object and runs slower than primitive arithmetic — which is why it’s the right tool for genuinely unbounded values (cryptography, arbitrary-precision financial totals, factorials of large n) and the wrong tool for a hot loop that fits comfortably in a long.

How Math.addExact actually detects overflow

It’s worth knowing what addExact is actually checking, rather than treating it as a black box. For int x + int y = result, overflow occurred if and only if x and y have the same sign and that sign differs from result‘s sign — two numbers with different signs can never overflow an addition (their sum always lands between them), and two numbers with the same sign can only overflow into the opposite sign. Expressed without branching on sign at all:

static boolean additionOverflows(int x, int y) {
    int result = x + y;                          // allowed to wrap
    return ((x ^ result) & (y ^ result)) < 0;    // true iff x AND y both differ in sign from result
}
$ 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

Output: 04-overflow-bit-trick.txt, source: OverflowBitTrickDemo.java. This isn’t a novel trick invented for this post — it’s functionally the same check the JDK’s own Math.addExact source uses internally, computing the wrapped result once and testing sign relationships on it rather than doing the addition twice at different widths. The test suite below cross-checks this exact detector against BigInteger ground truth across 100,000 random int pairs per run, not just the five hand-picked cases shown here.

int vs long vs BigInteger: when to use which

TypeSizeRangeOverflow behaviorUse when
int32 bits−2.1B to 2.1BWraps silentlyTypical counts, indices, values you’ve bounded yourself
long64 bits−9.2 quintillion to 9.2QWraps silently, much laterTimestamps, file sizes, financial totals in the smallest unit
BigIntegerArbitraryUnlimitedNever overflowsCryptography, arbitrary-precision totals, factorial of large n

Correctness tests

Every claim above is backed by a JUnit assertion, not just a demo transcript — including a 100,000-iteration-per-repeat randomized comparison of the hand-rolled bit-trick detector against BigInteger-computed ground truth:

Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.438 s -- in com.ankurm.arithmetic.ArithmeticClaimsTest

Output: 05-correctness-tests.txt, source: ArithmeticClaimsTest.java.

See Also

Conclusion

Adding two integers in Java can be as simple as a + b — but a solid understanding of the operator means knowing that it never checks its own answer. The real toolkit for getting it right: Math.addExact and the rest of the exact-arithmetic family when you want a loud failure instead of a silent one, widening to long when you know your numbers fit comfortably under its much higher ceiling, and BigInteger when there’s no ceiling you’re willing to assume at all.

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.