Java Tips | コレクション:ランダム抽出

Java Java
スポンサーリンク

ランダム抽出は「誰を選ぶかを“公平に”決める」技

ランダム抽出は、ざっくり言うと
「コレクションの中から、ランダムに要素を取り出す」処理です。

テスト用にランダムなユーザーを 1 人選びたい。
キャンペーン当選者をランダムに 10 人選びたい。
おすすめ候補の中から、ランダムに 3 件だけ表示したい。

こういう「誰を選ぶかを固定したくない」「偏りを減らしたい」場面で、
ランダム抽出ユーティリティがあると、業務コードがかなり書きやすくなります。


基本形:List から 1 件だけランダムに取り出す

インデックスをランダムに決める

一番シンプルな「ランダム抽出」は、
「0〜size-1 のランダムなインデックスを決めて、その要素を返す」です。

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public final class Randoms {

    private Randoms() {}

    public static <T> T pickOne(List<T> source) {
        if (source == null || source.isEmpty()) {
            return null;
        }
        int index = ThreadLocalRandom.current().nextInt(source.size());
        return source.get(index);
    }
}
Java

使い方はこうなります。

List<String> names = List.of("山田", "佐藤", "鈴木", "田中");

String randomName = Randoms.pickOne(names);
System.out.println(randomName); // 毎回どれか1人
Java

ここでの重要ポイントは二つです。

一つ目は、「nextInt(source.size()) が 0 以上 size 未満のランダムなインデックスを返す」ということです。
これにより、すべての要素が同じ確率で選ばれます。

二つ目は、「ThreadLocalRandom.current() を使っている」ことです。
new Random() を毎回 new するより、スレッドごとの乱数生成器を使う ThreadLocalRandom の方が、実務では扱いやすくパフォーマンスも良いことが多いです。


複数件を「重複なし」でランダム抽出する

シャッフル+先頭から N 件、が一番分かりやすい

「1 件」ではなく「ランダムに 5 件欲しい」「当選者を 10 人選びたい」といった場合、
重複なしで取りたいことがほとんどです。

一番分かりやすいのは「シャッフルしてから先頭 N 件を取る」方法です。

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

public final class Randoms {

    private Randoms() {}

    public static <T> List<T> pickMany(List<T> source, int count) {
        if (source == null || source.isEmpty() || count <= 0) {
            return List.of();
        }
        if (count >= source.size()) {
            // 要求数が多すぎる場合は全部返す
            return new ArrayList<>(source);
        }

        List<T> copy = new ArrayList<>(source);
        Collections.shuffle(copy);  // ランダム順に並べ替え
        return copy.subList(0, count);
    }
}
Java

使い方はこうです。

List<String> names = List.of("山田", "佐藤", "鈴木", "田中", "高橋");

List<String> winners = Randoms.pickMany(names, 2);
System.out.println(winners); // 例: [鈴木, 山田]
Java

ここで深掘りしたい重要ポイントは三つです。

一つ目は、「シャッフル+先頭 N 件」で“重複なしのランダム抽出”を自然に表現できていること」です。
シャッフルで順番をランダムにしてから、上から N 件取るだけなので、ロジックが直感的です。

二つ目は、「元の List を直接シャッフルせず、コピーを作ってからシャッフルしている」ことです。
Collections.shuffle は List を破壊的に並べ替えるので、元の順番を壊さないように new ArrayList<>(source) を挟んでいます。

三つ目は、「count >= source.size() のときは“全部返す”という仕様をユーティリティ側で決めている」ことです。
「欲しい数が多すぎるときにどうするか」を一箇所で決めておくと、呼び出し側が迷わなくなります。


再現性のあるランダム抽出(テスト用)

シード付き Random を使う

テストや検証では、「ランダムだけど、毎回同じ結果になってほしい」ことがあります。
その場合は、Random にシード(種)を渡して使います。

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

public final class Randoms {

    private Randoms() {}

    public static <T> List<T> pickMany(List<T> source, int count, long seed) {
        if (source == null || source.isEmpty() || count <= 0) {
            return List.of();
        }
        if (count >= source.size()) {
            return new ArrayList<>(source);
        }

        List<T> copy = new ArrayList<>(source);
        Random random = new Random(seed);
        Collections.shuffle(copy, random);  // シード付きシャッフル
        return copy.subList(0, count);
    }
}
Java

使い方はこうです。

List<String> names = List.of("山田", "佐藤", "鈴木", "田中", "高橋");

List<String> winners1 = Randoms.pickMany(names, 2, 12345L);
List<String> winners2 = Randoms.pickMany(names, 2, 12345L);

System.out.println(winners1); // 毎回同じ結果
System.out.println(winners2); // winners1 と同じ
Java

ここでの重要ポイントは、
「同じシードを使えば、同じ“ランダム抽出結果”を再現できる」ということです。

テストコードで「ランダム抽出のロジックは使いたいが、結果は固定したい」ときに、
このパターンを使うととても便利です。


業務での使いどころ

例1:おすすめ候補からランダムに 3 件だけ表示する

スコア順に並んだおすすめ候補が 100 件あるとして、
その中からランダムに 3 件だけ表示したい、というケース。

List<Product> candidates = findRecommendedProducts(userId);

List<Product> random3 = Randoms.pickMany(candidates, 3);
Java

ここでのポイントは、
「スコア順の“上位 100 件”まではロジックで決めて、その中から“どれを見せるか”はランダムにする」という設計が簡単に書けることです。

例2:キャンペーン当選者をランダムに選ぶ

応募者一覧から、当選者を 10 人ランダムに選ぶ。

List<User> applicants = findApplicants(campaignId);

List<User> winners = Randoms.pickMany(applicants, 10);
Java

ここでのポイントは、
「重複なしで公平に選ぶ」という要件を、ユーティリティで保証していることです。
呼び出し側は「何人当選させるか」だけを考えればよくなります。


ランダム抽出の注意点

「本当にランダムでいいのか?」と「偏り」を意識する

ランダム抽出は便利ですが、
業務では次のような点を一度考えた方がいいです。

ランダムなので、「たまたま同じ人ばかり選ばれる」こともありうる。
ログや問い合わせ対応のときに、「なぜこの人が選ばれたのか」を説明しづらい。
セキュリティ的に厳密なランダム(暗号論的乱数)が必要な場面では、SecureRandom を使うべきこともある。

特に「抽選」「当選」などユーザーに影響が大きい処理では、
「ランダムの方法」「抽選ロジック」を仕様としてきちんと決めておくことが大事です。


まとめ:ランダム抽出ユーティリティで身につけてほしい感覚

ランダム抽出は、
単に「適当に選ぶテクニック」ではなく、
「誰を選ぶかを、意図的に“公平さ”と“再現性”のバランスを取りながら決める技術」です。

1 件だけなら「ランダムなインデックスで get」という基本形を押さえる。
複数件・重複なしなら「シャッフル+先頭 N 件」というパターンをユーティリティ化する。
テストや検証では、シード付き Random で「再現性のあるランダム抽出」を使う。
元の List を壊さないように、必ずコピーしてからシャッフルする。
「どこでランダムにするか」「どこは固定にするか」を、ビジネスの意図とセットで設計する。

あなたのコードのどこかに、
new Random().nextInt(...) をその場しのぎで書いている箇所があれば、
それを一度「ランダム抽出ユーティリティ」に整理できないか眺めてみてください。

その小さな整理が、
「ランダムさを、ちゃんと“設計された振る舞い”として扱えるエンジニア」への、
確かな一歩になります。

タイトルとURLをコピーしました