Java 17 Random Number Generators: RandomGenerator API

Introduction Before Java 17, Java had several random number generator classes - java.util.Random, ThreadLocalRandom, SplittableRandom and SecureRandom - with no common interface, so code could not switch between them, and no way to use the newer, better algorithms developed since Random was designed in 1995. Java 17Click to read tutorial: Overview of New Features in Java 17 (JEP 356) introduced enhanced pseudo-random number generators: a common RandomGenerator interface, a family of modern algorithms, and a factory for choosing one by name or by its properties. This tutorial explains the RandomGenerator API with examples - using the interface, choosing an algorithm, getting reproducible sequences from a seed, and generators designed for parallel work. The RandomGenerator interface java.util.random.RandomGenerator declares the methods all generators share: nextInt(), nextInt(bound), the new nextInt(origin, bound), nextDouble(), nextGaussian(), nextBoolean(), and streams such as ints() and doubles(). All the existing classes now implement it, so code can be written against the interface and given any generator -
Writing code against RandomGenerator
//RandomGeneratorDemo.java
import java.security.SecureRandom;
import java.util.Random;
import java.util.SplittableRandom;
import java.util.random.RandomGenerator;

public class RandomGeneratorDemo {
  // Works with any generator: old or new, seeded or secure
  static String rollDice(RandomGenerator rng, int count) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < count; i++) sb.append(rng.nextInt(1, 7)).append(' ');   // nextInt(origin, bound) is new
    return sb.toString().trim();
  }

  public static void main(String[] args) {
    // Seeded generators give the same rolls on every run
    System.out.println("Random(42)          : " + rollDice(new Random(42), 8));
    System.out.println("SplittableRandom(42): " + rollDice(new SplittableRandom(42), 8));
    // Unseeded ones differ every run, but work through the same interface
    for (RandomGenerator rng : new RandomGenerator[] {new SecureRandom(), RandomGenerator.getDefault()}) {
      System.out.println(rng.getClass().getSimpleName() + " rolled " + rollDice(rng, 8).split(" ").length + " dice");
    }
    System.out.println("Default algorithm: " + RandomGenerator.getDefault().getClass().getSimpleName());
  }
}
 OUTPUT of the above code
Random(42)          : 3 4 1 3 1 2 6 3
SplittableRandom(42): 6 1 4 1 6 6 3 6
SecureRandom rolled 8 dice
L32X64MixRandom rolled 8 dice
Default algorithm: L32X64MixRandom
RandomGenerator.getDefault() returns a generator using a good general-purpose algorithm chosen by the JDK - currently L32X64MixRandom. Use it when you do not have a specific reason to pick an algorithm.
The new algorithms Java 17 added a family of generators called LXM - each combines a Linear congruential generator with an Xor-based generator and a Mixing function - plus two popular Xoroshiro/Xoshiro algorithms. Their names describe their structure: L64X128MixRandom combines a 64-bit LCG with a 128-bit xor-based generator. They are fast, have very long periods, and pass the statistical tests which java.util.Random's old 48-bit LCG fails. RandomGeneratorFactory lists every available algorithm with its properties -
Listing the available algorithms
//ListAlgorithms.java
import java.util.Comparator;
import java.util.random.RandomGeneratorFactory;

public class ListAlgorithms {
  public static void main(String[] args) {
    RandomGeneratorFactory.all()
        .sorted(Comparator.comparing(RandomGeneratorFactory::name))
        .forEach(f -> System.out.printf("%-24s state bits=%-5d splittable=%-5b jumpable=%-5b%s%n",
            f.name(), f.stateBits(), f.isSplittable(), f.isJumpable(), f.isDeprecated() ? " (deprecated)" : ""));
  }
}
 OUTPUT of the above code
