Java 17 introduces Enhanced Pseudo-Random Number Generators through JEP 356, providing a modern, flexible framework for generating random numbers. This update brings new interfaces, multiple algorithm implementations, and better support for stream-based operations, addressing limitations of the legacy java.util.Random class.
The Problem with Legacy Random
The traditional java.util.Random class has several limitations:
Random random = new Random();
int value = random.nextInt(100); // Limited to one algorithm
Issues:
- Only one algorithm (Linear Congruential Generator)
- No easy way to switch algorithms
- Limited support for modern use cases
- Not designed for streams and parallel processing
The New Framework
Java 17 introduces a hierarchy of interfaces and implementations:
Core Interfaces
RandomGenerator- Base interface for all generatorsRandomGenerator.SplittableGenerator- For parallel streamsRandomGenerator.JumpableGenerator- For jumping ahead in sequenceRandomGenerator.LeapableGenerator- For large jumpsRandomGenerator.ArbitrarilyJumpableGenerator- For arbitrary jumps
New Factory Method
RandomGenerator generator = RandomGenerator.of("L64X128MixRandom");
int value = generator.nextInt(100);
Available Algorithms
Java 17 provides multiple high-quality PRNG algorithms:
| Algorithm | Best For | Thread-Safe |
|---|---|---|
L32X64MixRandom | General purpose, small state | No |
L64X128MixRandom | General purpose, medium state | No |
L64X256MixRandom | High-quality randomness | No |
L64X1024MixRandom | Highest quality, large state | No |
L128X128MixRandom | Very high quality | No |
L128X256MixRandom | Excellent quality | No |
L128X1024MixRandom | Best quality, largest state | No |
Xoroshiro128PlusPlus | Fast, good quality | No |
Xoshiro256PlusPlus | Fast, better quality | No |
Random | Legacy compatibility | Yes |
SecureRandom | Cryptographic security | Yes |
Basic Usage
Creating Generators
// Using factory method with algorithm name
RandomGenerator rng1 = RandomGenerator.of("L64X128MixRandom");
// Using default algorithm
RandomGenerator rng2 = RandomGenerator.getDefault();
// Using specific implementation class
RandomGenerator rng3 = new Xoshiro256PlusPlus();
// Legacy Random still works
Random legacyRandom = new Random();
Generating Random Values
RandomGenerator rng = RandomGenerator.of("L64X128MixRandom");
// Integers
int randomInt = rng.nextInt();
int boundedInt = rng.nextInt(100); // 0 to 99
int rangedInt = rng.nextInt(10, 20); // 10 to 19
// Longs
long randomLong = rng.nextLong();
long boundedLong = rng.nextLong(1000L);
// Doubles
double randomDouble = rng.nextDouble(); // 0.0 to 1.0
double rangedDouble = rng.nextDouble(10.0, 20.0);
// Booleans
boolean randomBoolean = rng.nextBoolean();
// Bytes
byte[] bytes = new byte[16];
rng.nextBytes(bytes);
Stream-Based Operations
One of the most powerful features is stream support:
Infinite Streams
RandomGenerator rng = RandomGenerator.of("L64X128MixRandom");
// Stream of random integers
rng.ints()
.limit(10)
.forEach(System.out::println);
// Stream of bounded integers
rng.ints(100) // limit to 100 values
.limit(10)
.forEach(System.out::println);
// Stream with range
rng.ints(10, 1, 100) // 10 values between 1 and 99
.forEach(System.out::println);
Practical Stream Examples
// Generate random passwords
String password = RandomGenerator.of("L64X128MixRandom")
.ints(12, 33, 127) // 12 printable ASCII characters
.collect(StringBuilder::new,
StringBuilder::appendCodePoint,
StringBuilder::append)
.toString();
// Generate test data
List<Integer> testScores = RandomGenerator.of("L64X128MixRandom")
.ints(100, 0, 101) // 100 scores from 0 to 100
.boxed()
.collect(Collectors.toList());
// Simulate dice rolls
Map<Integer, Long> diceRolls = RandomGenerator.of("L64X128MixRandom")
.ints(10000, 1, 7) // 10,000 rolls of a 6-sided die
.boxed()
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
));
Splittable Generators for Parallel Processing
For parallel streams, use splittable generators:
RandomGenerator.SplittableGenerator splitRng =
RandomGeneratorFactory.of("L64X128MixRandom")
.create(System.currentTimeMillis());
// Parallel stream with independent generators
List<Double> parallelRandoms = IntStream.range(0, 1000000)
.parallel()
.mapToObj(i -> splitRng.split().nextDouble())
.collect(Collectors.toList());
Real-World Examples
1. Monte Carlo Simulation
public class MonteCarloSimulation {
public static double estimatePi(int iterations) {
RandomGenerator rng = RandomGenerator.of("L64X128MixRandom");
long insideCircle = rng.doubles(iterations)
.map(x -> new double[]{x, rng.nextDouble()})
.filter(point -> {
double x = point[0];
double y = point[1];
return (x * x + y * y) <= 1.0;
})
.count();
return 4.0 * insideCircle / iterations;
}
public static void main(String[] args) {
double pi = estimatePi(1_000_000);
System.out.println("Estimated π: " + pi);
System.out.println("Actual π: " + Math.PI);
System.out.println("Error: " + Math.abs(pi - Math.PI));
}
}
2. Random Data Generator
public class TestDataGenerator {
private final RandomGenerator rng;
public TestDataGenerator() {
this.rng = RandomGenerator.of("L64X128MixRandom");
}
public String randomEmail() {
String username = rng.ints(8, 'a', 'z' + 1)
.collect(StringBuilder::new,
StringBuilder::appendCodePoint,
StringBuilder::append)
.toString();
return username + "@example.com";
}
public String randomPhoneNumber() {
return String.format("(%03d) %03d-%04d",
rng.nextInt(200, 1000),
rng.nextInt(200, 1000),
rng.nextInt(10000)
);
}
public LocalDate randomBirthdate() {
long minDay = LocalDate.of(1950, 1, 1).toEpochDay();
long maxDay = LocalDate.of(2005, 12, 31).toEpochDay();
long randomDay = rng.nextLong(minDay, maxDay + 1);
return LocalDate.ofEpochDay(randomDay);
}
public List<String> randomNames(int count) {
String[] firstNames = {"Alice", "Bob", "Charlie", "Diana", "Eve"};
String[] lastNames = {"Smith", "Johnson", "Williams", "Brown", "Jones"};
return rng.ints(count, 0, firstNames.length)
.mapToObj(i -> firstNames[i] + " " +
lastNames[rng.nextInt(lastNames.length)])
.collect(Collectors.toList());
}
}
3. Weighted Random Selection
public class WeightedRandomSelector<T> {
private final List<T> items;
private final List<Double> cumulativeWeights;
private final RandomGenerator rng;
public WeightedRandomSelector(Map<T, Double> weightedItems) {
this.rng = RandomGenerator.of("L64X128MixRandom");
this.items = new ArrayList<>(weightedItems.keySet());
this.cumulativeWeights = new ArrayList<>();
double sum = 0;
for (T item : items) {
sum += weightedItems.get(item);
cumulativeWeights.add(sum);
}
}
public T select() {
double random = rng.nextDouble() * cumulativeWeights.get(cumulativeWeights.size() - 1);
for (int i = 0; i < cumulativeWeights.size(); i++) {
if (random <= cumulativeWeights.get(i)) {
return items.get(i);
}
}
return items.get(items.size() - 1);
}
public List<T> selectMultiple(int count) {
return IntStream.range(0, count)
.mapToObj(i -> select())
.collect(Collectors.toList());
}
}
// Usage
Map<String, Double> items = Map.of(
"Common", 50.0,
"Uncommon", 30.0,
"Rare", 15.0,
"Epic", 4.0,
"Legendary", 1.0
);
WeightedRandomSelector<String> selector = new WeightedRandomSelector<>(items);
List<String> drops = selector.selectMultiple(100);
4. Shuffling Collections
public class CollectionShuffler {
public static <T> void shuffle(List<T> list, RandomGenerator rng) {
for (int i = list.size() - 1; i > 0; i--) {
int j = rng.nextInt(i + 1);
Collections.swap(list, i, j);
}
}
public static void main(String[] args) {
List<String> deck = new ArrayList<>();
for (String suit : List.of("♠", "♥", "♦", "♣")) {
for (String rank : List.of("A", "2", "3", "4", "5", "6",
"7", "8", "9", "10", "J", "Q", "K")) {
deck.add(rank + suit);
}
}
RandomGenerator rng = RandomGenerator.of("L64X128MixRandom");
shuffle(deck, rng);
System.out.println("Shuffled deck: " + deck.subList(0, 5));
}
}
Choosing the Right Algorithm
For General Use
RandomGenerator rng = RandomGenerator.of("L64X128MixRandom");
For High-Quality Randomness
RandomGenerator rng = RandomGenerator.of("L128X256MixRandom");
For Speed
RandomGenerator rng = RandomGenerator.of("Xoshiro256PlusPlus");
For Cryptographic Security
RandomGenerator rng = new SecureRandom();
Best Practices
1. Reuse Generator Instances
// Good - reuse the generator
RandomGenerator rng = RandomGenerator.of("L64X128MixRandom");
for (int i = 0; i < 1000; i++) {
int value = rng.nextInt(100);
}
// Bad - creating new generators is expensive
for (int i = 0; i < 1000; i++) {
RandomGenerator rng = RandomGenerator.of("L64X128MixRandom");
int value = rng.nextInt(100);
}
2. Use Streams for Bulk Generation
// Good - efficient stream-based generation
List<Integer> numbers = RandomGenerator.of("L64X128MixRandom")
.ints(1000, 0, 100)
.boxed()
.collect(Collectors.toList());
// Less efficient - loop-based generation
List<Integer> numbers2 = new ArrayList<>();
RandomGenerator rng = RandomGenerator.of("L64X128MixRandom");
for (int i = 0; i < 1000; i++) {
numbers2.add(rng.nextInt(100));
}
3. Choose Appropriate Algorithm
Don’t use SecureRandom for non-security purposes - it’s much slower.
Conclusion
Java 17’s Enhanced Pseudo-Random Number Generators provide a modern, flexible framework for generating random numbers. With multiple high-quality algorithms, excellent stream support, and better parallel processing capabilities, this feature addresses the limitations of legacy Random while maintaining backward compatibility. Whether you’re building simulations, generating test data, or implementing game mechanics, the new PRNG framework offers the tools you need for high-quality randomness.