“隨機數(shù)”的本質(zhì)代表了人類的不可預知性,這一特性在編程領域可以說是必不可少的。需要隨機數(shù)發(fā)揮關鍵性作用的例子幾乎隨處可見:游戲中的數(shù)值浮動、抽獎系統(tǒng)中的號碼生成、安全領域的秘鑰生成、服務器集群中的負載均衡……這些或大或小、或簡單或復雜的功能,都需要基于隨機數(shù)實現(xiàn)。
所以,本文的目的就是要用最簡單易懂的方式來介紹如何在Java中使用隨機數(shù)。
不過,在開始之前,首先需要明確的一點是:本文中介紹的Java自帶API生成的隨機數(shù)都是“偽隨機數(shù)”——生成它們的生成算法是一種叫做“線性同余”的算法,它接收一個數(shù)字作為種子,根據(jù)該種子輸出一個看似隨機的數(shù)字。這種算法的輸出雖然看似“隨機”,不過它的結(jié)果其實是有周期的(只是周期很長),而數(shù)論的知識又告訴我們:有周期的東西一定可以預言。這就宣判了Java API生成的隨機數(shù)并不是真正“隨機”的隨機數(shù),其生成結(jié)果其實取決于種子的數(shù)值,換句話說,使用相同種子生成的“隨機數(shù)”一定相同,無論試多少次。
聽到這里,可能會有不少初學者對其表示失望。不過別急著走,實際上我們只需每次都找一個不同的種子,那么也能夠令其生成的結(jié)果看起來無限接近于“隨機”。
那么如何才能找一個“每次都不一樣”的種子呢?需要注意的是,種子可以是有規(guī)律的——這也是隨機數(shù)生成函數(shù)存在的意義(否則還要它干嘛)。這樣的“種子”在現(xiàn)實生活中或者程序中幾乎隨處可見:例如從1997年1月1日0時0分0秒到你現(xiàn)在看這篇文章的時間的每一個毫秒數(shù)——就都能滿足要求(它們都不同,數(shù)量也足夠,而且還能繼續(xù)擴展)。
正因如此,“偽隨機數(shù)”在絕大多數(shù)開發(fā)環(huán)境下都完全能夠發(fā)揮出類似于“真隨機數(shù)”的效果(那么到底能不能用計算機生成“真隨機數(shù)”呢?其實也是可以的,不過這不是本文要討論的內(nèi)容)。
道理現(xiàn)在大家都懂了,接下來我們就用一些喜聞樂見的例子來展示如何在Java中使用隨機數(shù)。
在Java中,獲取隨機數(shù)的方法非常簡單。Java本身提供了兩個“開箱即用”的工具:一個是專門用于生成隨機數(shù)的工具類:java.util.Random;另一個則是Math類提供的用于生成隨機數(shù)的方法:Math.random()。
我們先從第一個,也是最容易理解的說起:Random類。
需要使用Random類,首先需要將其實例化。該類提供了兩種實例化方法:Random()和Random(long seed)
示例如下:
第一種構(gòu)造器是最貼心的——它會自動找一個“盡可能與同一個程序中其它使用該構(gòu)造方法生成的Random對象所使用的種子不同的種子”來構(gòu)造一個新對象。嗯……聽起來可能比較拗口,簡單地說就是它會盡可能保證該對象提供的隨機數(shù)是完全“隨機”的。
第二種構(gòu)造器則允許開發(fā)者使用指定的種子來生成隨機數(shù),其類型是long(它存在的意義是:如果某一天您發(fā)明了一種史無前例完美的種子生成算法,能確保生成“真隨機數(shù)”,那么這種構(gòu)造器就能派上用場)。
使用Random對象來生成一個隨機整形:
第一個方法是生成一個隨機整形,那么它能夠提供2^23種可能性。
第二種則是指定生成“0-給定范圍”的隨機數(shù)。例如上圖的示例中,其返回值將是0-99中的某一個數(shù)值。
很多情況下,上面提到的兩個方法足以應對絕大多數(shù)隨機數(shù)生成需求。不過,Random還提供了更多的隨機數(shù)生成形式:
上圖的示例中,從上至下依次用于:
1.生成一個隨機整形(前文已經(jīng)提到了)
2.生成一個隨機雙精度浮點型
3.生成一個隨機布爾型
4.生成一個隨機浮點型
5.生成一個隨機長整形
6.使用隨機生成的byte結(jié)果填滿給定的byte數(shù)組
可以看出,都十分簡單。
編程是一門實踐的藝術,F(xiàn)在,我們就嘗試使用上述技術做一個很簡單并且有趣的游戲:在我們的Java世界中有一個“戰(zhàn)士”角色,它(暫定為“它”)每次攻擊都會打出一個隨機的傷害數(shù)值(這就像你玩過的大多數(shù)游戲那樣)。當然,我們的“戰(zhàn)士”是訓練有素的,因此其傷害值一定是在100以上,不會比這個值還低。但是這個“戰(zhàn)士”的力量也不是無限大,所以它造成的傷害數(shù)值最大只能達到200。也就是說,每一次攻擊,這位戰(zhàn)士根據(jù)發(fā)揮情況的不同會造成100-200之間的一個隨機傷害。
那么,該如何使用上面提到的隨機數(shù)技術來模擬一下該“戰(zhàn)士”攻擊5次的效果呢?
首先,我們將“戰(zhàn)士類”創(chuàng)造出來:
可以看出,上圖設計的“戰(zhàn)士”可以執(zhí)行“攻擊”方法。不過現(xiàn)在這個方法只能返回0——這可不符合前文提到的要求。因此,我們需要賦予它一定范圍內(nèi)的“力量”:
因為最低傷害值也要到100,因此我們就用100來做返回值的“基底”,在此基礎上,浮動范圍也是100,因此我們加上一個0-101(不包括)之間的隨機數(shù)作為最終結(jié)果。
注意,這里只需使用一個Random實例即可,因為每次使用該實例執(zhí)行nextInt()方法時均會獲得一個新的隨機數(shù),這也是不給定種子時的效果。
OK,設計工作完成。是時候讓這個“戰(zhàn)士”來展示一下自己的實力了:
上面的代碼中,我們創(chuàng)建了一個“戰(zhàn)士”實例并循環(huán)執(zhí)行5次攻擊動作,效果是否像我們設計的那樣呢?
嗯,完美。雖然這一次這個“戰(zhàn)士”可能沒吃飽,攻擊力偏低了。我們再試一次:
可以看出,這次它的發(fā)揮要比上一次好很多。只要運氣好,次次都能打出200的逆天傷害也是有可能的(當然,概率學告訴我們這種情況非常少見)。
那么,如果我們給定一個“種子”,會怎么樣呢?例如這一次我們使用“1L”作為種子創(chuàng)建Random。你會發(fā)現(xiàn):此時,這個戰(zhàn)士無論進行多少次測試,每5次的傷害數(shù)值均完全一致,包括順序(這里就不再用圖片演示了,可以自行體驗)。這也就驗證了前文提到的“隨機數(shù)生成算法依賴于種子”的說法。
那么Random說完了,接下來再介紹一下Math工具類提供的random方法:
可以看出,這一方法也很簡單,它是一個靜態(tài)方法,因此直接使用Math類進行調(diào)用即可。每次調(diào)用它均會隨機地返回一個范圍為0-1(不包括)的double型數(shù)值(它無法指定種子,因此使用起來更加簡單)。
我相信,有些初學者在第一次看到它會覺得很奇怪:為何這個方法要返回一個如此怪異的結(jié)果?
實際上,你能通過這個值獲得任意范圍的隨機數(shù)。不信?我們接下來繼續(xù)上面的例子:
現(xiàn)在,我們的“戰(zhàn)士”經(jīng)過一段時間的刻苦努力,終于從1級的新手成長為99級的老手,傷害的可能值也成長為現(xiàn)在的100-999(下限沒變,上限提高了一大截)。
那么,我們該如何用Math提供的random方法來實現(xiàn)這一效果呢?
首先,我們依舊是采用100作為返回值的“基底”,那么浮動數(shù)值的取值范圍就是0-899(包括)之間?墒乔拔闹幸呀(jīng)提到了,Math提供的random方法只能返回一個隨機的、0-1之間的double數(shù)值。這該怎么辦?
先舉個簡單的例子,假如我要生成5-7(不包括)之間的隨機數(shù)(也就是只有5,6兩種取值),那么可以這樣獲。
返回值=5+(int)((7-5)*Math.random())
例如,某一次random返回了0.2,那么7-5=2,2*0.2=0.4,轉(zhuǎn)換為int類型后變?yōu)?,最終返回值5+0=5。嗯,這個值確實是5-7之間的一個隨機數(shù)。
再例如,某一次random反悔了0.8,那么7-5=2,2*0.8=1.6,轉(zhuǎn)換為int后還是1,此時返回值5+1=6,依舊滿足條件。
綜上,我們可以看出,使用0-1之間的小數(shù)產(chǎn)生M到N之間的隨機數(shù),可以根據(jù)以下公式獲得:
結(jié)果=M+(int)((M-N)*Math.random())
這樣一來,我們的戰(zhàn)士也就可以整裝待發(fā)了(之所以乘以900不是乘以899,是因為產(chǎn)生的隨機數(shù)無限接近但不包括最大值,因此我們要加上1):
如上,F(xiàn)在,我們再讓它來展示一下自己的實力:
效果如何呢?如下所示:
當然,和之前一樣,只要運氣足夠好,終有一天它能打出一刀999的傷害。
在此基礎上,還能添加暴擊效果,使其有一定幾率將傷害數(shù)值乘以2,或者添加miss效果,使其有一定幾率傷害為0……由此可見,按照這個原理繼續(xù)擴展的話,終有一天你也能寫出一套“只需體驗3分鐘”就能讓用戶愛上的款游戲作品了!
當然,那個目標可能并不容易實現(xiàn)。但是至少現(xiàn)在你應該已經(jīng)掌握了在Java中方使用隨機數(shù)的方法了,這也是本文的意義所在!
感謝閱讀。