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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?