skip to content

How do you generate a random number in a range without bias, and how do modern Java APIs help?

level: middleimportance: should knowfreq 35%

answer

  1. avoid rng.nextInt() % n (modulo bias)
  2. nextInt(bound) uses rejection sampling -> uniform
  3. nextInt(origin, bound) = [origin, bound) half-open
  4. ints/longs/doubles streams for bulk
  5. RandomGenerator (Java 17) unifies the API

basics

~10 s

Use the built-in bounded methods like nextInt(bound) or nextInt(origin, bound) instead of doing 'rng.nextInt() % range'. The modulo trick can make some numbers come up slightly more often.

solid answer

~40 s

Don't compute ranges with modulo on a raw value (rng.nextInt() % n) — unless n divides the value space evenly, some remainders occur slightly more often, giving modulo bias. Use the library's bounded methods, which handle this for you: Random.nextInt(bound) returns [0, bound) uniformly via rejection of the skewed top slice; Java 17's nextInt(origin, bound) and equivalents on ThreadLocalRandom give an arbitrary half-open range. For producing many values, use the stream methods (ints(count, origin, bound), doubles(...)) which are convenient and lazy. The same range methods exist on Random, ThreadLocalRandom, and SecureRandom because they share the RandomGenerator interface (since Java 17). Pick the generator by purpose (security/concurrency/general) and let these methods handle uniformity.

go deeper

for a junior

Uses nextInt(bound) and knows the upper bound is exclusive.

for a middle

Explains modulo bias and prefers the bounded/origin-bound methods; uses stream methods for bulk.

for a senior

Describes rejection sampling, the Integer.MIN_VALUE/abs pitfall, and which overloads arrived in Java 17.

for a principal

Knows the RandomGenerator unification and guides API choice/uniformity guarantees across generators in team standards.

### The trap: modulo bias A naive way to get a number in `[0, n)` is `Math.abs(rng.nextInt()) % n`. The problem is **modulo bias**. `nextInt()` yields one of 2^32 equally likely values. If `n` does **not** divide 2^32 evenly, then when you take `% n`, the lower remainders get one extra representative each, so they come up *slightly* more often than the higher ones. For small ranges the skew is tiny; for large ranges relative to the value space it can be significant. Either way, it's an avoidable correctness bug, and `Math.abs(Integer.MIN_VALUE)` is itself negative — another lurking bug. ### The fix: bounded library methods The JDK's bounded methods do this correctly: ```java int d = rng.nextInt(6); // [0, 6) uniform int dice = rng.nextInt(1, 7); // [1, 7) i.e. 1..6 (Java 17+ on Random; always on ThreadLocalRandom) double u = rng.nextDouble(0, 1); // [0.0, 1.0) ``` Internally `nextInt(bound)` uses **rejection sampling**: it discards the small unevenly-distributed top slice of values and retries, guaranteeing a uniform result over `[0, bound)`. You get correctness for free, so there's essentially never a reason to hand-roll modulo. ### Arbitrary ranges with origin/bound The `(origin, bound)` overloads give a half-open interval `[origin, bound)`. `ThreadLocalRandom` has had these since Java 7; `java.util.Random` gained them in Java 17 via the shared `RandomGenerator` interface. They cover ints, longs, and doubles. ### Bulk generation with streams When you need many values, the **stream** methods are clean and lazy: ```java int[] rolls = ThreadLocalRandom.current() .ints(10, 1, 7) // 10 values in [1, 7) .toArray(); rng.doubles(1000).average(); ``` These return `IntStream`/`LongStream`/`DoubleStream`, so you can map/filter/collect. ### Unified API since Java 17 Java 17 introduced the `RandomGenerator` interface (and `RandomGeneratorFactory`). `Random`, `ThreadLocalRandom`, and `SecureRandom` all implement it, so the bounded and stream methods look the same across them — you choose the *implementation* by purpose (security -> SecureRandom, concurrency -> ThreadLocalRandom, else -> Random) and use the same uniform-range methods on whichever you picked. ### Takeaways - Never use `% n` for ranges; use `nextInt(bound)` / `nextInt(origin, bound)`. - Half-open `[origin, bound)` excludes the upper end. - Use `ints/longs/doubles` streams for bulk. - The range API is uniform across the three generators thanks to `RandomGenerator`.

  • Why does nextInt(6) not suffer modulo bias?
    It uses rejection sampling: it discards the small skewed top portion of the raw value range and retries, so every value in [0, 6) is equally likely.
  • What range does nextInt(1, 7) cover?
    The half-open interval [1, 7) — integers 1 through 6 inclusive, suitable for a six-sided die.

saying these in an interview costs you the question

  • Using % to bound a random value and assuming it's uniform
  • Forgetting the bound is exclusive (off-by-one)
  • Math.abs(nextInt()) being negative for Integer.MIN_VALUE
  • Assuming origin/bound overloads exist on old Random (they're Java 17+)

context