1. 程式人生 > >恕我直言,我懷疑你並不會生成隨機數

恕我直言,我懷疑你並不會生成隨機數

有一次,我在逛 Stack Overflow 的時候,發現有這樣一個問題:“Java 中如何產生一個指定範圍內的隨機數”,我心想,“就這破問題,竟然有 398 萬的閱讀量,統計確定沒搞錯?不就一個 Math.random() 的事兒嘛。”

於是我直接動用自己的權力投了一票反對。結果,沒等到權力執行後的喜悅,卻收到了一條提醒:“聲望值低於 125 的人有投票權,但不會公開顯示。”我呀,我去,扎心了。就衝我這急脾氣,不用程式碼證明一下自己的實力,我還有臉說自己有十年的開發經驗嗎?於是我興沖沖地就開啟 IDEA,敲下了下面這段程式碼:

public class GenerateMathRandomInteger {
    public static void main(String[] args) {
        int leftLimit = 2;
        int rightLimit = 11;

        Runnable r = () -> {
            int generatedInteger = leftLimit + (int) (Math.random() * rightLimit);
            System.out.println(generatedInteger);
        };
       for (int i = 1; i < 10; i++) {
           new Thread(r).start();
       }
    }
}

這段程式碼我寫得沒毛病吧?乍看上去,引數和類的命名都很合理,就連 Lambda 表示式也用上了。但程式輸出的結果卻出乎我的意料:

8
10
10
4
3
4
6
12
3

12 是從哪裡蹦出來的?當然是從程式的 bug 裡蹦出來。leftLimit + (int) (Math.random() * rightLimit) 生成的隨機數可能超出指定的範圍。不行,Math.random() 信不過,必須要換一種方法。靈機一動,我想到了 Random 類,於是我寫下了新的程式碼:

public class GenerateRandomInteger {
    public static void main(String[] args) {
        int leftLimit = 2;
        int rightLimit = 11;

        Random random = new Random();
        int range = rightLimit - leftLimit + 1;

        Runnable r = () -> {
            int generatedInteger = leftLimit + random.nextInt() % range;
            System.out.println(generatedInteger);
        };
       for (int i = 1; i < 10; i++) {
           new Thread(r).start();
       }
    }
}

這一次,我滿懷信心,Math.random() 解決不了的問題,random.nextInt() 就一定能夠解決。結果,輸出結果再次啪啪啪打了我這張帥臉。

0
-3
10
2
2
-4
-4
-6
6

竟然還有負數,這真的是殘酷的現實,我被教育了,似乎找回了剛入職那會被領導蹂躪的感覺。幸好,我的心態已經不像年輕時候那樣易怒,穩得一匹:出問題不要緊,找解決方案就對了。

於是 5 分鐘後我寫出了下面這段程式碼:

public class GenerateRandomInteger {
    public static void main(String[] args) {
        int leftLimit = 2;
        int rightLimit = 11;

        Random random = new Random();
        int range = rightLimit - leftLimit;

        Runnable r = () -> {
            int generatedInteger = leftLimit + (int)(random.nextFloat() * range);
            System.out.println(generatedInteger);
        };
       for (int i = 1; i < 10; i++) {
           new Thread(r).start();
       }
    }
}

無論是調整執行緒的數量,還是多次重新執行,結果都符合預期,在 2 - 11 之間。

7
2
5
8
6
2
9
9
7

nextFloat() 方法返回一個均勻分佈在 0 - 1 之間的隨機浮點數(包含 0.0f,但不包含 1.0f),乘以最大值和最小值的差,再強轉為 int 型別就可以保證這個隨機數在 0 到(最大值-最小值)之間,最後再加上最小值,就恰好可以得到指定範圍內的數字。

如果你肯讀原始碼的話,會發現 Random 類有一個 nextInt(int bound) 的方法,該方法會返回一個隨機整數,均勻分佈在 0 - bound 之間(包含 0,但不包含指定的 bound)。那麼利用該方法也可以得到一個有效的隨機數,來看示例程式碼。

