- JAVA
Java乱数の作り方|Random nextIntの範囲指定と使い分け
Javaで乱数を作るいちばん基本の書き方は、new Random().nextInt(10) です。これで0〜9の整数が1つ得られます。 1〜6のように下限を付けたいときは、Java 17以降なら nextInt(1, 7)、どのバージョンでも動く形なら nextInt(6) + 1 と書きます。どちらも上限の数は含まれないので、「6まで出したいなら7を書く」という点だけ覚えておけば範囲指定で迷いません。
Javaには乱数を作る方法が5つあります(Random・Math.random()・ThreadLocalRandom・SecureRandom・Java 17で加わった RandomGenerator)。この記事では、5つの用途とスレッドセーフかどうかを比較表にまとめ、すべてのコードをOpenJDK 21.0.10で実行した出力を載せています。 +1 を忘れたときに範囲がどうずれるかを60万回の度数分布で確かめ、重複しない乱数、seed固定による再現、パスワードやトークンの作り方まで順に扱います。Java 25での差分は公式APIドキュメントで確認して注記しています。
Java乱数の早見表
どれを使うか
| クラス | 一言でいうと | 向いている用途 | スレッドセーフ | seed固定 | 予測されにくさ |
|---|---|---|---|---|---|
java.util.Random |
基本の乱数 | 学習、ゲーム、抽選、テストデータ | 安全(共有すると待ちが生じて遅くなる) | できる | 低い(出力から続きを計算できる) |
Math.random() |
0.0以上1.0未満の小数を1行で | 手早く小数の乱数がほしい | 安全(内部で1つのRandomを共有) | できない | 低い |
ThreadLocalRandom |
スレッドごとの乱数 | 複数スレッドで大量に作る | スレッドごとに独立。current() を毎回呼ぶ |
できない | 低い |
SecureRandom |
暗号用の乱数 | パスワード、トークン、確認コード、鍵 | 安全 | 再現用には使えない | 高い |
RandomGenerator(Java 17以降) |
新しいアルゴリズムを選べる共通インターフェース | シミュレーション、アルゴリズムを選びたい | 既定では安全とは限らない(スレッドごとに作る) | できる | 低い |
範囲指定の書き方
| ほしい範囲 | Java 17以降 | どのバージョンでも |
|---|---|---|
| 0〜9 | r.nextInt(10) |
r.nextInt(10) |
| 1〜6 | r.nextInt(1, 7) |
r.nextInt(6) + 1 |
| min〜max(両端を含む) | r.nextInt(min, max + 1) |
r.nextInt(max - min + 1) + min |
| -3〜3 | r.nextInt(-3, 4) |
r.nextInt(7) - 3 |
| 0.0以上1.0未満 | r.nextDouble() |
r.nextDouble() / Math.random() |
| 1.5以上2.5未満 | r.nextDouble(1.5, 2.5) |
1.5 + r.nextDouble() |
| true / false | r.nextBoolean() |
r.nextBoolean() |
| 30%の確率でtrue | r.nextInt(100) < 30 |
r.nextInt(100) < 30 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java乱数の作り方|Random nextIntの範囲指定と使い分け」
まず動かす:Randomの基本メソッドと実行結果
Random は java.util.Random をimportし、new Random() で作ります。 下のコードは、記事と同じ結果になるようにseed(乱数の種)を42に固定しています。new Random() と引数なしにすれば、実行のたびに違う値になります。
import java.util.Random;
public class RBasic {
public static void main(String[] args) {
Random r = new Random(42); // 結果を記事と揃えるため seed を固定
System.out.println("nextInt() : " + r.nextInt());
System.out.println("nextInt(10) : " + r.nextInt(10)); // 0〜9
System.out.println("nextInt(1, 7) : " + r.nextInt(1, 7)); // 1〜6(Java 17以降)
System.out.println("nextInt(6) + 1 : " + (r.nextInt(6) + 1)); // 1〜6(どの版でも)
System.out.println("nextLong() : " + r.nextLong());
System.out.println("nextLong(100) : " + r.nextLong(100));
System.out.println("nextDouble() : " + r.nextDouble()); // 0.0以上1.0未満
System.out.println("nextDouble(1,2) : " + r.nextDouble(1.0, 2.0));
System.out.println("nextFloat() : " + r.nextFloat());
System.out.println("nextBoolean() : " + r.nextBoolean());
System.out.println("nextGaussian() : " + r.nextGaussian());
System.out.println("nextGaussian(50,10): " + r.nextGaussian(50, 10));
byte[] b = new byte[4];
r.nextBytes(b);
System.out.println("nextBytes : " + java.util.Arrays.toString(b));
System.out.println("ints(5, 1, 7) : " + r.ints(5, 1, 7).boxed().toList());
System.out.println("doubles(2) : " + r.doubles(2).boxed().toList());
}
}
実行結果(OpenJDK 21.0.10):
nextInt() : -1170105035
nextInt(10) : 3
nextInt(1, 7) : 1
nextInt(6) + 1 : 3
nextLong() : 5694868678511409995
nextLong(100) : 98
nextDouble() : 0.6655489517945736
nextDouble(1,2) : 1.9033722646721782
nextFloat() : 0.36878288
nextBoolean() : false
nextGaussian() : -1.7534879738855944
nextGaussian(50,10): 46.60579015993288
nextBytes : [-15, 16, 89, -21]
ints(5, 1, 7) : [2, 3, 4, 6, 4]
doubles(2) : [0.38656687435934867, 0.17737847790937833]
| メソッド | 返す値 | 導入 |
|---|---|---|
nextInt() |
int 全体(負の数も含む) |
1.0 |
nextInt(bound) |
0以上 bound 未満 | 1.2 |
nextInt(origin, bound) |
origin以上 bound 未満 | 17 |
nextLong() / nextLong(bound) / nextLong(origin, bound) |
long の乱数 |
1.0 / 17 / 17 |
nextDouble() |
0.0以上1.0未満 | 1.0 |
nextDouble(bound) / nextDouble(origin, bound) |
範囲つきの小数 | 17 |
nextFloat() |
0.0以上1.0未満の float |
1.0 |
nextBoolean() |
true か false |
1.2 |
nextGaussian() |
平均0、標準偏差1の正規分布 | 1.0 |
nextGaussian(mean, stddev) |
平均と標準偏差を指定した正規分布 | 17 |
nextBytes(byte[]) |
配列をランダムなバイトで埋める | 1.1 |
ints(個数, origin, bound) |
乱数のStream(doubles・longs も同様) |
8 |
引数なしの nextInt() は負の数も返します。 「0以上の整数がほしい」ときは、必ず nextInt(bound) のように上限を渡します。
範囲指定:nextInt(bound) と nextInt(origin, bound)
nextInt(bound) は「0以上 bound 未満」、nextInt(origin, bound) は「origin以上 bound 未満」を返します。 どちらも bound そのものは出ません。min〜max の両端を含めたいときの公式は次の2つです。
int a = r.nextInt(max - min + 1) + min; // どのバージョンでも
int b = r.nextInt(min, max + 1); // Java 17以降
よく使う範囲指定を、まとめて実行しました。
import java.util.Random;
public class Range {
static void run(String label, Runnable r) {
try { r.run(); System.out.println(label + " -> OK"); }
catch (RuntimeException e) { System.out.println(label + " -> " + e); }
}
public static void main(String[] args) {
Random r = new Random(1);
int min = 5, max = 10;
StringBuilder a = new StringBuilder(), b = new StringBuilder(), c = new StringBuilder();
for (int i = 0; i < 12; i++) {
a.append(r.nextInt(max - min + 1) + min).append(' '); // 5〜10
b.append(r.nextInt(min, max + 1)).append(' '); // 5〜10(Java 17以降)
c.append(r.nextInt(-3, 4)).append(' '); // -3〜3
}
System.out.println("nextInt(max-min+1)+min : " + a);
System.out.println("nextInt(min, max+1) : " + b);
System.out.println("nextInt(-3, 4) : " + c);
System.out.printf("小数 1.5〜2.5未満 : %.3f%n", r.nextDouble(1.5, 2.5));
System.out.printf("小数2桁に丸める : %.2f%n", Math.round(r.nextDouble(0, 100) * 100) / 100.0);
System.out.println("10刻み 10〜50 : " + (r.nextInt(1, 6) * 10));
System.out.println("偶数 0〜18 : " + (r.nextInt(10) * 2));
System.out.println("英大文字1文字 : " + (char) ('A' + r.nextInt(26)));
String[] menu = {"カレー", "うどん", "パスタ"};
System.out.println("配列から1つ : " + menu[r.nextInt(menu.length)]);
System.out.println("30%で当たり : " + (r.nextInt(100) < 30));
run("nextInt(0)", () -> r.nextInt(0));
run("nextInt(-5)", () -> r.nextInt(-5));
run("nextInt(5, 5)", () -> r.nextInt(5, 5));
run("nextInt(10, 1)", () -> r.nextInt(10, 1));
run("nextDouble(1, 1)", () -> r.nextDouble(1.0, 1.0));
}
}
実行結果(OpenJDK 21.0.10):
nextInt(max-min+1)+min : 8 8 7 9 6 9 5 7 7 10 6 9
nextInt(min, max+1) : 9 7 9 6 8 7 10 9 10 8 9 10
nextInt(-3, 4) : -2 3 0 2 -1 -3 -1 0 3 -1 -1 0
小数 1.5〜2.5未満 : 1.878
小数2桁に丸める : 13.98
10刻み 10〜50 : 10
偶数 0〜18 : 0
英大文字1文字 : R
配列から1つ : カレー
30%で当たり : false
nextInt(0) -> java.lang.IllegalArgumentException: bound must be positive
nextInt(-5) -> java.lang.IllegalArgumentException: bound must be positive
nextInt(5, 5) -> java.lang.IllegalArgumentException: bound must be greater than origin
nextInt(10, 1) -> java.lang.IllegalArgumentException: bound must be greater than origin
nextDouble(1, 1) -> java.lang.IllegalArgumentException: bound must be greater than origin
| エラーメッセージ | 原因 | 直し方 |
|---|---|---|
bound must be positive |
nextInt(0) や負の数を渡した |
bound は1以上にする。リストの size() を渡すときは空でないか確かめる |
bound must be greater than origin |
nextInt(5, 5) のように origin と bound が同じ、または逆 |
bound は origin より大きくする。値が1つしかないなら乱数は不要 |
配列やリストから1つ選ぶときは list.get(r.nextInt(list.size())) と書きます。 位置は0〜size() - 1 なので、ここでは +1 を付けません。
+1を忘れると範囲がどうずれるか(度数分布で確認)
範囲指定のまちがいは、1回や2回の実行では気づきにくいので、たくさん引いて回数を数えると確実です。 「1〜6を出したい」場面で、7通りの書き方をそれぞれ60万回実行し、0〜7が何回出たかを数えました。
import java.util.Random;
public class OffByOne {
static void hist(String label, java.util.function.IntSupplier s) {
int[] count = new int[8]; // 0〜7 を数える
for (int i = 0; i < 600_000; i++) count[s.getAsInt()]++;
System.out.println(label);
for (int v = 0; v <= 7; v++) System.out.printf(" %d: %6d%n", v, count[v]);
}
public static void main(String[] args) {
Random r = new Random(2026);
hist("(1) r.nextInt(6)", () -> r.nextInt(6));
hist("(2) r.nextInt(6) + 1", () -> r.nextInt(6) + 1);
hist("(3) r.nextInt(1, 6)", () -> r.nextInt(1, 6));
hist("(4) r.nextInt(1, 7)", () -> r.nextInt(1, 7));
hist("(5) (int)(Math.random()*6)+1", () -> (int) (Math.random() * 6) + 1);
hist("(6) (int) Math.random()*6+1", () -> (int) Math.random() * 6 + 1);
hist("(7) Math.round(Math.random()*5)+1", () -> (int) Math.round(Math.random() * 5) + 1);
}
}
実行結果(OpenJDK 21.0.10。縦に出力された回数を、見やすいように表へ並べ替えています。数値はそのままです):
| 書き方 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 判定 |
|---|---|---|---|---|---|---|---|---|
(1) r.nextInt(6) |
99970 | 100236 | 99619 | 99612 | 100367 | 100196 | 0 | 0〜5になる(+1 忘れ) |
(2) r.nextInt(6) + 1 |
0 | 100155 | 100162 | 99793 | 100271 | 100100 | 99519 | 1〜6で均等 |
(3) r.nextInt(1, 6) |
0 | 119808 | 120274 | 119933 | 119829 | 120156 | 0 | 6が出ない(上限は含まれない) |
(4) r.nextInt(1, 7) |
0 | 100195 | 100475 | 99675 | 99670 | 100252 | 99733 | 1〜6で均等 |
(5) (int)(Math.random()*6)+1 |
0 | 99921 | 99997 | 99834 | 100043 | 99801 | 100404 | 1〜6で均等 |
(6) (int) Math.random()*6+1 |
0 | 600000 | 0 | 0 | 0 | 0 | 0 | 常に1(かっこの位置) |
(7) Math.round(Math.random()*5)+1 |
0 | 59871 | 120451 | 119539 | 120219 | 119759 | 60161 | 1と6が半分しか出ない |
7の列はすべて0回でした。読み取れることを整理します。
- (1)
+1を忘れると、範囲が0〜5にずれます。 6が出ないだけでなく、0が出ます。配列の位置として使うなら正しく、サイコロの目として使うならずれている、という違いです。 - (3)
nextInt(1, 6)は1〜5になります。 「1から6まで」と読めてしまう書き方ですが、bound は含まれません。1つあたり約12万回(60万÷5)になっていることからも、5種類しか出ていないとわかります。 - (6) キャストは掛け算より先に働くので、
(int) Math.random()が先に0になり、常に0 * 6 + 1 = 1です。(int) (Math.random() * 6)と、掛け算全体をかっこで囲みます。 - (7)
Math.roundで丸めると、両端の1と6だけ出る回数が半分になります。 0〜0.5未満が1に、4.5〜5が6になり、両端だけ幅が半分だからです。例外もなく範囲も合っているので、度数を数えないと見つけにくいずれです。
範囲つきの整数は nextInt(origin, bound) か nextInt(n) + min で作り、Math.random() を掛け算して丸める書き方は小数がほしいときだけにすると、こうしたずれを避けられます。
5つの方法の使い分け
1〜6の整数を、5つの方法(と SplittableRandom)で作るコードです。
import java.security.SecureRandom;
import java.util.Random;
import java.util.SplittableRandom;
import java.util.concurrent.ThreadLocalRandom;
import java.util.random.RandomGenerator;
public class Five {
public static void main(String[] args) {
Random random = new Random();
System.out.println("Random : " + random.nextInt(1, 7));
System.out.println("Math.random : " + ((int) (Math.random() * 6) + 1));
System.out.println("ThreadLocalRandom : " + ThreadLocalRandom.current().nextInt(1, 7));
SecureRandom secure = new SecureRandom();
System.out.println("SecureRandom : " + secure.nextInt(1, 7));
RandomGenerator gen = RandomGenerator.getDefault();
System.out.println("RandomGenerator : " + gen.nextInt(1, 7));
System.out.println("SplittableRandom : " + new SplittableRandom().nextInt(1, 7));
System.out.println("既定のアルゴリズム : " + gen.getClass().getSimpleName());
System.out.println("SecureRandom : " + secure.getAlgorithm());
System.out.println("Random は RandomGenerator か: " + (random instanceof RandomGenerator));
System.out.println("SecureRandom は Random か : " + (secure instanceof Random));
}
}
実行結果(OpenJDK 21.0.10、Linux。上の6行は実行のたびに変わります):
Random : 1
Math.random : 2
ThreadLocalRandom : 1
SecureRandom : 1
RandomGenerator : 1
SplittableRandom : 2
既定のアルゴリズム : L32X64MixRandom
SecureRandom : NativePRNG
Random は RandomGenerator か: true
SecureRandom は Random か : true
Java 17以降、Random・SecureRandom・ThreadLocalRandom・SplittableRandom はすべて RandomGenerator インターフェースを実装しています。 そのため nextInt(origin, bound) などの範囲つきメソッドが、どのクラスでも同じ書き方で使えます。メソッドの引数を RandomGenerator 型にしておくと、呼び出す側で種類を差し替えられます。
選び方の判断表
| 状況 | 選ぶもの | 理由 |
|---|---|---|
| 学習用、ゲーム、抽選、テストデータ | Random |
書き方が素直で、seedを固定して再現もできる |
| 1行で小数の乱数がほしい | Math.random() |
importもインスタンスも不要 |
| 複数スレッドから大量に呼ぶ | ThreadLocalRandom.current() |
スレッドごとに独立していて待ちが生じない |
| パスワード、トークン、確認コード、セッションID | SecureRandom |
出力から次の値を予測できない |
| アルゴリズムを選びたい、周期の長い乱数がほしい | RandomGenerator.of("アルゴリズム名") |
Java 17で加わったLXM系などを使える |
| 並列ストリームや分割して使う | SplittableRandom |
split() で独立した乱数を分けられる |
| テストで毎回同じ結果にしたい | new Random(seed) |
同じseedなら同じ列になる |
RandomGeneratorで選べるアルゴリズム(Java 21.0.10)
RandomGeneratorFactory.all() で一覧を取りました。
| 名前 | グループ | 状態のビット数 | 分割(split) | ジャンプ |
|---|---|---|---|---|
L32X64MixRandom(既定) |
LXM | 96 | できる | ― |
L64X128MixRandom |
LXM | 192 | できる | ― |
L64X128StarStarRandom |
LXM | 192 | できる | ― |
L64X256MixRandom |
LXM | 320 | できる | ― |
L64X1024MixRandom |
LXM | 1088 | できる | ― |
L128X128MixRandom |
LXM | 256 | できる | ― |
L128X256MixRandom |
LXM | 384 | できる | ― |
L128X1024MixRandom |
LXM | 1152 | できる | ― |
Xoroshiro128PlusPlus |
Xoroshiro | 128 | ― | できる |
Xoshiro256PlusPlus |
Xoshiro | 256 | ― | できる |
Random |
Legacy | 48 | ― | ― |
SplittableRandom |
Legacy | 64 | できる | ― |
SecureRandom |
Legacy | (上限値を返す) | ― | ― |
RandomGenerator.getDefault() が返すのは L32X64MixRandom でした。公式ドキュメントには「アルゴリズムは時間とともに改良されるため、同じものを返し続ける保証はない」と書かれています。結果を再現したい場合は、RandomGeneratorFactory.of("L64X128MixRandom").create(42L) のように名前とseedを明示します。
スレッドセーフと速さの比較
Random は複数スレッドから同時に使っても壊れませんが、1つのインスタンスを共有すると待ちが生じて遅くなります。 8スレッドがそれぞれ500万回 nextInt(100) を呼ぶ時間を測りました(計測部分の抜粋)。
// 8スレッドの固定スレッドプールで、各スレッドが PER = 5_000_000 回呼ぶ
Random shared = new Random();
run("共有した Random", () -> shared.nextInt(100));
run("Math.random()", () -> (int) (Math.random() * 100));
run("ThreadLocalRandom.current()", () -> ThreadLocalRandom.current().nextInt(100));
実行結果(OpenJDK 21.0.10、CPU 2コアの検証用コンテナ。2回実行):
| 方法 | 8スレッド×500万回(1回目 / 2回目) |
|---|---|
1つの Random を共有 |
1086 ms / 1169 ms |
Math.random() |
1359 ms / 2466 ms |
ThreadLocalRandom.current() |
253 ms / 173 ms |
同じプログラムで、1スレッドで500万回呼んだ場合も測りました。
| 方法 | 1スレッド×500万回 |
|---|---|
Random |
64 ms |
SecureRandom |
1061 ms |
数値は環境で変わりますが、複数スレッドでは ThreadLocalRandom が共有の Random の4〜7倍速く、SecureRandom は Random より1桁以上時間がかかるという傾向が読み取れます。SecureRandom は予測されにくさと引き換えに計算量が多いので、必要な箇所に絞って使います。
| クラス | 複数スレッドでの扱い(公式ドキュメントの記載) |
|---|---|
Random |
スレッドセーフ。ただし同じインスタンスを同時に使うと競合で性能が下がるため、マルチスレッドでは ThreadLocalRandom を検討する |
Math.random() |
複数スレッドから正しく使えるよう同期されている。多数のスレッドが高頻度で呼ぶなら、スレッドごとに乱数生成器を持つと競合が減る |
ThreadLocalRandom |
現在のスレッド専用。ThreadLocalRandom.current().nextInt(...) の形で毎回 current() から呼ぶ |
SecureRandom |
複数スレッドから同時に使える |
RandomGenerator の実装 |
原則としてスレッドセーフである必要はない。マルチスレッドでは ThreadLocalRandom か、分割・ジャンプできる生成器を使う |
ThreadLocalRandom は、current() の戻り値をフィールドに保存してほかのスレッドへ渡す使い方には向きません。使う場所で毎回 ThreadLocalRandom.current() と書くのが決まった形です。
もう1つ、スレッドごとに new Random(1) のように同じseedで作ると、全スレッドが同じ列を出します。
同じseedを2スレッドで: [85, 88, 47] [85, 88, 47]
ThreadLocalRandom : [89, 45, 51] [97, 1, 85]
(2スレッドで new Random(1).ints(3, 0, 100) と ThreadLocalRandom.current().ints(3, 0, 100) をそれぞれ実行した結果です。)
重複しない乱数の作り方(シャッフル)
nextInt を繰り返すだけでは、同じ数が何度も出ます。重複なしで選びたいときは「候補を全部並べてシャッフルし、先頭から取る」のが確実です。
import java.util.*;
import java.util.stream.*;
public class Unique {
public static void main(String[] args) {
Random r = new Random(7);
// (1) 1〜10 を重複なしで並べ替える
List<Integer> nums = IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toCollection(ArrayList::new));
Collections.shuffle(nums, r);
System.out.println("shuffle : " + nums);
// (2) 1〜43 から6個を重複なしで選ぶ
List<Integer> pool = IntStream.rangeClosed(1, 43).boxed().collect(Collectors.toCollection(ArrayList::new));
Collections.shuffle(pool, r);
List<Integer> picked = new ArrayList<>(pool.subList(0, 6));
Collections.sort(picked);
System.out.println("先頭6個 : " + picked);
// (3) Set に貯める(範囲が広く、個数が少ないとき)
Set<Integer> set = new LinkedHashSet<>();
int tries = 0;
while (set.size() < 6) { set.add(r.nextInt(1, 1_000_001)); tries++; }
System.out.println("Set : " + set + " 試行=" + tries);
// (4) Stream の distinct
System.out.println("distinct : " + r.ints(1, 44).distinct().limit(6).boxed().toList());
// (5) 重複を許すと実際に重なる
List<Integer> dup = r.ints(10, 1, 11).boxed().toList();
System.out.println("重複あり : " + dup + " 種類=" + new HashSet<>(dup).size());
// (6) 配列の Fisher–Yates シャッフル
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8};
for (int i = arr.length - 1; i > 0; i--) {
int j = r.nextInt(i + 1);
int t = arr[i]; arr[i] = arr[j]; arr[j] = t;
}
System.out.println("配列シャッフル: " + Arrays.toString(arr));
// (7) リストから重複なしで3件
List<String> members = List.of("青木", "井上", "上田", "江口", "小川");
List<String> copy = new ArrayList<>(members);
Collections.shuffle(copy, r);
System.out.println("3件 : " + copy.subList(0, 3));
}
}
実行結果(OpenJDK 21.0.10。(7) の直前に1件だけ選ぶ処理を1回はさんで実行した出力です):
shuffle : [1, 2, 10, 4, 8, 5, 9, 6, 3, 7]
先頭6個 : [3, 12, 15, 17, 35, 39]
Set : [405246, 354195, 470171, 136845, 46453, 189209] 試行=6
distinct : [14, 7, 8, 16, 23, 4]
重複あり : [10, 3, 6, 1, 6, 10, 8, 5, 3, 8] 種類=6
配列シャッフル: [3, 5, 4, 2, 6, 7, 8, 1]
3件 : [上田, 青木, 井上]
(5) では、1〜10から10回引いて6種類しか出ていません(10・3・6・8が2回ずつ)。「範囲と同じ回数だけ引けば全部の数が1回ずつ出る」わけではないことが、出力からわかります。
| 方法 | 書き方 | 向いている場面 |
|---|---|---|
| シャッフルして先頭から取る | Collections.shuffle(list); list.subList(0, k) |
候補の数が多すぎない(数万件程度まで)。確実で読みやすい |
| Setに貯める | while (set.size() < k) set.add(r.nextInt(...)); |
範囲がとても広く、選ぶ個数が少ない |
Streamの distinct |
r.ints(1, 44).distinct().limit(6) |
1行で書きたい。考え方はSetと同じ |
| 配列を自分でシャッフル | 後ろから順に、ランダムな位置と入れ替える | int[] のまま並べ替えたい |
List.of(...)で作ったリストは変更できないので、Collections.shuffleに渡す前にnew ArrayList<>(...)でコピーします。- Setに貯める方法は、範囲いっぱいに近い個数を選ぼうとすると、重複で引き直す回数が増えていきます。範囲の半分以上を選ぶならシャッフルが向いています。
- 配列のシャッフルで
r.nextInt(i + 1)としているのは、「自分自身と入れ替える(動かさない)」場合も含めるためです。ここをr.nextInt(i)にすると並び方に偏りが出ます。
seedを固定して同じ乱数を再現する
new Random(42) のようにseed(種)を渡すと、何度実行しても同じ順番で同じ値が出ます。 テストや、不具合の再現、シミュレーション結果の比較に使えます。
import java.util.*;
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;
public class Seed {
public static void main(String[] args) {
System.out.println("seed=42 : " + new Random(42).ints(5, 0, 100).boxed().toList());
System.out.println("seed=42 : " + new Random(42).ints(5, 0, 100).boxed().toList());
System.out.println("seed=43 : " + new Random(43).ints(5, 0, 100).boxed().toList());
System.out.println("seedなし : " + new Random().ints(5, 0, 100).boxed().toList());
System.out.println("seedなし : " + new Random().ints(5, 0, 100).boxed().toList());
RandomGenerator g1 = RandomGeneratorFactory.of("L64X128MixRandom").create(42L);
RandomGenerator g2 = RandomGeneratorFactory.of("L64X128MixRandom").create(42L);
System.out.println("L64X128MixRandom(42): " + g1.ints(5, 0, 100).boxed().toList());
System.out.println("L64X128MixRandom(42): " + g2.ints(5, 0, 100).boxed().toList());
List<Integer> a = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> b = new ArrayList<>(a);
Collections.shuffle(a, new Random(42));
Collections.shuffle(b, new Random(42));
System.out.println("shuffle(seed=42): " + a + " " + b);
try { java.util.concurrent.ThreadLocalRandom.current().setSeed(42); }
catch (UnsupportedOperationException e) { System.out.println("ThreadLocalRandom.setSeed: " + e); }
java.security.SecureRandom s = new java.security.SecureRandom();
s.setSeed(42);
System.out.println("SecureRandom.setSeed(42)後: " + s.nextInt(100) + " / 別インスタンス: " + seeded());
}
static int seeded() { var s = new java.security.SecureRandom(); s.setSeed(42); return s.nextInt(100); }
}
実行結果(OpenJDK 21.0.10。「seedなし」と最終行は実行のたびに変わります):
seed=42 : [30, 63, 48, 84, 70]
seed=42 : [30, 63, 48, 84, 70]
seed=43 : [56, 51, 67, 98, 57]
seedなし : [98, 45, 49, 85, 58]
seedなし : [94, 18, 83, 89, 22]
L64X128MixRandom(42): [98, 1, 80, 2, 55]
L64X128MixRandom(42): [98, 1, 80, 2, 55]
shuffle(seed=42): [2, 3, 4, 5, 1] [2, 3, 4, 5, 1]
ThreadLocalRandom.setSeed: java.lang.UnsupportedOperationException
SecureRandom.setSeed(42)後: 95 / 別インスタンス: 63
| クラス | seedを固定できるか | 補足 |
|---|---|---|
Random |
できる(new Random(seed) / setSeed) |
同じseedなら同じ列 |
RandomGenerator の各アルゴリズム |
できる(RandomGeneratorFactory.of(名前).create(seed)) |
アルゴリズムが違えば、同じseedでも列は違う |
SplittableRandom |
できる(new SplittableRandom(seed)) |
|
Math.random() |
できない | 再現したいなら Random を使う |
ThreadLocalRandom |
できない(setSeed は UnsupportedOperationException) |
|
SecureRandom |
再現には使えない | 実行結果のとおり、setSeed(42) 後も別インスタンスとは違う値。渡したseedは既存の種に追加される |
テストしやすいコードにするコツは、乱数を使うメソッドの中で new Random() せず、引数やコンストラクタで RandomGenerator(または Random)を受け取る形にすることです。 本番では new Random()、テストでは new Random(42) を渡せば、同じ結果を何度でも確かめられます。
static int rollDice(RandomGenerator rng) { // 乱数生成器を外から受け取る
return rng.nextInt(1, 7);
}
パスワードやトークンにはSecureRandomを使う
Random の出力は、いくつかの値を観測すると続きを計算できます。 Random の内部状態は48ビットで、決まった式で次の状態へ進むためです。seedを知らない new Random() から nextInt() を2つ受け取り、内部状態を逆算して3つ目を当てるプログラムを書いて実行したところ、次の結果になりました。
予測した3つ目: 2000055005
実際の3つ目 : 2000055005
一致: true
公式ドキュメントにも「java.util.Random のインスタンスは暗号学的に安全ではない。セキュリティが重要な用途では SecureRandom の使用を検討する」と書かれています。パスワード、パスワード再設定用のトークン、確認コード、セッションID、APIキーのように「他人に当てられると困る値」は、SecureRandom で作ります。
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HexFormat;
import java.util.UUID;
public class Token {
private static final SecureRandom SR = new SecureRandom();
private static final String CHARS =
"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"; // 見分けにくい 0 O 1 l I を除外
static String password(int len) {
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) sb.append(CHARS.charAt(SR.nextInt(CHARS.length())));
return sb.toString();
}
static String urlToken(int bytes) {
byte[] buf = new byte[bytes];
SR.nextBytes(buf);
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
}
static String hexToken(int bytes) {
byte[] buf = new byte[bytes];
SR.nextBytes(buf);
return HexFormat.of().formatHex(buf);
}
public static void main(String[] args) {
System.out.println("パスワード16文字 : " + password(16));
System.out.println("URL用トークン : " + urlToken(32));
System.out.println("16進トークン : " + hexToken(16));
System.out.println("6桁の確認コード : " + String.format("%06d", SR.nextInt(1_000_000)));
System.out.println("UUID v4 : " + UUID.randomUUID());
System.out.println("アルゴリズム : " + SR.getAlgorithm() + " / " + SR.getProvider().getName());
}
}
実行結果(OpenJDK 21.0.10、Linux。値は実行のたびに変わります):
パスワード16文字 : xpjVav34sxEmTrM4
URL用トークン : 1ZypPNFHUdMBgjDvZ9zG_WBpqTb3yC0KLVQ1C2RKDvo
16進トークン : 1eed806d60ceff803a51c5b984e72dc7
6桁の確認コード : 412296
UUID v4 : 8dea13b4-38d2-4f4a-8ec3-f0eb002450c1
アルゴリズム : NativePRNG / SUN
| 作りたいもの | 書き方 | ポイント |
|---|---|---|
| パスワード | 文字の候補から SR.nextInt(CHARS.length()) で1文字ずつ選ぶ |
長さと文字種を十分に取る |
| URLに載せるトークン | SR.nextBytes(buf) → Base64.getUrlEncoder().withoutPadding() |
32バイト(256ビット)あれば十分な長さ |
| 16進のトークン | SR.nextBytes(buf) → HexFormat.of().formatHex(buf) |
HexFormat はJava 17以降 |
| 6桁の確認コード | String.format("%06d", SR.nextInt(1_000_000)) |
先頭の0を保つために %06d |
| 一意なID | UUID.randomUUID() |
内部で暗号用の乱数生成器を使う(公式ドキュメントの記載) |
SecureRandomはnew SecureRandom()で作り、1つを使い回します。setSeedで固定の値を渡して再現させる使い方はしません。SecureRandomもRandomGeneratorを実装しているので、nextInt(origin, bound)がそのまま使えます。- 選ばれるアルゴリズムはOSや設定で変わります(この環境では
NativePRNG)。特定のアルゴリズムを指定する必要がなければ、引数なしのコンストラクタで十分です。
そのほかの応用:重み付き抽選と正規分布
確率に差をつけた抽選と、平均・ばらつきを指定した乱数を10万回ずつ実行しました。
Random r = new Random(5);
String[] names = {"大当たり", "当たり", "参加賞"};
int[] weights = {10, 30, 60}; // 合計100
int[] count = new int[3];
for (int i = 0; i < 100_000; i++) {
int x = r.nextInt(100), acc = 0;
for (int k = 0; k < 3; k++) { acc += weights[k]; if (x < acc) { count[k]++; break; } }
}
for (int k = 0; k < 3; k++) System.out.printf("%s: %d回(%.1f%%)%n", names[k], count[k], count[k] / 1000.0);
double sum = 0, sq = 0; int n = 100_000;
for (int i = 0; i < n; i++) { double g = r.nextGaussian(50, 10); sum += g; sq += g * g; }
double mean = sum / n;
System.out.printf("nextGaussian(50,10): 平均=%.2f 標準偏差=%.2f%n", mean, Math.sqrt(sq / n - mean * mean));
実行結果(OpenJDK 21.0.10):
大当たり: 10058回(10.1%)
当たり: 30054回(30.1%)
参加賞: 59888回(59.9%)
nextGaussian(50,10): 平均=50.06 標準偏差=9.99
重みの合計までの乱数を1つ引き、重みを足し上げながら「どの区間に入ったか」を調べる方法です。指定した10%・30%・60%に近い割合になっています。nextGaussian(50, 10) は平均50・標準偏差10の正規分布で、テスト用の点数データなど「真ん中あたりが多い」値を作るのに使えます。
Java 25での差分(公式ドキュメントで確認)
この記事のコードはJava 21.0.10で実行しました。Java 25は手元で動かしておらず、Java SE 25の公式APIドキュメントで次の点を確認しています。
| 項目 | Java 21 | Java 25(公式で確認) |
|---|---|---|
Random のメソッド |
この記事のとおり | Java 22〜25での追加なし |
Random.from(RandomGenerator) |
使える(Java 19で追加。この記事でも Collections.shuffle に渡して動作を確認) |
同じ |
RandomGenerator |
この記事のとおり | equiDoubles(double, double, boolean, boolean) がJava 22で追加(等間隔に並ぶ double のStreamを返す) |
RandomGenerator.getDefault() |
L32X64MixRandom |
実装要件として L32X64MixRandom を選ぶと記載 |
| スレッドセーフ・暗号用途の注意書き | この記事のとおり | 同じ記載 |
nextInt(origin, bound) はJava 17以降で使えます。Java 11や8で動かすコードでは、nextInt(max - min + 1) + min の形にします。
よくある質問
Q. Math.random() と Random はどちらを使えばよいですか?
A. 整数の乱数がほしいなら Random の nextInt が向いています。範囲をそのまま指定でき、キャストや丸めによるずれが起きません。Math.random() は0.0以上1.0未満の小数を1行で得たいときに便利です。なお、100万回呼んで最小値と最大値を調べたところ、最大は0.9999979…で、1.0は出ませんでした。
Q. nextInt(1, 7) がコンパイルエラーになります。
A. 引数2つの nextInt(origin, bound) はJava 17で加わりました。Java 11以前では nextInt(6) + 1 と書きます。java -version で実行しているバージョンを確かめてみてください。
Q. ループの中で毎回 new Random() してもよいですか?
A. 動作はしますが、1つ作って使い回すほうが無駄がなく、seedを固定したテストもしやすくなります。フィールドに private static final Random RANDOM = new Random(); と置くか、複数スレッドから呼ぶなら ThreadLocalRandom.current() を使います。
Q. 乱数が毎回同じ値になります。
A. new Random(42) のように固定のseedを渡していないかを確かめてください。同じseedからは必ず同じ列が出ます。毎回違う値がほしいときは、引数なしの new Random() にします。
まとめ
- 基本は
new Random().nextInt(bound)。 0以上 bound 未満を返し、bound そのものは出ない。 - 1〜6は
nextInt(1, 7)(Java 17以降)かnextInt(6) + 1。 min〜maxはnextInt(min, max + 1)またはnextInt(max - min + 1) + min。 - 範囲のずれは度数分布で確かめる。
+1忘れは0〜5に、nextInt(1, 6)は1〜5に、Math.roundを使うと両端だけ半分になる。 - 複数スレッドでは
ThreadLocalRandom.current()。 計測では、共有したRandomより4〜7倍速かった。 - 重複しない乱数は、候補をシャッフルして先頭から取る。 範囲が広く個数が少ないときはSetに貯める。
new Random(seed)で同じ列を再現できる。Math.random()・ThreadLocalRandom・SecureRandomは再現用には使えない。- パスワードやトークンは
SecureRandom。Randomは出力2つから3つ目を計算できた。Java 25でも使い分けは同じで、追加はRandomGenerator.equiDoubles(Java 22)のみ。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java乱数の作り方|Random nextIntの範囲指定と使い分け」
株式会社RJCでは、乱数の作り方と使い分けのようなJavaの基礎を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。