本ページはプロモーションが含まれています

Java入門

Java乱数の基本4選!Randomクラスの正しい使い方

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

Web開発やアプリ制作において、javaで乱数を使いたい場面は意外と多いものです。

ゲームのアイテムドロップ確率、テストデータの生成、さらにはセキュリティに関わるパスワードリセットトークンの生成など、さまざまなシーンで乱数を利用するコードを見てきました。

しかし、「とりあえずMath.random()を使っている」という方が非常に多いのも事実です。簡単な用途なら問題ありませんが、javaには用途に応じた複数の乱数クラスが用意されています。これらを理解せずに使うと、パフォーマンスの低下や、重大なセキュリティ上の欠陥につながる可能性も否定できません。

この記事は、「javaの乱数について基礎からきちんと知りたい」と考える初学者の方に向けて書いています。

この記事を読めば、以下の4つの主要なjava 乱数生成方法を理解できます。

  1. Math.random()
  2. Random クラス
  3. ThreadLocalRandom クラス
  4. SecureRandom クラス

それぞれの特徴と正しい使い分けを学び、自信を持ってjavaの乱数を扱えるようになりましょう。

Javaで乱数を使うシーンとは?

java プログラミングにおいて、乱数は「予測できない値」が必要なあらゆる場面で活躍します。具体的な利用シーンを見ていきましょう。

ゲーム開発やシミュレーションで使われる理由

乱数が最も直感的に使われるのがゲーム開発です。

例えば、以下のような処理に使われます。

  • 敵を倒したときのアイテムドロップ率(10%の確率でレアアイテムをドロップする、など)
  • 敵キャラクターの行動パターン(50%の確率で攻撃、50%の確率で防御する)
  • 攻撃時のダメージ計算(100〜120の間でランダムなダメージを与える)
  • マップの自動生成

もし乱数がなければ、敵は毎回同じアイテムを落とし、毎回同じ行動をとる予測可能なゲームになってしまいます。乱数が「ランダム性」や「意外性」を生み出し、ゲームを面白くする重要な要素となるのです。

また、現実世界の現象を模倣するシミュレーションでも乱数は不可欠です。例えば、交通渋滞のシミュレーションでは、車がランダムなタイミングで発生することで、現実的なモデルを作れます。

テストデータを自動生成する場面

プログラムが正しく動作するか確認する「テスト」の場面でも、乱数は役立ちます。

システムをテストする際、大量のダミーデータが必要になることがあります。例えば、ECサイトの会員情報テストのために、1,000人分の架空のユーザーデータ(年齢、性別、住所など)が必要だとします。

これを手作業で入力するのは大変です。しかし、乱数を使えば、年齢を「18歳から80歳までのランダムな整数」として自動生成できます。

// 例: 18歳から80歳までの年齢をランダムに生成
// (Randomクラスの使い方は後ほど詳しく解説します)
import java.util.Random;

Random rand = new Random();
// nextInt(bound) は 0 から bound-1 までの値を返す
// (80 - 18 + 1) = 63
// 0〜62 の乱数 + 18 = 18〜80 の乱数
int age = rand.nextInt(63) + 18; 

このように乱数を利用することで、バリエーションに富んだテストデータを効率良く大量に用意できるのです。

確率的な処理を実装するケース

特定の確率に基づいて処理を分岐させたい場合にも、javaの乱数が使われます。

代表的な例が「A/Bテスト」です。WebサイトのデザインAとデザインBのどちらがより効果的か試したい場合、アクセスしてきたユーザーをランダムに振り分けます。

  • 50%のユーザーにはデザインAを表示
  • 50%のユーザーにはデザインBを表示

このような処理は、0から1までの乱数を生成し、0.5未満ならA、0.5以上ならB、といったロジックで簡単に実装可能です。

他にも、抽選システムや、機械学習における「モンテカルロ法」のような確率的アルゴリズムにも、乱数の考え方が応用されています。

Javaで乱数を生成する基本方法

javaには乱数を生成する方法がいくつか用意されています。ここでは最も基本的でよく使われる4つの方法を紹介します。

Math.random()の使い方と特徴

Math.random() は、javaで最も手軽に乱数を使えるメソッドです。