public class GenerateRandomNextInt {
    public static void main(String[] args) {
        int leftLimit = 2;
        int rightLimit = 11;

        Random random = new Random();
        Runnable r = () -> {
            int generatedInteger = leftLimit + random.nextInt(rightLimit - leftLimit + 1);
            System.out.println(generatedInteger);
        };
       for (int i = 1; i < 10; i++) {
           new Thread(r).start();
       }
    }
}

由於 nextInt() 不包含 bound,因此需要 + 1。程式執行的結果也符合預期:

8
2
9
8
4
6
4
5
7

你看,我之前兩次嘗試都以失敗告終,但我仍然沒有放棄希望,經過自己的深思熟慮,我又找到了兩種可行的解決辦法。這讓我想起了普希金的一首詩歌:

假如生活欺騙了你,不要悲傷,不要心急,憂鬱的日子裡需要鎮靜,一切都會過去,一切都是瞬息,一切都會過去。希望之火需要再燃,需要呵護,不致讓暴風雨將其熄滅,不致讓自己在黑暗、陰冷、無助中絕望。

一首好詩吟完之後,我們再來想想還有沒有其他的方案。反正我是想到了,Java 7 以後可以使用 ThreadLocalRandom 類,程式碼示例如下:

public class GenerateRandomThreadLocal {
    public static void main(String[] args) {
        int leftLimit = 2;
        int rightLimit = 11;

        Runnable r = () -> {
            int generatedInteger = ThreadLocalRandom.current().nextInt(leftLimit, rightLimit +1);
            System.out.println(generatedInteger);
        };
       for (int i = 1; i < 10; i++) {
           new Thread(r).start();
       }
    }
}

程式輸出的結果如下:

11
9
6
10
6
6
10
7
3

ThreadLocalRandom 類繼承自 Random 類,它使用了內部生成的種子來初始化(外部無法設定,所以不能再現測試場景),並且不需要顯式地使用 new 關鍵字來建立物件(Random 可以通過構造方法設定種子),可以直接通過靜態方法 current() 獲取針對本地執行緒級別的物件:

static final ThreadLocalRandom instance = new ThreadLocalRandom();

static final void localInit() {
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    int probe = (p == 0) ? 1 : p; // skip 0
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    U.putLong(t, SEED, seed);
    U.putInt(t, PROBE, probe);
}

public static ThreadLocalRandom current() {
    if (U.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}

這樣做的好處就是,在多執行緒或者執行緒池的環境下,可以節省不必要的記憶體開銷。

最後,我再提供一個解決方案,使用 Apache Commons Math 類庫的 RandomDataGenerator 類。在使用該類庫之前,需要在 pom.xml 檔案中引入該類庫的依賴。

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-math3</artifactId>
    <version>3.6.1</version>
</dependency>

在需要生成指定範圍的隨機數時,使用 new RandomDataGenerator() 獲取隨機生成器例項,然後使用 nextInt() 方法直接獲取最大值與最小值之間的隨機數。來看示例。

public class RandomDataGeneratorDemo {
    public static void main(String[] args) {
        int leftLimit = 2;
        int rightLimit = 11;

        Runnable r = () -> {
            int generatedInteger = new RandomDataGenerator().nextInt( leftLimit,rightLimit);
            System.out.println(generatedInteger);
        };
       for (int i = 1; i < 10; i++) {
           new Thread(r).start();
       }
    }
}

輸出結果如下所示:

8
4
4
4
10
3
10
3
6

結果完全符合我們的預期——這也是我的最後一招,沒想到就這麼愉快地全交給你了。

好了,我親愛的讀者朋友,以上就是本文的全部內容了。如果覺得文章對你有點幫助,請微信搜尋「 沉默王二 」第一時間閱讀。示例程式碼已經上傳到 GitHub,傳送門~

我是沉默王二,一枚有趣的程式設計師。原創不易,莫要白票,請你為本文點個贊吧,這將是我寫作更多優質文章的最強動力。