Javaプログラミングにおいて、乱数の生成は非常に頻繁に使用される重要な機能の一つです。

ゲーム開発におけるキャラクターの行動決定やアイテムのドロップ率設定、シミュレーション、データ分析、さらにはセキュリティに関わるパスワード生成まで、その用途は多岐にわたります。

Javaには乱数を生成するための手段が複数用意されており、目的や用途に合わせて最適な方法を選択することが重要です。

初心者がまず触れることになる Math.random() から、標準的な Random クラス、マルチスレッド環境に適した ThreadLocalRandom、そして高度なセキュリティが求められる場面で使われる SecureRandom、さらにJava 17で導入された新しいインターフェースまで、それぞれの特徴と具体的な実装方法を詳しく解説します。

Math.random() を使用した手軽な乱数生成

Javaで最も手軽に乱数を得る方法は、java.lang.Math クラスの random() メソッドを使用することです。

このメソッドは、0.0以上1.0未満の double 型の値を返します。

Math.random() の基本動作

このメソッドは内部的に java.util.Random クラスのインスタンスを生成して使用しています。

そのため、手軽に利用できる反面、細かい制御には向きません。

Java
public class MathRandomExample {
    public static void main(String[] args) {
        // 0.0以上1.0未満の乱数を生成
        double val = Math.random();
        System.out.println("生成された乱数: " + val);
    }
}
実行結果
生成された乱数: 0.7329184562014728

範囲を指定して整数を取得する方法

Math.random() を使って特定の範囲 (例えば1から10まで) の整数を取得したい場合は、計算式を工夫する必要があります。

基本となる計算式は (int)(Math.random() * 範囲) + 最小値 です。

Java
public class MathRandomRange {
    public static void main(String[] args) {
        int min = 1;
        int max = 10;
        // 1から10の範囲で整数を生成
        // Math.random() * (10 - 1 + 1) + 1 -> Math.random() * 10 + 1
        int randomNumber = (int)(Math.random() * (max - min + 1)) + min;
        
        System.out.println("1から10の乱数: " + randomNumber);
    }
}
実行結果
1から10の乱数: 7

注意点として、キャスト演算子 (int) は Math.random() * 範囲 全体にかける必要があることに注意してください。

括弧を忘れると、常に0になってしまうなどのバグの原因となります。

java.util.Random クラスによる標準的な乱数生成

より柔軟に、かつ効率的に乱数を生成したい場合は、java.util.Random クラスを使用します。

このクラスは、整数、浮動小数点、真偽値など、様々な型の乱数を生成するメソッドを備えています。

Randomクラスの基本的な使い方

Random クラスを使用するには、まずインスタンスを生成します。

Java
import java.util.Random;

public class RandomExample {
    public static void main(String[] args) {
        Random rand = new Random();

        // 次のint型乱数 (全範囲)
        int i = rand.nextInt();
        // 0から9までの整数
        int i10 = rand.nextInt(10);
        // double型 (0.0~1.0未満)
        double d = rand.nextDouble();
        // boolean型 (trueかfalse)
        boolean b = rand.nextBoolean();

        System.out.println("int: " + i);
        System.out.println("0-9: " + i10);
        System.out.println("double: " + d);
        System.out.println("boolean: " + b);
    }
}
実行結果
int: -1429384756
0-9: 4
double: 0.12837465928374
boolean: true

シード値 (Seed) の概念

Random クラスのコンストラクタに 「シード値」を渡すと、常に同じパターンの乱数系列が生成されます。

これはデバッグや、ゲームのリプレイ機能などで再現性を持たせたい場合に非常に有効です。

Java
import java.util.Random;

public class SeedExample {
    public static void main(String[] args) {
        // 同じシード値 100L を指定
        Random rand1 = new Random(100L);
        Random rand2 = new Random(100L);

        System.out.println("rand1: " + rand1.nextInt(100));
        System.out.println("rand2: " + rand2.nextInt(100));
        // 両者は必ず同じ結果になる
    }
}
実行結果
rand1: 15
rand2: 15

シード値を指定しない場合、内部的には現在時刻 (ナノ秒単位) などに基づいたユニークな値が自動的に割り当てられます。

ThreadLocalRandom によるパフォーマンスの最適化

マルチスレッド環境下で java.util.Random クラスの単一インスタンスを共有すると、競合 (コンテンション) が発生し、パフォーマンスが低下する可能性があります。

これを解決するために導入されたのが java.util.concurrent.ThreadLocalRandom です。

