Back to Blog
Java

Java Random Range: Generate Integers in a Range

java random range: Learn how to generate random integers within a specific range in Java using Random, ThreadLocalRandom, SecureRandom, and the RandomGenerator interfa...

javarandomThreadLocalRandomSecureRandomRandomGeneratorconcurrency
Illustration of Java random number generation within a range, showing a number line with bounds and a random value.

Generating a random integer within a specific range is a common task in Java, but the right approach depends on the context. The phrase java random range usually refers to producing an integer between a lower and upper bound, and Java offers several APIs for this. The choice affects thread safety, performance, and whether the result is suitable for security-sensitive operations.

The Core Problem: Generating a Random Integer in a Range

The most frequent requirement is to obtain a random integer between a minimum value (inclusive) and a maximum value (inclusive or exclusive). The standard approach in older Java versions uses java.util.Random and its nextInt(int bound) method, which returns a value from 0 (inclusive) to bound (exclusive). To shift this into a custom range, you add an offset. For an inclusive upper bound, you must add 1 to the range size before calling nextInt.

import java.util.Random; public class RangeRandom { public static int randomInclusive(Random random, int min, int max) { if (max < min) { throw new IllegalArgumentException("max must be >= min"); } return random.nextInt((max - min) + 1) + min; } }

The (max - min) + 1 expression converts the inclusive range into an exclusive bound. For example, min = 5, max = 10 yields nextInt(6) which returns 0–5, then adding 5 gives 5–10. This pattern is straightforward but has a subtle overflow risk when max - min + 1 exceeds Integer.MAX_VALUE, which we'll address later.

Using java.util.Random for a Range

java.util.Random is the classic random number generator. It is thread-safe, but its thread safety relies on an atomic seed update, which can cause contention under high concurrency. For single-threaded code, it is perfectly adequate. If you are on Java 17 or later, the RandomGenerator interface provides a more direct method: nextInt(int origin, int bound), where origin is inclusive and bound is exclusive. This eliminates the manual offset.

