恕我直言,我懷疑你並不會生成隨機數
有一次,我在逛 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,傳送門~
我是沉默王二,一枚有趣的程式設計師。原創不易,莫要白票,請你為本文點個贊吧,這將是我寫作更多優質文章的最強動力。