static メソッドであるため、クラスのインスタンス化(new)をせずに、Math.random() と書くだけで呼び出せます。

// Math.random() の呼び出し
double randomValue = Math.random();
System.out.println(randomValue); 
// 出力例: 0.845... や 0.123... など

このメソッドには重要な特徴が2つあります。

  1. 0.0以上、1.0未満の double 型(小数)の値を返す
    • 0.0 は含まれますが、1.0 は含まれません (0.0 <= 戻り値 < 1.0)。
  2. 内部的には Random クラスが使われている
    • 手軽ですが、機能は Random クラスの nextDouble() と同じです。

単純に0から1未満の小数が欲しい場合に適しています。

Randomクラスを使う方法

java.util.Random クラスは、javaで乱数を扱う際の基本となるクラスです。

Math.random() と違い、最初に new Random() としてインスタンスを生成する必要があります。

import java.util.Random;

// 1. Randomクラスのインスタンスを生成
Random rand = new Random();

// 2. 乱数を取得するメソッドを呼ぶ
int intValue = rand.nextInt();        // int型の範囲でランダムな整数
int intValueBounded = rand.nextInt(10); // 0以上、10未満のランダムな整数
double doubleValue = rand.nextDouble(); // 0.0以上、1.0未満のランダムな小数
boolean boolValue = rand.nextBoolean(); // true または false

Random クラスは Math.random() よりも多機能です。nextInt() で整数、nextDouble() で小数、nextBoolean() で真偽値など、さまざまな型の乱数を生成できます。汎用的な乱数が必要な場合は、Math.random() よりも Random クラスの利用が推奨されます。

ThreadLocalRandomとの違い

java.util.concurrent.ThreadLocalRandom クラスは、マルチスレッド環境での利用に特化した乱数クラスです。

使い方は Random クラスと似ていますが、new でインスタンスを生成しません。ThreadLocalRandom.current() という static メソッドを使って、現在のスレッド専用の乱数ジェネレータを取得します。

import java.util.concurrent.ThreadLocalRandom;

// 現在のスレッド用のジェネレータを取得
ThreadLocalRandom tlr = ThreadLocalRandom.current();

// 使い方 (Randomクラスとほぼ同じ)
int intValue = tlr.nextInt(10); // 0以上、10未満の整数
double doubleValue = tlr.nextDouble(); // 0.0以上、1.0未満の小数

Random クラスのインスタンスを複数のスレッドで共有すると、処理の競合が発生してパフォーマンスが低下する恐れがあります。ThreadLocalRandom は、スレッドごとに独立した乱数ジェネレータを管理するため、競合が発生しません。

Webサーバーのような並列処理が前提の環境では、Random よりも ThreadLocalRandom を使う方が高速です。

範囲を指定した乱数の作り方(0〜10など)

実務で最もよく使うのが「特定の範囲の乱数」の生成です。例えば「0から10まで」や「5から10まで」といった整数です。

Math.random() を使う場合

Math.random() は 0.0 以上 1.0 未満の小数を返します。これを利用して範囲指定の整数を作るには、少し計算が必要です。

「(最大値 - 最小値 + 1) を掛けて、最小値を足す」

  • 例1: 0〜10 (両端を含む) の整数
    • 最大値 = 10, 最小値 = 0
    • 欲しい個数 = (10 - 0 + 1) = 11個 (0, 1, ..., 10)
    • Math.random() * 11 は、0.0 以上 11.0 未満の小数
    • これを (int) でキャスト(整数化)すると、0〜10 の整数が得られます。
// 0〜10 のランダムな整数
int min = 0; int max = 10;
int randomInt = (int)(Math.random() * (max - min + 1)) + min;
System.out.println(randomInt);
  • 例2: 5〜10 (両端を含む) の整数
    • 最大値 = 10, 最小値 = 5
    • 欲しい個数 = (10 - 5 + 1) = 6個 (5, 6, 7, 8, 9, 10)
    • Math.random() * 6 は、0.0 以上 6.0 未満 (→ 整数化で 0〜5)
    • これに最小値 5 を足します。
