skip to content
everything.coreApi.mathAndRandom

math and random numbers

Math, StrictMath, BigInteger, and RandomGenerator.

chapter
chapter 04.03
reading time
8 min read

Math is a box of static functions. the ones that save you from real bugs: the *Exact methods (overflow throws instead of wrapping) and floorMod (a remainder that's never negative). when 64 bits or binary fractions aren't enough, BigInteger and BigDecimal.

for randomness: RandomGenerator or ThreadLocalRandom for speed, SecureRandom for anything someone would like to guess. plain java.util.Random is predictable after two outputs.

the picture

Math is a calculator taped to the side of the JVM: Math.max, Math.sqrt, Math.pow, Math.sin, all static, all on primitives. there's nothing to construct.

its twin StrictMath has the same methods with one difference: StrictMath must give bit-for-bit the same answer on every machine (it's a port of a C library called fdlibm). Math is allowed to use whatever the CPU is good at, as long as it's within 1 ulp (one step between neighbouring doubles). that's not theoretical. on an arm mac with jdk 25, about 7% of random inputs give a different sin:

java
double x = 6.6543411191902235;
Math.sin(x)        // 0.3626927853276773
StrictMath.sin(x)  // 0.36269278532767724

use Math. reach for StrictMath only when two machines have to agree on the last bit (replays, lockstep simulations, test fixtures). (since java 17, plain + - * / on doubles are strict everywhere, JEP 306, so strictfp does nothing anymore.)

overflow: wrap or throw

int arithmetic wraps around silently. Integer.MAX_VALUE + 1 is Integer.MIN_VALUE, because the bits just roll over like an odometer:

Integer.MAX_VALUE21474836470x7fffffff
sign · 1 bitvalue · 31 bits
Integer.MAX_VALUE + 1-21474836480x80000000
sign · 1 bitvalue · 31 bits
the top bit is the sign. add 1 to the biggest int and the carry runs all the way up into it. java doesn't check, it just hands you a very negative number. the whole story is in casting and overflow.

when a wrong number is worse than a crash (money, sizes, indexes, ids), use the Exact methods. they do the same math and throw ArithmeticException the moment the result doesn't fit:

Exact.java
void main() {
    IO.println(Integer.MAX_VALUE + 1);                       // wraps, silently
    try {
        Math.addExact(Integer.MAX_VALUE, 1);                 // throws
    } catch (ArithmeticException e) {
        IO.println(e.getMessage());
    }
    try {
        int n = Math.toIntExact(3_000_000_000L);             // long → int, checked
    } catch (ArithmeticException e) {
        IO.println(e.getMessage());
    }
    IO.println(Math.clamp(150, 0, 100));                     // java 21+
}
output, from jdk 25
-2147483648
integer overflow
integer overflow
100

this one needs a real JVM (catching a runtime exception), so it doesn't run in the browser. copy it into java Main.java to try it.

the family: addExact, subtractExact, multiplyExact, incrementExact, negateExact, toIntExact, and absExact java 15+. the JIT turns them into a normal add plus one overflow-flag check, so they're close to free.

remainder vs floorMod

% in java is a remainder, not a modulo: it truncates toward zero, so the result has the sign of the left side. -7 % 3 is -1. that's fine for math, and terrible for anything you use as an index.

a =
-10
b =
0123456
-10 % 7-3
Math.floorMod(-10, 7)4
slots[-10 % 7] → ArrayIndexOutOfBoundsException: Index -3 out of bounds for length 7
drag a below zero. % goes negative and falls off the array. floorMod wraps around the dial like a clock and always lands on a real slot, 0 to b−1.

so for "which bucket", "which day of the week", "which slot in the ring buffer", use Math.floorMod(a, n). there's a matching floorDiv (rounds down, so floorDiv(-7, 3) is -3 where / gives -2), and since java 18, ceilDiv and ceilMod.

predict the output
java
IO.println(Math.round(-2.5) + " " + Math.round(2.5));

BigInteger and BigDecimal

BigInteger is an integer with as many digits as you need. it's immutable, so every operation returns a new one:

Big.java
import java.math.BigInteger;
 
void main() {
    BigInteger f = BigInteger.ONE;
    for (int i = 2; i <= 30; i++) f = f.multiply(BigInteger.valueOf(i));
    IO.println(f);                         // 30!, way past long's 19 digits
    IO.println(BigInteger.TWO.pow(100));
}
output
265252859812191058636308480000000
1267650600228229401496703205376

BigDecimal is the one you need for money. a double stores binary fractions, and 0.1 has no exact binary form (that's why 0.1 + 0.2 prints 0.30000000000000004, see floating point). a BigDecimal is an unscaled BigInteger plus a scale (how many digits after the point), so 0.1 is stored as 1 with scale 1. exact.

Money.java
import java.math.BigDecimal;
import java.math.RoundingMode;
 
void main() {
    IO.println(new BigDecimal(0.1));                    // the double's real value. oops
    var tenth = new BigDecimal("0.1");                  // from a string: exact
    IO.println(tenth + " " + tenth.add(new BigDecimal("0.2")));
    var a = new BigDecimal("2.0");
    var b = new BigDecimal("2.00");
    IO.println(a.equals(b) + " " + a.compareTo(b));     // equals also compares scale
    try {
        BigDecimal.ONE.divide(BigDecimal.valueOf(3));   // 0.333... forever
    } catch (ArithmeticException e) {
        IO.println(e.getMessage());
    }
    IO.println(BigDecimal.ONE.divide(BigDecimal.valueOf(3), 5, RoundingMode.HALF_EVEN));
    IO.println(new BigDecimal("2.345").setScale(2, RoundingMode.HALF_EVEN)
            + " " + new BigDecimal("2.355").setScale(2, RoundingMode.HALF_EVEN));
}
output, from jdk 25
0.1000000000000000055511151231257827021181583404541015625
0.1 0.3
false 0
Non-terminating decimal expansion; no exact representable decimal result.
0.33333
2.34 2.36

this one needs a real JVM (catching a runtime exception), so it doesn't run in the browser. copy it into java Main.java to try it.

three rules and you're fine: build them from strings (or BigDecimal.valueOf(double), which goes through the double's shortest string), compare with compareTo (not equals, which thinks 2.0 and 2.00 differ, which also makes them two different keys in a HashMap), and always say how to round when you divide.

random numbers

since java 17 (JEP 356) there's one interface for all of them, java.util.random.RandomGenerator, and a menu of algorithms behind it:

algorithmstateperiodnotes
Random48 bits248the java 1.0 one. thread-safe through a CAS, which makes it slow when shared
SplittableRandom64 bits264split() gives an independent child, made for fork/join
L32X64MixRandom96 bits≈ 296what RandomGenerator.getDefault() returns in jdk 25
L64X128MixRandom192 bits≈ 2192a good pick for simulations, splittable
Xoshiro256PlusPlus256 bits2256 − 1jumpable: skip ahead 2128 steps to hand out non-overlapping streams
SecureRandomOS entropyn/aunpredictable. for keys, tokens, passwords, session ids

(from RandomGeneratorFactory.all() on jdk 25.) in practice:

java
var rnd = RandomGenerator.getDefault();               // fast, good quality, not thread-safe
int die = rnd.nextInt(1, 7);                          // 1..6, origin inclusive, bound exclusive
int roll = ThreadLocalRandom.current().nextInt(1, 7); // from any thread, no sharing
var seeded = RandomGenerator.of("L64X128MixRandom");  // pick an algorithm by name

and never Math.abs(rnd.nextInt()) % 6: that's biased toward small numbers, and it's the Math.abs bug from above. nextInt(bound) does it right.

Random is not a secret

java.util.Random is a linear congruential generator: the next state is state * 0x5DEECE66D + 0xB, keep the low 48 bits. nextInt() returns the top 32 bits of the state. so every number you print leaks two thirds of the state, and the other 16 bits are 65,536 guesses away.

Server.java
  1. 1var rnd = new Random(); // seeded from the clock. you can't see it.
  2. 2int a = rnd.nextInt(); // printed in a url, a game, a "random" id...
  3. 3int b = rnd.nextInt();
what you saw
press new Random()
predicted from those two
·
what Random actually returns
·

this is java.util.Random's exact algorithm, running in your browser. the seed comes from your browser's crypto api, so even this page doesn't know it.

press new Random(), then predict. this is java.util.Random, ported bit for bit (it matches jdk 25 for every seed we tried). two outputs are enough to know every number it will ever produce, and the real calls come out identical.

that's fine for a dice game and a disaster for a password reset token. for anything security-shaped, use SecureRandom, which pulls from the OS's entropy source and can't be rewound like this. more in security and crypto.

don't share it, don't recreate it

ThreadLocalRandom15 ms
one shared Random6,800 ms
new Random() per call11,000 ms
log scale
8 threads × 5 million nextInt() each, temurin 25 on a 14-core M4 Pro. a shared Random serializes every thread on one atomic seed. new Random() per call is worse: the constructor updates a global atomic too, so you get the contention and the allocation.

even on one thread, new Random() per call took 600 ms for 40 million numbers vs 25 ms for ThreadLocalRandom. and seeding with the clock yourself is its own bug: two Randoms seeded with the same currentTimeMillis() produce identical sequences. make one generator and keep it (one per thread, or ThreadLocalRandom).

keylines
  • Math may differ from StrictMath in the last bit. that's allowed, and it happens.
  • overflow wraps silently. addExact and friends throw instead.
  • % keeps the sign of the left side. for indexes, floorMod.
  • Math.abs(Integer.MIN_VALUE) is negative.
  • BigDecimal: build from strings, compare with compareTo, always pass a rounding mode to divide.
  • java.util.Random is predictable and slow when shared. ThreadLocalRandom for speed, SecureRandom for secrets.