ThreadLocalRandom のメリット

ThreadLocalRandom はスレッドごとに独立した乱数生成器を持つため、マルチスレッドでのオーバーヘッドがほぼゼロになります。

また、範囲指定のメソッドが充実しており、記述が簡潔になる点も大きなメリットです。

Java
import java.util.concurrent.ThreadLocalRandom;

public class ThreadLocalRandomExample {
    public static void main(String[] args) {
        // インスタンス化せず current() メソッドで取得
        ThreadLocalRandom rand = ThreadLocalRandom.current();

        // 10から20の間の乱数を生成 (20は含まない)
        int randomVal = rand.nextInt(10, 20);
        
        // long型での範囲指定も可能
        long randomLong = rand.nextLong(1000L, 2000L);

        System.out.println("10-19の整数: " + randomVal);
        System.out.println("1000-1999の整数: " + randomLong);
    }
}
実行結果
10-19の整数: 14
1000-1999の整数: 1582

現代のJava開発において、特に高いパフォーマンスが求められる場合は、Randomクラスよりも ThreadLocalRandom の使用が推奨されます。

SecureRandom による強力なセキュリティ

パスワード生成や暗号化キーの作成、セッショントークンの発行など、予測不可能な乱数が必要なセキュリティ上の用途には、SecureRandom を使用しなければなりません。

なぜ SecureRandom なのか

Random クラスや ThreadLocalRandom は「擬似乱数生成器 (PRNG)」であり、アルゴリズムに基づいて計算されます。

そのため、出力された数値の傾向から次の数値を予測されるリスクがあります。

一方で java.security.SecureRandom は、OSが提供するエントロピー (予測困難なノイズ) を利用するため、暗号学的に安全な乱数を提供します。

Java
import java.security.SecureRandom;
import java.util.Base64;

public class SecureRandomExample {
    public static void main(String[] args) {
        SecureRandom secureRand = new SecureRandom();

        // 安全なトークンを生成する例
        byte[] values = new byte[16];
        secureRand.nextBytes(values);
        
        String token = Base64.getUrlEncoder().withoutPadding().encodeToString(values);
        System.out.println("安全なトークン: " + token);
    }
}
実行結果
安全なトークン: 7dF_k9p2RzW8qLmN3XvA5g

SecureRandom は生成コストが高く (処理が遅く)、システムのリソースを消費するため、単なるゲームのランダム要素などに使うのは過剰です。

用途を見極めて使い分けることが重要です。

Java 17 以降の新しい乱数生成: RandomGenerator

Java 17では、乱数生成の仕組みが大幅に刷新され、java.util.random.RandomGenerator インターフェースが導入されました。

これにより、従来の Random クラスの階層構造が整理され、新しいアルゴリズム (LXMアルゴリズムなど) が利用可能になりました。

RandomGenerator の利用方法

これまで紹介したクラスもこのインターフェースを実装していますが、新しいファクトリメソッドを使うことで、最適なアルゴリズムを名前で指定してインスタンス化できます。

Java
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;

public class ModernRandomExample {
    public static void main(String[] args) {
        // デフォルトのアルゴリズムで生成
        RandomGenerator gen = RandomGenerator.getDefault();
        
        // 特定のアルゴリズムを指定 (例: L64X128MixRandom)
        RandomGenerator customGen = RandomGenerator.of("L64X128MixRandom");

        System.out.println("デフォルト乱数: " + gen.nextInt(100));
        System.out.println("カスタムアルゴリズム乱数: " + customGen.nextInt(100));
    }
}
実行結果
デフォルト乱数: 42
カスタムアルゴリズム乱数: 87

Java 17 以降の環境でライブラリを設計したり、高度なシミュレーションを行う場合は、この RandomGenerator インターフェースを利用するのが最新のベストプラクティスです。

実践的なテクニックと応用

ここからは、開発現場でよく遭遇する特定のニーズに合わせた乱数生成テクニックを紹介します。

Stream API を活用した複数の乱数生成

Java 8から導入された Stream API を使うと、複数の乱数をまとめて生成し、処理するのが非常に簡単になります。

Java
import java.util.Random;
import java.util.List;
import java.util.stream.Collectors;