// 5〜10 のランダムな整数
int min = 5;
int max = 10;
int randomInt = (int)(Math.random() * (max - min + 1)) + min;
System.out.println(randomInt);

Random または ThreadLocalRandom を使う場合

Random クラス (または ThreadLocalRandom) には、範囲指定を簡単にするメソッドが用意されています (Java 1.7以降)。

  • nextInt(int bound): 0以上、bound 未満 の整数を返します。

この bound に「欲しい個数」を指定するのがコツです。

  • 例1: 0〜10 (両端を含む) の整数
    • 欲しい個数は 11個 (0〜10)
    • bound に 11 を指定します。
Random rand = new Random();
// 0以上、11未満 (つまり 0〜10) の整数
int randomInt = rand.nextInt(11);
System.out.println(randomInt);
  • 例2: 5〜10 (両端を含む) の整数
    • 欲しい個数は 6個 (5〜10)
    • bound に 6 を指定します。rand.nextInt(6) は 0〜5 を返します。
    • これに最小値 5 を足します。
Random rand = new Random();
int min = 5; int max = 10;
// (max - min + 1) = 6
// 0〜5 の乱数 + 5 = 5〜10 の乱数
int randomInt = rand.nextInt(max - min + 1) + min; System.out.println(randomInt);

Random クラスを使う方が、計算式が直感的で間違いが少ないためおすすめです。

乱数を整数・小数で扱う方法

Random クラス(または ThreadLocalRandom)を使えば、整数と小数の乱数を簡単に生成できます。

小数の乱数を生成する

nextDouble() メソッドを使います。

このメソッドは、Math.random() と同様に、0.0以上、1.0未満double 型の値を返します。

Random rand = new Random();
double d1 = rand.nextDouble(); // 0.0 <= d1 < 1.0
System.out.println(d1);

もし「0.0以上、50.0未満」のように範囲を指定したい場合は、結果を乗算します。

Random rand = new Random();
// 0.0〜1.0 の乱数に 50.0 を掛ける
double d50 = rand.nextDouble() * 50.0; // 0.0 <= d50 < 50.0
System.out.println(d50);

整数の乱数を生成する

nextInt() メソッドを使います。

nextInt() には引数がある場合とない場合があります。

  • nextInt() (引数なし):int 型が取りうる全範囲(約-21億から+21億)のランダムな整数を返します。
Random rand = new Random();
int i = rand.nextInt();
// -2,147,483,648 〜 2,147,483,647
System.out.println(i);
  • nextInt(int bound) (引数あり):0以上、bound 未満 のランダムな整数を返します。bound には正の数を指定しなくてはなりません。実用上、こちらを使う場面が圧倒的に多いです。
Random rand = new Random();
// 0以上、100未満 (0〜99)
int i100 = rand.nextInt(100);
System.out.println(i100); 

負の数を含む範囲で乱数を作る

「-10から+10まで」のように、負の数を含む範囲の乱数を作るには、nextInt(bound) と最小値の加算を組み合わせます。

公式は先ほどと同じです。

rand.nextInt(最大値 - 最小値 + 1) + 最小値

  • 例: -10〜+10 (両端を含む) の整数
    • 最大値 = 10最小値 = -10欲しい個数 (bound) = 10 - (-10) + 1 = 10 + 10 + 1 = 21最小値 = -10
    nextInt(21) は 0〜20 の整数を返します。これに最小値 -10 を足します。この計算式を覚えておけば、javaであらゆる範囲の整数乱数を生成できます。
Random rand = new Random();
int min = -10;
int max = 10;
int bound = max - min + 1; // 21
int randomInt = rand.nextInt(bound) + min;
System.out.println(randomInt); // -10 から 10 までの値が出力される

より安全な乱数を使いたい場合

これまで紹介した RandomThreadLocalRandom は、ゲームやシミュレーションには十分です。しかし、セキュリティが要求される用途には使えません。

SecureRandomの使い方

暗号化キー、セッショントークン、パスワード生成など、セキュリティ上「予測不可能」であることが極めて重要な場合には、java.security.SecureRandom クラスを使います。

使い方は Random クラスと似ています。

import java.security.SecureRandom;

// 1. SecureRandomのインスタンスを生成
SecureRandom secRand = new SecureRandom();

