Random Numbers
Programs use random numbers to shuffle quiz questions, simulate dice, pick sample data, add jitter to retry delays, generate test data, and create one-time passwords and tokens. Java offers several generators, and the key decision is not how to get a random number but which kind: a fast, repeatable pseudo-random sequence for simulations and games, or a cryptographically secure source for anything an attacker might try to guess.
Pseudo-Random Numbers
Most generators are pseudo-random: they compute a sequence of numbers from a starting value called the seed, using a deterministic formula. The output passes statistical tests for randomness, but the same seed always produces the same sequence:
Random r1 = new Random(42);
Random r2 = new Random(42);
System.out.println(r1.nextInt(100) + " " + r1.nextInt(100)); // 30 63
System.out.println(r2.nextInt(100) + " " + r2.nextInt(100)); // 30 63: identical
A fixed seed is valuable for tests, reproducible simulations, and debugging. Without a seed (new Random()), the generator seeds itself from changing system values, so each run differs.
Math.random()
double x = Math.random(); // 0.0 ≤ x < 1.0
int dice = (int) (Math.random() * 6) + 1; // 1 to 6
Math.random() is convenient for a quick experiment, but it only produces doubles, cannot be seeded, and requires manual scaling. Prefer a generator object.
java.util.Random
import java.util.Random;
Random rnd = new Random();
rnd.nextInt(); // any int, including negatives
rnd.nextInt(100); // 0 to 99
rnd.nextInt(1, 7); // 1 to 6: origin inclusive, bound exclusive (Java 17+)
rnd.nextLong();
rnd.nextDouble(); // 0.0 ≤ x < 1.0
rnd.nextDouble(5.0, 10.0); // 5.0 ≤ x < 10.0 (Java 17+)
rnd.nextBoolean();
rnd.nextGaussian(); // normal distribution: mean 0, standard deviation 1
The upper bound is always exclusive. nextInt(1, 7) simulates a six-sided die; nextInt(1, 6) would never roll a six.
On older Java versions without the two-argument form, compute the range manually: min + rnd.nextInt(max - min + 1).
Streams of random numbers
int[] scores = new Random(1).ints(5, 0, 101).toArray(); // five scores from 0 to 100
List<Double> ds = new Random(1).doubles(3).boxed().toList();
ints, longs, and doubles return streams, useful for generating test data.
Shuffling and picking
List<String> questions = new ArrayList<>(List.of("Q1", "Q2", "Q3", "Q4", "Q5", "Q6"));
Collections.shuffle(questions, new Random(2026)); // repeatable shuffle
String pick = questions.get(rnd.nextInt(questions.size())); // a random element
Passing a seeded Random to shuffle makes the order reproducible.
ThreadLocalRandom
Random is thread-safe, but threads sharing one instance compete for it, which slows highly concurrent code. ThreadLocalRandom gives each thread its own generator:
import java.util.concurrent.ThreadLocalRandom;
int delayMs = ThreadLocalRandom.current().nextInt(100, 500);
Always call ThreadLocalRandom.current() at the point of use; do not store the result in a field shared between threads. It cannot be seeded, so it is for production randomness rather than reproducible tests.
SecureRandom: For Security
Random, ThreadLocalRandom, and Math.random() are predictable. java.util.Random uses a 48-bit linear formula, and an attacker who observes a few outputs can calculate all future ones. They must never be used for:
- passwords, password-reset tokens, and one-time passwords (OTPs),
- session IDs and API keys,
- encryption keys, salts, and nonces,
- coupon codes, invitation links, or anything else that grants access or value when guessed.
java.security.SecureRandom draws on unpredictable entropy from the operating system and is designed so that its outputs cannot be predicted:
import java.security.SecureRandom;
SecureRandom secure = new SecureRandom();
int digit = secure.nextInt(10);
byte[] token = new byte[32];
secure.nextBytes(token); // 256 random bits
String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(token);
SecureRandom is a subclass of Random, so it has the same methods. Create one instance and reuse it (it is thread-safe); creating a new one for every value is unnecessary. It is somewhat slower than Random, which is irrelevant at the rate tokens are created.
For unique identifiers that need not be secret, UUID.randomUUID() produces a 36-character random UUID, generated internally with SecureRandom.
RandomGenerator (Java 17+)
Java 17 introduced the java.util.random.RandomGenerator interface, implemented by Random, SecureRandom, ThreadLocalRandom, and a family of newer, higher-quality algorithms:
import java.util.random.RandomGenerator;
RandomGenerator g = RandomGenerator.of("L64X128MixRandom");
int n = g.nextInt(1, 101);
RandomGenerator dflt = RandomGenerator.getDefault();
Writing methods against RandomGenerator lets callers supply whichever generator suits them: a seeded one in tests, a fast one in simulations, a secure one where needed.
Choosing a Generator
| Need | Use |
|---|---|
| Reproducible sequence for tests, simulations, or demos | new Random(seed) |
| General-purpose randomness in single-threaded code | new Random() or RandomGenerator.getDefault() |
| Randomness in highly concurrent code | ThreadLocalRandom.current() |
| Tokens, OTPs, passwords, keys, coupon codes | SecureRandom |
| Unique IDs | UUID.randomUUID() |
| High-quality statistical generators | RandomGenerator.of("L64X128MixRandom") and related algorithms |
Practical Example: Quiz Shuffling, Dice, and Secure Codes
import java.security.SecureRandom;
import java.util.*;
public class Rnd {
private static final SecureRandom SECURE = new SecureRandom();
private static final String ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no 0/O, 1/I
static String otp(int digits) {
StringBuilder sb = new StringBuilder(digits);
for (int i = 0; i < digits; i++) sb.append(SECURE.nextInt(10));
return sb.toString();
}
static String couponCode(int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) sb.append(ALPHABET.charAt(SECURE.nextInt(ALPHABET.length())));
return sb.toString();
}
public static void main(String[] args) {
Random seeded = new Random(42);
System.out.println(seeded.nextInt(100) + " " + seeded.nextInt(100) + " " + seeded.nextInt(1, 7));
Random again = new Random(42);
System.out.println(again.nextInt(100) + " " + again.nextInt(100) + " " + again.nextInt(1, 7));
List<String> questions = new ArrayList<>(List.of("Q1", "Q2", "Q3", "Q4", "Q5", "Q6"));
Collections.shuffle(questions, new Random(2026));
System.out.println("Quiz order: " + questions);
int[] counts = new int[7];
Random dice = new Random(7);
for (int i = 0; i < 60_000; i++) counts[dice.nextInt(1, 7)]++;
System.out.println("Dice: " + Arrays.toString(Arrays.copyOfRange(counts, 1, 7)));
String code = otp(6);
String coupon = couponCode(8);
System.out.println("OTP length " + code.length() + ", digits only: " + code.matches("\\d{6}"));
System.out.println("Coupon length " + coupon.length() + ", valid: " + coupon.matches("[A-HJ-NP-Z2-9]{8}"));
}
}
Output (Java 17 or later):
30 63 1
30 63 1
Quiz order: [Q3, Q2, Q5, Q4, Q1, Q6]
Dice: [10018, 9900, 9996, 9938, 10109, 10039]
OTP length 6, digits only: true
Coupon length 8, valid: true
The first two lines prove that two Random objects with the same seed produce the same sequence. That repeatability is also what makes the quiz shuffle reproducible: every run with seed 2026 gives the same question order, so a teacher could regenerate an exam paper exactly. Remove the seed to give each student a different order.
The dice simulation rolls 60,000 times. Each face appears close to the expected 10,000 times, showing that nextInt(1, 7) covers all six faces evenly; index 0 of counts is unused, which is why the program prints only indexes 1 to 6.
The OTP and coupon code use SecureRandom, because they protect accounts and money. The coupon alphabet leaves out the easily confused characters 0, O, 1, and I, which matters when people type codes by hand. The program prints only whether the generated values have the right form, because their actual contents change on every run and are unpredictable, exactly as they should be.