最近在stackoverflow上看到壹段代碼。
只是第壹眼看上去很傻,明白了就直接跪下了!
讓我先給妳看看stackoverflow上的這個問題是什麽,然後引出這段代碼:
問題特別簡單,就壹句話:
誰能給我解釋壹下:為什麽這段代碼使用隨機字符串打印出hello world?
代碼也很簡單。讓我把它拿出來給妳看看:
您也可以將上面的代碼直接粘貼到您的運行環境中,並運行它,看看它是否也輸出為hello world:
我就問妳壹句:就算我把所有代碼都給妳,我第壹次看《hello world》的時候妳是不是不知所措?
高贊答道。
高贊的回答也很簡單,就兩句話。
讓我給妳翻譯壹下。這家夥說:
當我們調用Random的構造函數時,我們會得到壹個“種子”參數。比如本例中的:-229985452或者-147909649。
那麽Random將從指定的種子值開始生成隨機數。
並且用相同的種子構造的每個隨機對象將根據相同的模式生成數字。
我沒有看得很清楚,是嗎?
沒關系,我給妳壹段代碼,妳就能恍然大悟上面那段說的是什麽:
這段代碼在我的機器上運行,結果如下:
妳拿著它跑,妳跑的結果壹定是這樣的。
這是為什麽呢?
答案寫在Javadoc上:
在上面的代碼中,兩個-229985452是同壹個種子,三個對nextInt()的調用是同壹個調用序列。
因此,它們生成並返回看似隨機的相同數字。
我們在程序中的正常用法應該是這樣的:
當new Random()時,不指定值。
我們都知道Random是壹種偽隨機算法,在構造的時候指定種子參數是壹種更偽隨機的算法。
因為如果我能猜出妳的種子,或者妳的種子泄露了,那麽理論上我就能猜出妳的隨機數生成序列。
我已經在前面的代碼中演示了這壹點。
前面稍微解釋了壹下“種子”的關鍵點,再回到品嘗的問題上,大概就能看出壹些端倪了。
主要看這個循環裏的代碼。
首先,nextInt(27)定義了當前返回的數字k必須是[0,27]之間的數字。
如果返回0,循環結束;否則,循環結束。然後做壹個類型轉換。
然後是char類型的強制轉換。
當您看到數字變為char類型時,您應該以壹種有條件的方式來考慮ascii代碼:
從ascii代碼表中,我們可以看到“96”是這裏的符號:
因此,以下代碼的範圍是[96+1,96+26]:
也就是[97,122],也就是ascii碼對應的a-z。
所以,我帶妳去反匯編上面的演示代碼。
首先是NewRandom的前五次返回(-229985452)。Nextint (27)如下:
NewRandom的前五次返回(-147909649)。Nextint (27)如下:
所以,當妳查看ascii碼表時,妳可以看到它對應的字母:
現在,至於這個高深莫測的代碼為什麽輸出“hello world”,我心裏是不是很清楚,它就像壹面鏡子?
看穿了,這只是壹個騙局。
然後這個問題下面有個評論,給我看了另壹種打開方式:
您可以指定打印hello world,因此理論上我也可以指定鍵入其他單詞。
比如這位兄弟打了壹個短語:敏捷的褐皮狐貍跳過壹只懶狗。
如果直譯的話,意思是“敏捷的棕狐貍和懶惰的狗雜交”,似乎是壹派胡言。
但是,妳要知道,我的英語水平比較高,在這裏壹眼看到這句話肯定不容易。
所以我查了壹下:
果然有點故事,屬於戲法中的戲法。
看了這位兄弟的快速褐狐例子,我有了新的想法。
既然它能打出所有的字母,我也能打出我想要的特定短語嗎?
我很好,謝謝妳,還有妳。
在這個問題的回答中,“好心人”已經寫出了查找指定單詞對應的seed的功能的代碼。
我直接貼,妳也可以直接拿著用:
所以我在找之前提到的那句話,很簡單:
而且我在跑步的時候,明顯感覺自己花了很多時間在搜索“感謝”這個詞。
為什麽?
我給妳講個故事吧。只有壹句話,妳壹定聽過:
我們這裏的generateSeed方法相當於這只猴子。感謝這個詞就是莎士比亞。
在generateSeed法中,通過26個字母的連續排列組合,總是可以排列出“謝謝”,但只能排列很短的時間。
單詞越長,用的時間越長。
比如我會有壹句恭喜妳,這麽長的壹句話,從00: 05到現在23個小時沒跑出來:
但理論上,只要有足夠的時間,這顆種子是會被找到的。
至此,妳應該完全明白為什麽上面提到的代碼會在隨機字符串的hello world中打印出來了。
妳以為我想帶妳去看源代碼?
不,我主要帶妳吃瓜。
首先,看看隨機無參數構造函數:
好家夥,原來是壹個“無參數”的shell,其實是我自己做了壹個種子,然後調用了參數構造方法。
只是在構建的時候加入了變量“System.nanoTime()”,讓種子看起來有點隨機。
等等,前面不是還有壹個“seedUniquifier”方法嗎?
這個方法是這樣的:
好家夥,第壹次看的時候,頭都大了。這裏面有兩個“神奇的數字”:
這個東西妳也不懂?
永遠不要下定決心,斯達克弗洛
壹搜索就會找到這個地方:
在這個問題中,他說他也對這兩個數字感到不解,他在網上搜了壹圈,相關的資料很少。但是我找到壹篇論文,裏面提到了壹個非常接近的“幻數”:
文中提到的數字如下:
看到了嗎?
這個Java源代碼中的數字少了壹個“1”。發生了什麽事?抄的時候不會出錯吧?
以下是高度贊揚的回答:
“這看起來確實像是壹個錯誤。”
很有意思。如果妳要說這是我哥寫Java源代碼的時候抄的代碼,我會激動的。
馬上去Java Bug的頁面用那串數字搜索,真的是出乎意料:
在對這個bug的描述中,他把我的註意力吸引到了源代碼的這個地方:
原來這個地方的標註代表的是壹篇論文,所以這個數字的來源壹定藏在這篇論文裏。
等等,為什麽我覺得這篇論文的名字有點耳熟?
前壹個stackoverflow中提到的鏈接是壹個紙地址:
看看這篇論文的名字是不是和Java裏的註釋壹樣:
那肯定是壹樣的東西,只是壹個小寫和壹個大寫。
所以,這裏是真錘。真的是第壹次開始用Java寫哥哥的副本數,輸了壹個“1”。
而且,我甚至可以想象,那位兄弟在寫這部分源代碼的時候,把數字“1178349727652981”貼上去,發現:咦,前面怎麽有兩個1?它被復制和刪除了。
至於刪除這個“1”,會帶來什麽問題?
反正這裏有個關聯問題,說並發調用new Random()的隨機性不夠大。
我沒有去研究這個。有興趣可以去看看。我只負責帶妳吃瓜。
於是,基於這個“瓜”,官方對這段代碼進行了壹次修改:
我這裏正好有JDK 15和JDK 8版本的代碼。我壹看,真的是“1”的差別:
而關於隨機數,現在很少用Random。
直接上ThreadLocalRandom,香不香?
什麽,妳拒絕了?