// 2. 乱数を取得する (Randomクラスと同じメソッドが使える)
int intValue = secRand.nextInt(100); // 0〜99
double doubleValue = secRand.nextDouble(); // 0.0〜1.0

// セキュリティ用途では byte配列 を使うことが多い
byte[] randomBytes = new byte[16];
secRand.nextBytes(randomBytes); // 16バイトのランダムな値で配列を満たす

SecureRandom は、OSが提供するランダムネス(マウスの動き、ネットワークのタイミングなど)を利用するため、Random よりも生成コストが重くなります。しかし、その分、はるかに予測困難な乱数を生成します。

暗号用途でRandomを使ってはいけない理由

Random クラス(Math.randomThreadLocalRandom も含む)を暗号用途に使ってはいけません。

理由は、これらが 「疑似乱数ジェネレータ (PRNG)」 であり、そのアルゴリズムが 「予測可能」 だからです。

Random クラスは、「シード (Seed) 値」と呼ばれる初期値をもとに、特定の計算式で次の乱数を生成します。もし攻撃者がこの「シード値」やアルゴリズムを知ってしまうと、過去と未来の乱数系列をすべて再現できてしまいます。

例えば、パスワードリセットトークンを Random で生成したとします。攻撃者がそのパターンを解析し、次に生成されるトークンを予測できれば、他人のアカウントを乗っ取れてしまいます。

一方、SecureRandom「暗号論的に強力な疑似乱数ジェネレータ (CSPRNG)」 です。内部のシード値が予測不可能であり、たとえいくつかの出力値が知られても、次の値を予測することは事実上不可能です。

セキュリティが関わる場所では、必ず SecureRandom を使う。これは java 開発における鉄則です。

乱数の再現性とシード値とは?

Random クラスが「予測可能」である性質は、セキュリティ面では弱点です。しかし、テストやデバッグの際には、逆に大きなメリットとなります。

シード値を指定して同じ乱数を生成する方法

Random クラスは、インスタンス化の際に「シード値」と呼ばれる long 型の数値を渡せます。

Random rand = new Random(シード値);

シード値を指定すると、その Random インスタンスは、必ず同じ順番で同じ乱数系列を生成します。

// 1. シード値 12345L でインスタンスを生成
Random rand1 = new Random(12345L);
System.out.println("--- rand1 ---");
System.out.println(rand1.nextInt(100)); // 1回目
System.out.println(rand1.nextInt(100)); // 2回目
System.out.println(rand1.nextInt(100)); // 3回目

// 2. もう一度、同じシード値 12345L でインスタンスを生成
Random rand2 = new Random(12345L);
System.out.println("--- rand2 ---");
System.out.println(rand2.nextInt(100)); // 1回目
System.out.println(rand2.nextInt(100)); // 2回目
System.out.println(rand2.nextInt(100)); // 3回目

このコードを実行すると、rand1rand2 の出力は(例えば 85, 88, 47 ... のように)完全に一致します。

ちなみに、シード値を指定しない new Random() の場合、現在時刻(ナノ秒)などがシード値として自動的に使われます。そのため、実行するたびに異なる乱数系列が生成されるのです。

テストやデバッグでシード値を使うメリット

この「乱数を再現できる」性質は、バグの特定に絶大な効果を発揮します。

例えば、乱数を使うゲームで「特定の状況でだけバグが発生する」とします。もしシード値を指定していなければ、そのバグを再現するのは困難です。

しかし、テスト実行時にシード値を固定(または記録)しておけば、バグが発生した際のシード値を使って、何度でも 「バグが発生した状況」を100%再現 できます。

バグの再現は、デバッグ(バグ修正)において最も重要なステップです。Random のシード値は、再現性の担保に役立ちます。

乱数を使うときの注意点

javaで乱数を扱う際には、パフォーマンスや正確性の観点から、いくつか知っておくべき注意点があります。

毎回new Random()を呼ばないほうがいい理由

初心者にありがちな間違いが、乱数が必要になるたびに new Random() を呼び出すことです。