import java.util.Random; public class RangeRandomJava17 { public static int randomInclusive(Random random, int min, int max) { // nextInt(origin, bound) is exclusive of bound, so add 1 return random.nextInt(min, max + 1); } }

This method is clearer and avoids the overflow issue because the implementation internally handles the range calculation. However, it still requires bound to be greater than origin and the difference to not overflow an int. If you need a range that spans more than Integer.MAX_VALUE, you must use long or a different strategy.

ThreadLocalRandom for Concurrent Code

In multi-threaded applications, ThreadLocalRandom is the recommended choice. It provides a separate random generator for each thread, eliminating contention on a shared seed. You access it via ThreadLocalRandom.current(), which returns an instance bound to the calling thread. This class also implements RandomGenerator and offers nextInt(origin, bound) directly.

import java.util.concurrent.ThreadLocalRandom; public class ThreadRangeRandom { public static int randomInclusive(int min, int max) { return ThreadLocalRandom.current().nextInt(min, max + 1); } }

Because each thread has its own generator, you do not need to pass a Random instance around. This is particularly useful in parallel streams or thread pools where many threads generate random numbers concurrently. The overhead is minimal, and the code is clean. Note that ThreadLocalRandom is not suitable for cryptographic use.

SecureRandom When Security Matters

When the random value is used in security-sensitive contexts—such as generating tokens, passwords, or session IDs—you must use a cryptographically strong generator. SecureRandom provides this by using a secure entropy source and a strong algorithm. It is slower than Random and ThreadLocalRandom, but that is the price of unpredictability. You can use it in the same way for range generation.

import java.security.SecureRandom; public class SecureRangeRandom { public static int randomInclusive(SecureRandom random, int min, int max) { return random.nextInt(min, max + 1); } }

SecureRandom is thread-safe, but its internal synchronization can become a bottleneck under heavy concurrent load. In such cases, you might use a ThreadLocal<SecureRandom> to give each thread its own instance, though the security benefits remain. Do not use Random or ThreadLocalRandom for anything that requires cryptographic strength.

The RandomGenerator Interface in Java 17+

Java 17 introduced the RandomGenerator interface, which unifies all random sources. Random, ThreadLocalRandom, and SecureRandom all implement this interface. The interface provides standard methods like nextInt(), nextLong(), and the overloaded nextInt(origin, bound). You can also use the RandomGenerator.of(String) factory to obtain a specific implementation by name, such as "Random", "SecureRandom", or "L64X128MixRandom" (a splittable generator). This abstraction allows you to write code that works with any random source.

import java.util.random.RandomGenerator; public class GeneratorRangeRandom { public static int randomInclusive(RandomGenerator generator, int min, int max) { return generator.nextInt(min, max + 1); } }

This is useful for library code that needs to accept a random source from the caller. You can pass a Random, ThreadLocalRandom, or SecureRandom instance, and the method works identically. The interface also includes methods like ints(origin, bound) for generating streams of random integers.

Handling Edge Cases: Bounds and Inclusive/Exclusive

When working with ranges, you must consider several edge cases. The most common are negative ranges, zero-length ranges, and integer overflow. A zero-length range occurs when min == max; the only valid result is that value. Both nextInt(origin, bound) and the offset approach handle this correctly as long as the bound is strictly greater than the origin. For example, nextInt(5, 6) returns 5, and nextInt(5, 5) throws IllegalArgumentException because the bound must be greater than the origin.

Overflow is trickier. If you use the classic nextInt(max - min + 1) + min and max - min + 1 exceeds Integer.MAX_VALUE, the subtraction overflows and produces a negative number, leading to an IllegalArgumentException from nextInt. The Java 17 nextInt(origin, bound) method internally checks that bound - origin does not overflow, and it throws IllegalArgumentException if it does. To generate a random long in a wide range, use nextLong(origin, bound) instead, which handles 64-bit ranges without overflow.

Another edge case is when min is negative and max is positive. The offset approach works fine because the arithmetic is done in int space. For example, min = -5, max = 5 yields nextInt(11) - 5, which produces -5 to 5. The nextInt(origin, bound) method also handles negative origins correctly, as long as the bound is greater.

Choosing the Right Random Source

The decision among Random, ThreadLocalRandom, and SecureRandom depends on your concurrency model and security requirements. Use Random for simple, single-threaded code where you need a predictable, reproducible sequence (e.g., with a fixed seed for testing). Use ThreadLocalRandom in any multi-threaded environment where you do not need reproducibility or cryptographic strength. It is the default choice for most application code that generates random numbers in parallel. Use SecureRandom only when the output must be unpredictable to an attacker—for example, in security tokens, password salts, or cryptographic keys.

Math.random() is another option, but it is essentially a Random instance behind a static method. It returns a double between 0.0 (inclusive) and 1.0 (exclusive), so you must scale and cast to get an integer. This is less flexible and can be less readable, but it works for quick scripts. For most production code, you are better off using one of the dedicated random classes.

The RandomGenerator interface gives you a uniform way to write code that works with any of these sources. If you are building a library, accept a RandomGenerator parameter rather than a concrete class. This keeps your code flexible and future-proof.

Performance and Concurrency Considerations

Performance differences among these generators stem from their underlying algorithms and synchronization strategies. Random uses a linear congruential generator (LCG) and a 48-bit seed. Its thread safety is implemented with a CAS operation on the seed, which can cause cache-line contention when many threads call it simultaneously. In a high-throughput scenario, this contention can degrade performance significantly. ThreadLocalRandom avoids this by giving each thread its own generator instance, so there is no shared state and no contention. The seed is initialized from the thread's own state, and each thread advances its own sequence independently.

SecureRandom is the slowest because it uses a cryptographically strong algorithm, such as SHA1PRNG or DRBG, and it may block while gathering entropy from the operating system. The exact cost depends on the algorithm and the platform, but it is always more expensive than Random or ThreadLocalRandom. Therefore, you should not use SecureRandom for non-security purposes, such as game randomness or statistical sampling.

In practice, the performance impact is rarely noticeable unless you generate millions of random numbers per second. For most applications, the correctness and thread-safety characteristics matter more than raw speed. Choose ThreadLocalRandom for concurrent code, and you will avoid the most common performance pitfall: shared Random instances causing contention. If you need reproducibility, use Random with a fixed seed and ensure it is not shared across threads. If you need security, use SecureRandom and accept the performance cost.

A final note on stream generation: the RandomGenerator interface provides ints(origin, bound) and longs(origin, bound) methods that return infinite streams. These are useful for generating a sequence of random values without manually looping. They are also lazy, so you can limit them with limit(n) to avoid unbounded generation. This is a clean, idiomatic way to produce a collection of random integers in a range.

java random range: Practical Usage and Code Examples | RYUSLOG DEV