Why do `int` arithmetic and `double` arithmetic produce surprising results (silent overflow, lost precision), and how should you guard against them?
answer
- int wraps mod 2^32: MAX_VALUE+1 -> MIN_VALUE (silent)
- Math.addExact/multiplyExact throw on overflow
- 0.1 + 0.2 != 0.3 (IEEE 754 binary)
- never == on doubles; compare within epsilon
- BigDecimal from String, not from double
- midpoint: low + (high - low) / 2
basics
~20 sWhole-number math wraps around silently when it gets too big (e.g. a huge int can suddenly go negative). Decimal math (double) can't represent some values exactly, so 0.1 + 0.2 isn't exactly 0.3. Use bigger types, Math.*Exact, or BigDecimal to stay safe.
solid answer
~40 sJava's integer types have a fixed width and use two's-complement, so arithmetic wraps around modulo 2^width without any error: Integer.MAX_VALUE + 1 becomes Integer.MIN_VALUE. This silently corrupts things like sums, hash codes, and time-in-millis calculations. To detect it, use Math.addExact/multiplyExact (they throw ArithmeticException on overflow), promote to long, or use BigInteger. Floating-point double/float are IEEE 754 binary fractions, so decimal values like 0.1 have no exact representation; 0.1 + 0.2 yields 0.30000000000000004, and you must never compare doubles with == (compare within an epsilon). For money or any exact decimal arithmetic, use BigDecimal (constructed from String, not double, to avoid inheriting the binary error). A frequent overflow bug is computing a midpoint as (low + high) / 2, where low + high can overflow; write low + (high - low) / 2 instead.
code
java · 17 lines// Silent integer overflow
System.out.println(Integer.MAX_VALUE + 1); // -2147483648 (wraps)
// System.out.println(Math.addExact(Integer.MAX_VALUE, 1)); // throws ArithmeticException
long safe = (long) Integer.MAX_VALUE + 1; // 2147483648 (widen first)
// Floating-point precision
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3); // false
System.out.println(Math.abs((0.1 + 0.2) - 0.3) < 1e-9); // true
// Exact decimal
import java.math.BigDecimal;
System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2"))); // 0.3
// Safe midpoint (no overflow)
int low = 2_000_000_000, high = 2_100_000_000;
int mid = low + (high - low) / 2; // correct, no overflowgo deeper
Recognizes that very large int math can give weird negatives and that decimals like 0.1 aren't exact.
Explains two's-complement wraparound and IEEE 754 approximation, avoids == on doubles, and knows to use long/BigDecimal.
Reaches for Math.*Exact, widens before multiplying, uses BigDecimal-from-String for money, and knows the midpoint-overflow and accumulation pitfalls.
Establishes org-wide numeric conventions (money type, overflow policy, rounding modes) and weighs performance vs correctness of BigDecimal/long-cents at the system level.
## Two separate hazards Arithmetic 'surprises' in Java come from two unrelated facts: integers have **finite width and wrap**, and floating-point numbers are **binary approximations**. Different fixes apply to each. ## Hazard 1: integer overflow (silent wraparound) A Java `int` is exactly 32 bits and uses **two's-complement** representation, giving a range of `-2^31 .. 2^31 - 1` (`Integer.MIN_VALUE .. Integer.MAX_VALUE`). When a computation exceeds that range, Java does **not** throw — it keeps only the low 32 bits, which is equivalent to wrapping **modulo 2^32**: ``` Integer.MAX_VALUE + 1 == Integer.MIN_VALUE // 2147483647 + 1 -> -2147483648 ``` This 'corrupts' results silently. Real-world bites: summing many large numbers, `a * b` for large `a, b`, `hashCode` math, and time arithmetic like `seconds * 1000` (overflows an `int` after ~24 days of milliseconds). **Defenses:** - **Use a wider type up front** (`long` is 64 bits) when values can be large: `long ms = (long) seconds * 1000;` — note the cast must be on an operand *before* the multiply, or the int multiply overflows first. - **Fail loudly** with `Math.addExact`, `Math.subtractExact`, `Math.multiplyExact`, `Math.incrementExact`, etc. — these throw `ArithmeticException` on overflow instead of wrapping. - **Use `BigInteger`** for unbounded integer math. - **The midpoint bug**: `(low + high) / 2` can overflow when `low + high` exceeds the type's max (famous binary-search bug). Use `low + (high - low) / 2`, or `(low + high) >>> 1` for non-negative ints. ## Hazard 2: floating-point precision (binary fractions) `float` (32-bit) and `double` (64-bit) follow **IEEE 754**: a number is stored as sign x mantissa x 2^exponent. Because the base is **2**, only fractions whose denominators are powers of two are exact. Decimal fractions like `0.1`, `0.2`, `0.3` are **not** exactly representable, so they're stored as the nearest binary value: ``` 0.1 + 0.2 == 0.30000000000000004 // not 0.3 ``` Consequences: - **Never use `==` on floating-point results.** Compare with a tolerance: `Math.abs(a - b) < 1e-9`. - **Rounding error accumulates** over many operations. - **Large + small loses the small one** (`1e16 + 1 == 1e16`): the small value falls below the representable precision at that magnitude. - Special values exist: `+/-Infinity`, `NaN` (and `NaN != NaN`, so test with `Double.isNaN`). There is also a signed zero (`0.0` vs `-0.0`). **Defenses:** - **For money / exact decimals, use `BigDecimal`** — arbitrary-precision decimal arithmetic. Construct it from a **String** (`new BigDecimal("0.1")`), never from a `double` (`new BigDecimal(0.1)` already carries the binary error). Specify a `MathContext`/scale and `RoundingMode` for division. - **Scale to integers** when feasible (store cents as `long`). - Use `double` for measurements/physics where small relative error is fine, not for exact accounting. ## Bonus: integer division already lost the fraction Separate from overflow, `int` division truncates, so `double r = 1 / 3;` is `0.0`. That's a *type* mistake, not a precision limit — promote an operand: `1.0 / 3`. ## How to talk about it Name the two hazards, give one example each (`MAX_VALUE + 1` wraps; `0.1 + 0.2 != 0.3`), and name the matching tools (`Math.*Exact`/`long`/`BigInteger` for overflow; epsilon-compare/`BigDecimal`-from-String for precision). That demonstrates senior-level numeric literacy.
- Why is `new BigDecimal(0.1)` discouraged in favor of `new BigDecimal("0.1")`?The double literal 0.1 is already the nearest binary approximation, so the double constructor faithfully copies that imprecise value (0.1000000000000000055...). The String constructor parses the exact decimal you wrote, giving precisely 0.1.
- How can `seconds * 1000` produce a wrong result even though both fit in an int?The multiplication is performed in int, and the product can exceed Integer.MAX_VALUE and wrap to a negative or wrong value. Cast an operand to long first: (long) seconds * 1000, so the multiply happens in 64-bit arithmetic.
saying these in an interview costs you the question
- Assuming integer overflow throws an exception in Java
- Comparing doubles with ==
- Using double for monetary amounts
- Constructing BigDecimal from a double literal
- Writing (low + high) / 2 in binary search
- Casting to long after the int multiply already overflowed