Java17の乱数ジェネレーター
1. 概要
Java SE 17 のリリースでは、乱数生成用のAPIのアップデート JEP356が導入されています。
このAPIアップデートにより、新しいインターフェイスタイプが導入され、ジェネレータファクトリを簡単に一覧表示、検索、インスタンス化するメソッドが導入されました。 さらに、乱数ジェネレーターの実装の新しいセットが利用可能になりました。
このチュートリアルでは、新しい RandomGeneratorAPIと古いRandomAPIを比較します。 利用可能なすべての発電機工場をリストし、その名前またはプロパティに基づいて発電機を選択する方法を見ていきます。
また、新しいAPIのスレッドセーフとパフォーマンスについても説明します。
2. 古いランダムAPI
まず、Randomクラスに基づく乱数生成用のJavaの古いAPIを見てみましょう。
2.1. APIデザイン
元のAPIは、インターフェースのない4つのクラスで構成されています。
2.2. ランダム
最も一般的に使用される乱数ジェネレータは、java.utilパッケージのRandomです。
乱数のストリームを生成するには、乱数ジェネレータークラスのインスタンスを作成する必要があります– Random:
Random random = new Random();
int number = random.nextInt(10);
assertThat(number).isPositive().isLessThan(10);
ここで、デフォルトのコンストラクターは、乱数ジェネレーターのシードを、他の呼び出しとは異なる可能性が非常に高い値に設定します。
2.3. 代替案
java.util.Random に加えて、スレッドセーフとセキュリティの懸念に対処するために3つの代替ジェネレーターが利用可能です。
Random のすべてのインスタンスは、デフォルトでスレッドセーフです。 ただし、スレッド間で同じインスタンスを同時に使用すると、パフォーマンスが低下する可能性があります。 したがって、 java.util.concurrentパッケージのThreadLocalRandomクラスは、マルチスレッドシステムに適したオプションです。
Random インスタンスは暗号的に安全ではないため、 SecureRandom クラスを使用すると、セキュリティに敏感なコンテキストで使用するジェネレーターを作成できます。
最後に、java.utilパッケージのSplittableRandomクラスは、並列ストリームおよびフォーク/結合スタイルの計算を処理するために最適化されています。
3. 新しいRandomGeneratorAPI
それでは、RandomGeneratorインターフェースに基づく新しいAPIを見てみましょう。
3.1. APIデザイン
新しいAPIは、新しいインターフェースタイプとジェネレーターの実装を使用して、より優れた全体的な設計を提供します。
上の図では、古いAPIクラスが新しいデザインにどのように適合するかを確認できます。 これらのタイプに加えて、いくつかの乱数ジェネレーター実装クラスが追加されています。
- Xoroshiroグループ
- Xoroshiro128PlusPlus
- Xoshiroグループ
- Xoshiro256PlusPlus
- LXMグループ
- L128X1024MixRandom
- L128X128MixRandom
- L128X256MixRandom
- L32X64MixRandom
- L64X1024MixRandom
- L64X128MixRandom
- L64X128StarStarRandom
- L64X256MixRandom
3.2. 改善点
古いAPIのインターフェースの欠如により、異なるジェネレーターの実装を切り替えることが難しくなりました。 したがって、サードパーティが独自の実装を提供することは困難でした。
たとえば、 SplittableRandom は、コードの一部が Random と完全に同一であったとしても、APIの残りの部分から完全に切り離されていました。
したがって、新しい RandomGeneratorAPIの主な目標は次のとおりです。
- 異なるアルゴリズムの互換性を容易にする
- ストリームベースのプログラミングのサポートを強化する
- 既存のクラスでのコードの重複を排除する
- 古いRandomAPIの既存の動作を保持します
3.3. 新しいインターフェース
新しいルートインターフェイスRandomGeneratorは、既存および新規のすべてのジェネレーターに統一されたAPIを提供します。
さまざまなタイプのランダムに選択された値、およびランダムに選択された値のストリームを返すためのメソッドを定義します。
新しいAPIは、追加の4つの新しい専用ジェネレーターインターフェイスを提供します。
- SplitableGenerator を使用すると、現在のジェネレーターの子孫として新しいジェネレーターを作成できます
- JumpableGenerator を使用すると、適度な数のドローを先に進めることができます
- LeapableGenerator を使用すると、多数のドローを先に進めることができます
- 任意にJumpableGeneratorは、LeapableGeneratorにジャンプ距離を追加します
4. RandomGeneratorFactory
新しいAPIでは、特定のアルゴリズムの複数の乱数ジェネレーターを生成するためのファクトリクラスを利用できます。
4.1. すべて検索
RandomGeneratorFactoryメソッドallは、使用可能なすべてのジェネレーターファクトリの空でないストリームを生成します。
これを使用して、登録されているすべてのジェネレーターファクトリを印刷し、それらのアルゴリズムのプロパティを確認することができます:
RandomGeneratorFactory.all()
.sorted(Comparator.comparing(RandomGeneratorFactory::name))
.forEach(factory -> System.out.println(String.format("%s\t%s\t%s\t%s",
factory.group(),
factory.name(),
factory.isJumpable(),
factory.isSplittable())));
工場の可用性は、サービスプロバイダーAPIを介してRandomGeneratorインターフェイスの実装を見つけることによって決定されます。
4.2. プロパティで検索
all メソッドを使用して、乱数ジェネレーターアルゴリズムのプロパティによってファクトリをクエリすることもできます。
RandomGeneratorFactory.all()
.filter(RandomGeneratorFactory::isJumpable)
.findAny()
.map(RandomGeneratorFactory::create)
.orElseThrow(() -> new RuntimeException("Error creating a generator"));
したがって、Stream APIを使用すると、要件を満たすファクトリを見つけて、それを使用してジェネレーターを作成できます。
5. RandomGeneratorの選択
更新されたAPI設計に加えて、いくつかの新しいアルゴリズムが実装されており、将来さらに追加される可能性があります。
5.1. デフォルトを選択
ほとんどの場合、特定のジェネレーター要件はありません。 したがって、RandomGeneratorインターフェースから直接デフォルトのジェネレーターをフェッチできます。
これは、 Random のインスタンスを作成する代わりに、Java17で推奨される新しいアプローチです。
RandomGenerator generator = RandomGenerator.getDefault();
getDefault メソッドは現在、L32X64MixRandomジェネレーターを選択します。
ただし、アルゴリズムは時間の経過とともに変化する可能性があります。 したがって、このメソッドが今後のリリースでこのアルゴリズムを返し続けるという保証はありません。
5.2. 特定を選択
一方、特定のジェネレーター要件がある場合は、メソッドを使用して特定のジェネレーターを取得できます。
RandomGenerator generator = RandomGenerator.of("L128X256MixRandom");
このメソッドでは、乱数ジェネレーターの名前をパラメーターとして渡す必要があります。
名前付きアルゴリズムが見つからない場合は、IllegalArgumentExceptionがスローされます。
6. スレッドセーフ
の新しいジェネレーターの実装のほとんどは、スレッドセーフではありません。 ただし、RandomとSecureRandomはどちらもそのままです。
したがって、マルチスレッド環境では、次のいずれかを選択できます。
- スレッドセーフジェネレータのインスタンスを共有する
- 新しいスレッドが開始される前に、ローカルソースから新しいインスタンスを分割します
SplittableGenerator を使用して、2番目のケースを実現できます。
List<Integer> numbers = Collections.synchronizedList(new ArrayList<>());
ExecutorService executorService = Executors.newCachedThreadPool();
RandomGenerator.SplittableGenerator sourceGenerator = RandomGeneratorFactory
.<RandomGenerator.SplittableGenerator>of("L128X256MixRandom")
.create();
sourceGenerator.splits(20).forEach((splitGenerator) -> {
executorService.submit(() -> {
numbers.add(splitGenerator.nextInt(10));
});
})
このようにして、ジェネレーターインスタンスが同じ数のストリームにならないように初期化されるようにします。
7. パフォーマンス
Java17で利用可能なすべてのジェネレーター実装に対して簡単なパフォーマンステストを実行してみましょう。
同じ方法でジェネレーターをテストして、4つの異なるタイプの乱数を生成します。
private static void generateRandomNumbers(RandomGenerator generator) {
generator.nextLong();
generator.nextInt();
generator.nextFloat();
generator.nextDouble();
}
ベンチマークの結果を見てみましょう。
アルゴリズム | モード | スコア | エラー | ユニット |
L128X1024MixRandom | avgt | 95,637 | ±3,274 | ns / op |
L128X128MixRandom | avgt | 57,899 | ±2,162 | ns / op |
L128X256MixRandom | avgt | 66,095 | ±3,260 | ns / op |
L32X64MixRandom | avgt | 35,717 | ±1,737 | ns / op |
L64X1024MixRandom | avgt | 73,690 | ±4,967 | ns / op |
L64X128MixRandom | avgt | 35,261 | ±1,985 | ns / op |
L64X128StarStarRandom | avgt | 34,054 | ±0,314 | ns / op |
L64X256MixRandom | avgt | 36,238 | ±0,090 | ns / op |
ランダム | avgt | 111,369 | ±0,329 | ns / op |
SecureRandom | avgt | 9,457,881 | ±45,574 | ns / op |
SplittableRandom | avgt | 27,753 | ±0,526 | ns / op |
Xoroshiro128PlusPlus | avgt | 31,825 | ±1,863 | ns / op |
Xoshiro256PlusPlus | avgt | 33,327 | ±0,555 | ns / op |
SecureRandom は最も遅いジェネレーターですが、これは暗号的に強力なジェネレーターであるためです。
スレッドセーフである必要はないため、新しいジェネレーターの実装はランダムと比較して高速で実行されます。
8. 結論
この記事では、 Java SE17の新機能である乱数生成用のAPIの更新について説明しました。
古いAPIと新しいAPIの違いを学びました。 導入された新しいAPI設計、インターフェース、および実装を含みます。
例では、RandomGeneratorFactoryを使用して適切なジェネレーターアルゴリズムを見つける方法を見ました。 また、名前またはプロパティに基づいてアルゴリズムを選択する方法も確認しました。
最後に、新旧のジェネレーター実装のスレッドセーフとパフォーマンスを確認しました。
いつものように、完全なソースコードはGitHubでから入手できます。