L128X1024MixRandom       state bits=1152  splittable=true  jumpable=false
L128X128MixRandom        state bits=256   splittable=true  jumpable=false
L128X256MixRandom        state bits=384   splittable=true  jumpable=false
L32X64MixRandom          state bits=96    splittable=true  jumpable=false
L64X1024MixRandom        state bits=1088  splittable=true  jumpable=false
L64X128MixRandom         state bits=192   splittable=true  jumpable=false
L64X128StarStarRandom    state bits=192   splittable=true  jumpable=false
L64X256MixRandom         state bits=320   splittable=true  jumpable=false
Random                   state bits=48    splittable=false jumpable=false
SecureRandom             state bits=2147483647 splittable=false jumpable=false
SplittableRandom         state bits=64    splittable=true  jumpable=false
Xoroshiro128PlusPlus     state bits=128   splittable=false jumpable=true
Xoshiro256PlusPlus       state bits=256   splittable=false jumpable=true
The legacy classes appear too, so a factory lookup can return any generator. More state bits mean a longer period and better behaviour when many generators are used at once, at the cost of a little more memory. Choosing an algorithm and seeding it A generator is created by name with RandomGenerator.of(name), or with RandomGeneratorFactory.of(name).create(seed) when you need a reproducible sequence - for simulations, tests and games which must replay exactly -
Reproducible sequences from a seed
//SeededDemo.java
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;

public class SeededDemo {
  public static void main(String[] args) {
    RandomGeneratorFactory<RandomGenerator> factory = RandomGeneratorFactory.of("L64X128MixRandom");
    RandomGenerator first = factory.create(2021);
    RandomGenerator second = factory.create(2021);
    System.out.println("Run 1: " + first.ints(5, 0, 100).boxed().toList());
    System.out.println("Run 2: " + second.ints(5, 0, 100).boxed().toList());   // same seed, same numbers

    RandomGenerator xoshiro = RandomGenerator.of("Xoshiro256PlusPlus");      // unseeded: different every run
    System.out.println("Xoshiro gaussian in a sensible range: " + (Math.abs(xoshiro.nextGaussian()) < 10));
  }
}
 OUTPUT of the above code
Run 1: [61, 48, 22, 24, 14]
Run 2: [61, 48, 22, 24, 14]
Xoshiro gaussian in a sensible range: true
Generators for parallel work Using one generator from many threads is slow (because of contention) or incorrect, and giving each thread a generator with a random seed risks overlapping sequences. The new API offers two safe ways to make many independent generators -
  • Splittable generators (RandomGenerator.SplittableGenerator, such as the LXM algorithms) can split() off a new generator which is statistically independent - ideal for fork/join tasks and parallel streams, where each sub-task splits its own.
  • Jumpable generators (such as Xoshiro256PlusPlus) can jump() far ahead in their sequence, so a set of generators can be created which are guaranteed not to overlap.
Splitting a generator for parallel tasks
//SplitDemo.java
import java.util.random.RandomGenerator.SplittableGenerator;
import java.util.random.RandomGeneratorFactory;

public class SplitDemo {
  public static void main(String[] args) {
    SplittableGenerator root = (SplittableGenerator) RandomGeneratorFactory.of("L64X128MixRandom").create(7);
    // One independent generator per parallel task, all derived from one seed
    double estimate = root.splits(8)
        .parallel()
        .mapToDouble(rng -> {
          int inside = 0, samples = 250_000;
          for (int i = 0; i < samples; i++) {
            double x = rng.nextDouble(), y = rng.nextDouble();
            if (x * x + y * y <= 1) inside++;
          }
          return 4.0 * inside / samples;
        })
        .average().orElse(0);
    System.out.printf("Monte Carlo estimate of pi from 8 parallel generators: %.2f%n", estimate);
  }
}
 OUTPUT of the above code
Monte Carlo estimate of pi from 8 parallel generators: 3.14
root.splits(8) returns a stream of 8 generators split from the root. Each parallel task gets its own, so there is no contention, the sequences are independent, and the whole result is reproducible from the single seed 7. Which generator should you use?
  • Security (passwords, tokens, keys): always SecureRandom. None of the other generators are cryptographically secure.
  • General purpose: RandomGenerator.getDefault(), or ThreadLocalRandom.current() in multi-threaded code.
  • Reproducible simulations: a named LXM algorithm created with a seed, and split for parallel work.
  • Existing code using Random keeps working unchanged; change its declarations to RandomGenerator to make the algorithm easy to swap later.
Summary In this tutorial we saw how Java 17's enhanced pseudo-random number generators unify all generators under the RandomGenerator interface, and add the LXM and Xoshiro families of algorithms. We wrote code against the interface, listed the algorithms and their properties with RandomGeneratorFactory, created reproducible sequences from a seed, and split one generator into independent generators for a parallel Monte Carlo calculation. Lastly, we went through which generator to use. Next, read about the Java 17 API additionsClick to read tutorial: Java 17 API Additions: HexFormat, InstantSource and More.