// 悪い例: ループの中で毎回 new Random() する
public void printRandoms() {
    for (int i = 0; i < 100; i++) {
        // 非常に短い時間で new が繰り返される
        Random rand = new Random(); 
        System.out.println(rand.nextInt());
    }
}

このコードには重大な問題があります。

new Random() は、シード値に現在時刻を使います。もしコンピュータの処理が非常に速く、ループが1ミリ秒未満で何度も実行されると、複数の Random インスタンスが まったく同じシード値(同じ現在時刻)で生成される可能性があります。

同じシード値で生成されたインスタンスは、同じ乱数を返します。

その結果、printRandoms() の出力が「5, 5, 5, 12, 12, 12, 12, ...」のように、同じ数値ばかりになってしまう危険性があるのです。

Random インスタンスは、一度だけ生成し、それを使い回すのが正しい使い方です。

// 良い例: インスタンスを使い回す
// (フィールドとして保持する)
private Random rand = new Random(); 

public void printRandoms() {
    for (int i = 0; i < 100; i++) {
        // 同じインスタンスを使い回す
        System.out.println(rand.nextInt());
    }
}

並列処理での乱数の扱い方

Random インスタンスは使い回す」のが基本です。しかし、Webサーバーのように複数のスレッドが同時にそのインスタンスにアクセスする環境では、別の問題が発生します。

Random クラスはスレッドセーフです。しかし、その安全性を担保するために、内部で「ロック」と呼ばれる仕組みが働きます。

スレッドAが乱数を使っている間、スレッドBはロックが解除されるまで待たされます。多数のスレッドが殺到すると、この「待ち時間」がボトルネックとなり、システム全体のパフォーマンスが大きく低下します。

この問題を解決するのが ThreadLocalRandom です。

ThreadLocalRandom.current() は、スレッドごとに独立した乱数ジェネレータを提供します。ロックが発生しないため、マルチスレッド環境でも極めて高速に動作します。並列処理が想定される場面では、Random ではなく ThreadLocalRandom を選びましょう。

一見ランダムに見えて偏るケース

乱数の範囲指定で、nextInt(bound) を使わずに %(剰余) を使うと、乱数に「偏り」が発生する場合があります。

// 偏りが発生する可能性のある悪い例
Random rand = new Random();
// 0〜9 の乱数が欲しいとする
int randomInt = Math.abs(rand.nextInt()) % 10; 

rand.nextInt() は、int の全範囲(約42億通り)の値を返します。この全範囲の個数が、もし10で割り切れない場合、% 10 の結果(0〜9)は均等に出現しません。

例えば、Integer.MAX_VALUE (2,147,483,647) は10で割り切れません。そのため、0〜7 の数字は、8や9 よりもわずかに多く出現してしまいます。

これは非常に微妙な偏りであり、ほとんどの用途では問題になりません。しかし、統計やシミュレーションの正確性が求められる分野では、この偏りが結果に影響を与える可能性があります。

Java 1.7以降で推奨される rand.nextInt(10) は、このような偏りが発生しないように内部で調整されています。範囲指定の整数が欲しい場合は、% ではなく nextInt(bound) を使うのが安全で確実です。

まとめ:用途に応じて乱数クラスを使い分けよう

Javaには複数の乱数生成方法がありますが、それぞれに明確な役割があります。

汎用 → Random

  • new Random()
  • シングルスレッド環境(通常のバッチ処理やシンプルなアプリ)で、セキュリティを問わない一般的な乱数を扱う場合の基本です。
  • シード値を指定できるため、テストでの再現性担保にも役立ちます。

並列処理 → ThreadLocalRandom

  • ThreadLocalRandom.current()
  • Webサーバーや並列処理など、マルチスレッド環境で使うべき選択肢です。
  • ロックによる競合が発生せず、パフォーマンスが最も優れています。

セキュリティ → SecureRandom

  • new SecureRandom()
  • パスワード、トークン、暗号キーなど、予測不可能性が求められるセキュリティ関連の処理で 必須 です。
  • 他のクラスよりも生成コストは高いですが、最も安全です。

javaの乱数を正しく使い分けることは、バグが少なく、パフォーマンスが高く、安全なアプリケーションを作るための第一歩です。

  • この記事を書いた人
  • 最新記事

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

-Java入門