Java's API around random numbers used to be a bit muddied.
First and foremost, there's the class
Random, which has a solid API.
Then there are its subclasses
SecureRandom, which is slower but a cryptographically strong random number generator, and
ThreadLocalRandom, which is faster than
Random but not thread-safe.
Then, off to the side, we have
SplittableRandom, which is also not thread-safe, but has a method
split that returns a new
SplittableRandom that you can pass to a task in a newly spawned thread - this works great for fork/join-style computations.
The streams of random numbers
SplittableRandom returns, e.g. with
SplittableRandom::longs, employ this functionality when used as a parallel stream.
But have you ever tried replacing
They're unrelated, so it can be a bit cumbersome.
Then there are some methods like
nextLong(long bound) that you can find on some of the classes but not others.
Apparently, these classes aren't doing great under the hood either with a few identical pieces of code in several classes and because there's no overarching interface, third parties can't easily provide drop-in replacements for these classes.
These classes aren't doing great under the hood either.
But all of this changes with Java 17.
▚The New Type Hierarchy Around
Thanks to the work done as part of JEP 356, Java 17 ships with a new and better random generator API.
One of its core elements is the new interface hierarchy for random (number) generators with the new interface
RandomGenerator at its top.
RandomGenerator's API is basically that of
Random plus a few methods it was missing - like the ones with the upper bound I mentioned earlier.
Its implementations are not required to be thread-safe or cryptographically secure.
The four preexisting classes we just discussed were all refactored to share more of their code and make future RNGs easier to implement.
They were also expanded to provide the full API of
RandomGenerator, which they now implement.
All of this while being 100% backwards compatible of course.
Then there are five additional interfaces that extend
Their differentiating factor is how you can use one such generator to create another one that is statistically independent and individually uniform (or some approximation thereof), so you can pass them off to a new thread.
StreamableGenerator can return a stream of random generators.
This is helpful because if a generator can create a set of new generators all at once, it can make a special effort to ensure that they're statistically independent.
How can a generator create another generator?
That depends on the underlying algorithm.
Some can return a new generator by jumping forward a number of draws - depending on how many, they implement
Then we have
SplittableGenerator, which prescribes the behavior I described earlier for
SplittableRandom, meaning a generator that can split itself into two.
As mentioned before, all four existing classes extend the top-level interface
SplittableRandom also implements
That means you can either keep using the existing classes or migrate towards the new interface to make it easier to exchange implementations.
No new public classes were added, though, so how do you get an instance of, say,
A few new algorithms have been implemented and more will likely come in the future. I know next to nothing about this complicated field, though, so I'm not gonna speak to any specific algorithm. Instead, I assume you know what you're looking for and will tell you how to get it. Deal?
Generally speaking, all the new interfaces have a static
of method that takes an algorithm name as a
If an algorithm of that name is implemented and adheres to the interface
of is called on, it will return an instance of it.
Otherwise it throws an
String algorithmName = "..."; RandomGenerator.of(algorithmName); StreamableGenerator.of(algorithmName); JumpableGenerator.of(algorithmName); LeapableGenerator.of(algorithmName); ArbitrarilyJumpableGenerator.of(algorithmName); SplittableGenerator.of(algorithmName);
So to create a
LeapableGenerator, you can call
The Javadoc contains a list of algorithms that all JDK implementations must contain, but any specific JDK may add more. Due to advances in random number generator algorithm development and analysis, this list isn't set in stone, though. Algorithms can be deprecated at any time and can be removed in major JDK versions.
So picking algorithms by name may not be the best way to write robust code. Fortunately, a better alternative exists.
The second core element of the new API is the robust selection of algorithms based on requirements.
If you don't have any specific requirements, you can call
RandomGenerator.getDefault() to get an arbitrary algorithm:
RandomGenerator generator = RandomGenerator.getDefault();
If you need more control, you can use the new class
It also has a static
of(name) method, which returns a factory for a specific algorithm, but that gets you back to where you started (naming algorithms) and doesn't help with writing more robust code.
For that you can call the static method
all, which will return a stream of factories, one per algorithm.
The cool thing about that is that you can query factories for properties of the algorithm:
- Is it jumpable, leapable, or splittable?
- How many state bits does it have?
- Does it use a hardware device?
- What is its equidistribution?
You can then filter by your specific requirements, find any factory that fulfills them, and use it to create a
RandomGenerator generator = RandomGeneratorFactory.all() .filter(RandomGeneratorFactory::isJumpable) .filter(factory -> factory.stateBits() > 128) .findAny() .map(RandomGeneratorFactory::create) // if you need a `JumpableGenerator`: // .map(JumpableGenerator.class::cast) .orElseThrow();
Beyond the JDK, third parties can also implement random number generators.
RandomGenerator is registered as a service, so any JAR can provide their own implementations of it.
By using the annotation
@RandomGeneratorProperties, the information that I mentioned before - state bits, equidistribution, etc. - can be attached to it.
The JDK classes will then pick up these implementations and on the use site, they integrate perfectly with the mechanisms described above - be it the static factory
of methods or the more robust
- a new hierarchy of interfaces makes it easier to identify key properties of an algorithm and to switch between implementations
- the four existing random number classes got refactored, slightly extended, and now implement those interfaces while their behavior stays as is
- new algorithms have been and will be implemented as internal classes
- factories can be used to robustly pick algorithms that fulfill specific requirements