public class StreamRandomExample {
    public static void main(String[] args) {
        Random rand = new Random();

        // 1から100の乱数を10個生成し、リストに格納
        List<Integer> randomNumbers = rand.ints(10, 1, 101)
                                         .boxed()
                                         .collect(Collectors.toList());

        System.out.println("生成されたリスト: " + randomNumbers);

        // 平均値を求める
        double average = rand.ints(100, 1, 101).average().orElse(0.0);
        System.out.println("100個の平均: " + average);
    }
}
実行結果
生成されたリスト: [23, 89, 4, 56, 12, 98, 45, 33, 71, 10]
100個の平均: 52.41

重複のない乱数 (シャッフル)

例えば、「1から10までの数字をランダムな順番で1回ずつ使いたい」という場合、単純に乱数を生成すると重複が発生してしまいます。

このような場合は、リストに要素を詰めてからシャッフルする手法が最も効率的です。

Java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class UniqueRandomExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            numbers.add(i);
        }

        // リストの中身をランダムに並び替え
        Collections.shuffle(numbers);

        System.out.println("重複なし乱数: " + numbers);
    }
}
実行結果
重複なし乱数: [5, 2, 9, 1, 10, 3, 7, 4, 8, 6]

重み付き乱数の実装

「アイテムAは50%、アイテムBは30%、アイテムCは20%の確率で出現させたい」といった重み付きの抽選は、合計値を基準にした閾値判定で実装します。

Java
import java.util.Random;

public class WeightedRandom {
    public static void main(String[] args) {
        Random rand = new Random();
        int p = rand.nextInt(100); // 0~99

        String item;
        if (p < 50) {
            item = "アイテムA (50%)";
        } else if (p < 80) { // 50 + 30
            item = "アイテムB (30%)";
        } else {
            item = "アイテムC (20%)";
        }

        System.out.println("抽選結果: " + item);
    }
}

各乱数生成手法の比較

用途に合わせて最適なクラスを選択できるよう、それぞれの特徴を以下の表にまとめました。

クラス名適した用途スレッド安全性特徴
Math.random()極めて簡単な検証、一行で書きたい時安全 (内部でRandom共有)0.0~1.0未満のみ。整数は計算が必要。
Random一般的なアプリケーション開発安全 (ただし競合あり)最も標準的。シード値による再現が可能。
ThreadLocalRandom高負荷・マルチスレッド環境安全 (スレッドごとに独立)高速。範囲指定メソッドが使いやすい。
SecureRandomセキュリティ、暗号、認証トークン安全予測不可能。処理は比較的遅い。
RandomGeneratorJava 17以降の最新開発アルゴリズムに依存柔軟なアルゴリズム選択が可能。

基本的には、単一スレッドなら Random、マルチスレッドなら ThreadLocalRandom、セキュリティが絡むなら SecureRandom という使い分けを徹底しましょう。

乱数生成における注意点とベストプラクティス

最後に、乱数を扱う際に陥りやすい罠と、より良いコードを書くためのポイントを解説します。

インスタンスの再利用を心がける

java.util.Random クラスを使用する際、メソッドが呼ばれるたびに new Random() をするのは避けてください。

非常に短いスパンでインスタンス化を繰り返すと、シード値の元となるシステム時刻が同じになってしまい、同じ数値が連続して生成されるリスクがあります。

通常はクラスのフィールドとして保持するか、シングルトン的に扱うのが適切です。

境界値のミスを防ぐ

nextInt(int bound) メソッドは、0から bound - 1 までの値を返します。

bound そのものは含まれない (排他的) という点は、オフセットエラー (1ずれるバグ) を起こしやすいため、常に意識しておきましょう。

テストコードでの乱数

テストコードにおいて、結果が毎回変わる乱数は厄介です。

ロジックのテストを行う場合は、コンストラクタで Random インスタンスを注入できるように設計 (依存性の注入) し、テスト時には固定のシード値を持つ Random を渡すことで、結果の再現性を確保するのがプロのテクニックです。

まとめ

Javaにおける乱数生成は、単一のメソッドだけでなく、用途に応じた複数のクラスが提供されています。

  • 手軽に使うなら Math.random()
  • 標準的な利用なら Random クラス
  • パフォーマンス重視なら ThreadLocalRandom
  • セキュリティ重視なら SecureRandom
  • Java 17以降で高度な制御を行うなら RandomGenerator

これらの特徴を正しく理解し、適切な範囲指定や型の選択を行うことで、バグが少なく、かつ意図した通りのランダム性を備えたプログラムを構築することができます。

特に現代のJava開発では、パフォーマンスやセキュリティの観点からクラスを使い分ける知識が不可欠です。

本記事を参考に、それぞれのメソッドを自身のプロジェクトで活用してみてください。