Uniq发展论文
最近在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,香不香?
什么,你拒绝了?