Math & Random
The Math class, BigInteger/BigDecimal for arbitrary precision, and Random/SecureRandom for pseudo-randomness cover Java's numeric toolbox beyond primitive arithmetic — each with sharp edges worth knowing.
Java's built-in numeric arithmetic (int, long, double) is fast but limited: primitives overflow silently, floating-point cannot represent most decimal fractions exactly, and java.util.Random is not suitable for security purposes. The java.lang.Math class provides common mathematical functions; java.math.BigInteger and BigDecimal provide arbitrary-precision arithmetic; and java.util.Random / java.security.SecureRandom provide two very different flavors of randomness for very different use cases.
The Math Class: Common Numeric Operations
java.lang.Math is a final class of static methods for common mathematical operations: abs(), max(), min(), pow(), sqrt(), floor(), ceil(), round(), and trigonometric/logarithmic functions among many others. All its methods are stateless and operate on primitive numeric types.
Math.pow/sqrt/etc. work in double precision and return double, so results are subject to the same floating-point representation limits as any other double arithmetic.
System.out.println(Math.max(3, 7)); // 7
System.out.println(Math.pow(2, 10)); // 1024.0
System.out.println(Math.sqrt(2)); // 1.4142135623730951
System.out.println(Math.floor(4.7)); // 4.0
System.out.println(Math.ceil(4.1)); // 5.0
System.out.println(Math.round(4.5)); // 5 (rounds half-up for positive values)
System.out.println(Math.abs(-9)); // 9Overflow-Safe Arithmetic: addExact, multiplyExact, etc.
Regular int/long arithmetic silently wraps around on overflow rather than signaling an error, which can produce badly wrong results without any indication something went wrong. Math provides *Exact variants (addExact, subtractExact, multiplyExact, incrementExact, negateExact, toIntExact, and more) that throw ArithmeticException on overflow instead of silently wrapping — valuable whenever silent overflow would be a correctness bug rather than an intended wraparound.
int a = Integer.MAX_VALUE;
System.out.println(a + 1); // -2147483648, silent overflow
try {
Math.addExact(a, 1); // throws ArithmeticException instead
} catch (ArithmeticException e) {
System.out.println("Overflow detected: " + e.getMessage());
}Floating-Point Limitations
double and float use binary floating-point representation (IEEE 754), which cannot exactly represent most decimal fractions — 0.1 in binary floating point is actually a repeating binary fraction, stored as the closest representable approximation. This is why 0.1 + 0.2 famously does not equal 0.3 exactly in double arithmetic, and it is a fundamental property of binary floating-point representation, not a Java-specific bug.
For monetary or exact-decimal calculations, avoid double/float entirely and use BigDecimal instead (see the BigDecimal topic) — this is not a corner case, it is the correct general rule.
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 - safer comparison with a toleranceBigInteger: Arbitrary-Precision Integers
BigInteger represents integers of unbounded size (limited only by available memory), useful when a computation could exceed long's 64-bit range — cryptography, combinatorics, exact factorial/large-number computations. Like String, BigInteger is immutable: every arithmetic method returns a new BigInteger rather than modifying the receiver.
import java.math.BigInteger;
BigInteger a = new BigInteger("123456789012345678901234567890");
BigInteger b = BigInteger.valueOf(2);
BigInteger product = a.multiply(b);
System.out.println(product);
BigInteger factorial20 = BigInteger.ONE;
for (int i = 2; i <= 20; i++) {
factorial20 = factorial20.multiply(BigInteger.valueOf(i));
}
System.out.println(factorial20); // 2432902008176640000 -- fits, but 25! would overflow longBigDecimal: Arbitrary-Precision Decimals (Overview)
BigDecimal represents an arbitrary-precision signed decimal number as an unscaled integer value plus a scale (number of digits after the decimal point), giving it exact decimal representation unlike double/float. It is the correct choice for monetary calculations and anywhere exact decimal arithmetic matters. BigDecimal is covered in full depth, including rounding modes and the equals()-versus-compareTo() pitfall, in its own dedicated topic.
import java.math.BigDecimal;
BigDecimal price = new BigDecimal("19.99");
BigDecimal quantity = new BigDecimal("3");
BigDecimal total = price.multiply(quantity);
System.out.println(total); // 59.97 exactly, no floating-point driftRounding: Concepts and Common Approaches
Math.round() rounds a double/float to the nearest long/int, rounding half-values up (toward positive infinity) — not the 'round half to even' behavior some contexts expect. For control over rounding behavior on BigDecimal, RoundingMode constants (HALF_UP, HALF_EVEN, DOWN, CEILING, FLOOR, etc.) let you specify exactly how a value should round, which matters a great deal for financial calculations where the exact rounding rule can be a business/legal requirement.
import java.math.BigDecimal;
import java.math.RoundingMode;
BigDecimal value = new BigDecimal("2.345");
System.out.println(value.setScale(2, RoundingMode.HALF_UP)); // 2.35
System.out.println(value.setScale(2, RoundingMode.HALF_EVEN)); // 2.35 (nearest even at this scale)
System.out.println(value.setScale(2, RoundingMode.DOWN)); // 2.34java.util.Random: Pseudo-Random Numbers
java.util.Random generates pseudo-random numbers from a deterministic algorithm seeded by a starting value; two Random instances created with the same seed produce the identical sequence of values, which makes Random reproducible (useful for tests/simulations) but also predictable — an attacker who can observe some outputs or knows the seed can predict future ones. It provides nextInt(), nextInt(bound), nextLong(), nextDouble(), nextBoolean(), and nextGaussian() (normal distribution).
import java.util.Random;
Random rng = new Random(42); // fixed seed -> reproducible sequence, useful for tests
System.out.println(rng.nextInt(100)); // deterministic given the seed
System.out.println(rng.nextInt(100));
System.out.println(rng.nextBoolean());
System.out.println(rng.nextDouble()); // in [0.0, 1.0)SecureRandom: Cryptographically Strong Randomness
java.security.SecureRandom produces randomness suitable for security-sensitive purposes — session tokens, cryptographic keys, password reset codes, salts. It draws entropy from OS-level sources designed to be unpredictable even to an attacker who has observed prior outputs, unlike Random's fully deterministic algorithm. SecureRandom is slower than Random and that cost is the price of its unpredictability guarantee.
Never use plain java.util.Random for anything security-sensitive — session tokens, password reset codes, cryptographic material — its outputs are predictable to an attacker who can observe or infer enough of the sequence.
import java.security.SecureRandom;
SecureRandom secureRng = new SecureRandom();
byte[] token = new byte[16];
secureRng.nextBytes(token);
// encode 'token' (e.g. Base64) to use as a session ID or similarChoosing the Right Numeric/Random API
Use int/long/double for ordinary arithmetic where their range and precision suffice and performance matters. Use Math's *Exact methods when silent overflow would be a correctness bug. Use BigInteger when values may exceed 64-bit range. Use BigDecimal for money or any calculation requiring exact decimal results. Use Random for simulations, games, and tests where reproducibility and speed matter more than unpredictability; use SecureRandom whenever the output must not be predictable by an adversary.
Need Use
Fast general arithmetic int / long / double
Overflow must be detected Math.addExact / multiplyExact / ...
Integer beyond 64-bit range BigInteger
Exact decimal (money, etc.) BigDecimal
Reproducible pseudo-randomness Random(seed)
Security-sensitive randomness SecureRandomComplete Example: Combining the Toolbox
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.security.SecureRandom;
import java.util.Base64;
public class OrderProcessor {
public static void main(String[] args) {
BigDecimal unitPrice = new BigDecimal("14.995");
BigDecimal quantity = new BigDecimal("3");
BigDecimal total = unitPrice.multiply(quantity).setScale(2, RoundingMode.HALF_UP);
System.out.println("Total: $" + total);
SecureRandom secureRng = new SecureRandom();
byte[] orderIdBytes = new byte[8];
secureRng.nextBytes(orderIdBytes);
String orderId = Base64.getUrlEncoder().withoutPadding().encodeToString(orderIdBytes);
System.out.println("Order ID: " + orderId);
}
}Mental Model
Think of int/long/double as fast, fixed-size number containers that trade exactness/range for speed; BigInteger/BigDecimal trade some speed for exactness and unbounded range/precision. Similarly, Random trades unpredictability for speed and reproducibility, while SecureRandom trades speed for genuine unpredictability — pick the tool based on which trade-off your use case actually needs.
Key Takeaways
Math provides common numeric functions plus *Exact variants that throw on overflow instead of silently wrapping.
double/float use binary floating-point and cannot represent most decimal fractions exactly — a fundamental limitation, not a bug.
BigInteger gives unbounded-range integer arithmetic; BigDecimal gives exact decimal arithmetic with explicit scale and rounding control.
java.util.Random is deterministic given a seed — reproducible but predictable, and unsuitable for security purposes.
SecureRandom draws from unpredictable entropy sources and must be used for tokens, keys, and any security-sensitive randomness.
Choosing the right numeric/random API is a deliberate trade-off between speed, range, exactness, and predictability.
double total = 19.99 * 3; // 59.96999999999999, not exactly 59.97
BigDecimal total = new BigDecimal("19.99").multiply(new BigDecimal("3")); // exactly 59.97 double cannot represent most decimal fractions exactly; monetary or exact-decimal computations should use BigDecimal, not double/float.
String sessionToken = String.valueOf(new Random().nextLong()); // predictable
SecureRandom secureRng = new SecureRandom(); byte[] tokenBytes = new byte[16]; secureRng.nextBytes(tokenBytes); // unpredictable, suitable for security use java.util.Random is a deterministic, seed-based algorithm whose future outputs can be predicted by an attacker who observes enough prior outputs; security-sensitive randomness must use SecureRandom.
int result = Integer.MAX_VALUE + 1; // silently wraps to a negative number, no warning int result = Math.addExact(Integer.MAX_VALUE, 1); // throws ArithmeticException, surfacing the bug
Primitive int/long arithmetic overflows silently; use Math's *Exact methods wherever silent wraparound would represent a correctness bug you want to catch.
if (0.1 + 0.2 == 0.3) {... } // false due to floating-point representation if (Math.abs((0.1 + 0.2) - 0.3) < 1e-9) {... } // or use BigDecimal for exact comparisons
Direct equality comparison of computed doubles is unreliable due to floating-point rounding; use a tolerance-based comparison, or avoid double entirely when exactness matters.
Why does 0.1 + 0.2 not equal 0.3 exactly in Java?
double uses binary (IEEE 754) floating-point representation, which cannot exactly represent most decimal fractions like 0.1 or 0.2, so the computed sum is the closest representable approximation, not exactly 0.3.
What is the difference between Math.addExact(a, b) and a + b for ints?
a + b silently wraps around on overflow with no indication anything went wrong; Math.addExact throws ArithmeticException if the true mathematical result would not fit in an int, surfacing the overflow as an error.
When would you reach for BigInteger instead of long?
When a computation's values may exceed long's 64-bit range (roughly ±9.2 x 10^18), such as large factorials, cryptographic computations, or combinatorics on large inputs.
Why is java.util.Random unsuitable for generating session tokens or cryptographic keys?
Random is a deterministic pseudo-random number generator seeded from a starting value; given enough observed outputs (or the seed), its future outputs are predictable, which defeats the unpredictability security tokens require.
What entropy source difference distinguishes SecureRandom from Random?
SecureRandom draws from OS-level sources of unpredictable entropy designed to resist prediction even by an attacker who has seen prior outputs, while Random is a fully deterministic algorithm driven only by its seed.
How does Math.round() handle rounding for positive half-values like 4.5?
It rounds half-values up, toward positive infinity, so Math.round(4.5) returns 5 — not 'round half to even' behavior some other rounding contexts use.
Why might Random be preferable to SecureRandom for a game or simulation?
Random is faster and can be seeded for reproducible sequences (useful for deterministic tests or replayable simulations), and unpredictability is not a security requirement in that context.
Write a program that computes 30! using BigInteger, since it overflows long.
import java.math.BigInteger; public class FactorialBig { public static void main(String[] args) { BigInteger result = BigInteger.ONE; for (int i = 2; i <= 30; i++) { result = result.multiply(BigInteger.valueOf(i)); } System.out.println(result); } }
30! is far larger than long can hold (about 2.65 x 10^32), so BigInteger's unbounded-precision multiply() accumulates the exact result without overflow.
What does this print, and why?
2147483647, then -2147483648 — adding 1 to Integer.MAX_VALUE overflows the 32-bit int range and silently wraps around to the most negative int value, with no exception thrown.
Write a program that generates a cryptographically secure random 6-digit numeric code (for something like a verification code) using SecureRandom.
import java.security.SecureRandom; public class VerificationCodeGenerator { public static void main(String[] args) { SecureRandom secureRng = new SecureRandom(); int code = secureRng.nextInt(1_000_000); // 0..999999 System.out.printf("Verification code: %06d%n", code); } } secureRng.nextInt(1_000_000) yields a uniformly distributed value from 0 to 999999 drawn from an unpredictable entropy source; %06d zero-pads it to always display 6 digits.
System.out.println(Integer.MAX_VALUE);
System.out.println(Integer.MAX_VALUE + 1);