记得大学时看过一本书,上边写到一个撩妹的小技巧:提出和对方玩一个意识游戏,让她心中想一个1到10、对她而言比较有意义的一个数字,然后自己在另一个地方写下一个数字,往往能猜中。这个小技巧利用了人的心理,同时,「程序」其本身本质上就是数字,本文将收集一些有趣的数字,主要以Java语言中的数字为主。给枯燥的生活一些调剂。
1. 神奇的数字 7
和 142857
上边的那个问题的答案是7
。我们平时生活中总是会在各种各样的地方遇到7
这个数字,当你让别人去想一个1到10的数字时,它就欢呼雀跃地跳了出来。最广为人知的就是「一周有7天」这个事情了,同时,7
还可以延伸出来另外一个很有趣的数字142857
,看下边的算式:
1 / 7 = 0.142857, 142857, 142857...
2 / 7 = 0.285714, 285714...
3 / 7 = 0.428571...
4 / 7 = 0.571428...
5 / 7 = 0.714285...
6 / 7 = 0.857142...
当我看到这个结果时已经很震惊了,但是还有更多的:
着了魔的数字
142857 x 1 = 142857
142857 x 2 = 258714
142857 x 3 = 428571
142857 x 4 = 571428
142857 x 5 = 714285
148257 x 6 = 857142
2. 美剧「迷失」中的 4 8 15 16 23 42
这个太玄,就不多说了。
3. 藏在Java随机数中的hello world
这算是一个广为人知的现象,在Java的Random
类中实现的随机数生成算法并不是真正的随机数,只是概率上大致的随机,而其随机性取决于种子的选取,如果选取了特定的种子,那么产生的随机数也就确定了。
根据以上的事实,把-229985452
当做种子传入Random
类,就可以得到hello
,相应的-147909649
则能得到world
。在Java中执行如下程序,它最终会打印输出hello world
。
public static String randomString(int i) {
Random ran = new Random(i);
StringBuilder sb = new StringBuilder();
while (true) {
int k = ran.nextInt(27);
System.out.println("char:" + k + ",number:" + k);
if (k == 0)
break;
k += 96;
sb.append((char) k);
}
return sb.toString();
}
System.out.println(randomString(-229985452) + " " + randomString(-147909649));
根据这个原理,你可以找到很多字符串的种子。原来除了简单的编码,字符串还可以使用随机数的形式隐藏起来。
4. JDK的版本号
我们知道的JDK版本号往往是1.6
,1.8
,或者Java8
这种,但是在编译后的class文件中并不是这么记录的,偶尔在运行程序时会碰到这个错误
java.lang.UnsupportedClassVersionError: Bad version number in .class file [at java.lang.ClassLoader.defineClass1(Native Method)]
在class文件中版本号有[主版本号(major version number)].[次版本号(minor version number)]
,其中「主版本号」和「JDK版本号」的对应关系如下所示:
- Java SE 9 = 53 (0x35 hex),
- Java SE 8 = 52 (0x34 hex),
- Java SE 7 = 51 (0x33 hex),
- Java SE 6.0 = 50 (0x32 hex),
- Java SE 5.0 = 49 (0x31 hex),
- JDK 1.4 = 48 (0x30 hex),
- JDK 1.3 = 47 (0x2F hex),
- JDK 1.2 = 46 (0x2E hex),
- JDK 1.1 = 45 (0x2D hex).
5. class文件中的 0xcafebabe
,或者说是3405691582
如果你用任何一个文本文档编辑器打开一个Java的class文件或者Mac平台上的可执行文件,它的第一行一般如下所示:
//Java编译后的class文件
cafe babe 0000 0034 0117 0700 0201 0012
//iOS平台Objective-C编译后的可执行文件
cafe babe 0000 0005 0000 000c 0000 0009
其中第5、6个字节代表这个class文件的minor version number
,这里全为0,第7、8个字节代表major version number
的值,52,代表我的版本是1.8。而前4个字节从最开始一直就是「CAFE BABE」,这个词本来是 James Gosling对他经常去的一个咖啡馆,里的一个咖啡师的昵称,后来阴差阳错的一致被沿用至今。
6. Mach-o文件中也有0xcafebabe
Mach-o是一种在某些平台上(现在主要是Mac OS X和iOS)的可执行文件类型,在上一节中的代码中看到Objective-C编译后的文件中也出现了0xcafebabe
这个词,这个词的历史其实可以追回到NeXT时期。
在Unix/Linux中使用/etc/magic
中定义的值来判定读取到的文件类型(现在的Mac OS X中可以在/usr/share/file/magic
目录下找到),就是说在程序读取文件时,会根据读到的前几个字节进行文件类型的判断(而不是扩展名),在Unix/Linux的系统中有个命令file
可以判断一个二进制文件的类型。
NeXTSTEP是Mac OS X的前身,是一个基于BSD Unix的系统,自然也遵循了这个方法,当年NeXT获取了Objective-C的使用权,同时开发了一整套的开发套件,那么就需要找一个值来指代其文件类型,于是就使用了0xcafebabe
,通常认为是一位叫Mike DeMoney的哥们做的这个决定。
7. 怎么Mach-o和Java还有这层关系
差不多与此同时,Sun公司也在半秘密地开发著名的Oak项目,这个项目也选择了0xcafebabe
来作为魔术数。有一个有趣的故事是——这个有趣的故事好像是来源于Java项目的第一位工程师Patrick Naughton——cafe babe
代表的是一个Sun公司的漂亮的市场经理(就是开篇的那张照片,Kim Polese),她当时在一个C++编译器的项目中工作,那个项目叫做cafe(C++, A Front End),于是她就被叫做cafe baby——在都是程序员的环境里大约就是会这样——后来她调到了Oak项目。话说后来Kim在硅谷也是风生水起,现在是某公司CEO。下面来感受一下当年的Kim。
当然,一个好八卦必须要有很多版本。另一个说法是,NeXT的很多人后来不想跟乔帮主混了,去了FirstPerson公司,这些人在那里一手创立了Java——呃,当然Java创始人公认是James Gosling——但这些人确实对Java产生了很大的影响。不得不承认,Java的设计理念和Objective-C有太多的相似之处(Interface
属于完全的抄……呃……借鉴),而且很多地方Java和Objective-C的设计理念相同,只是由于抛掉了c语言的枷锁,得以走得更远。总之,他们来了,然后这群人里有一个人(Mike DeMoney)使用了和Mach-o相同的魔术数。
然而,八卦止于无聊的人(逃……),公认的Java创始人James Gosling出来说:很抱歉打扰大家谈论八卦的雅兴,不过小可我才是那个选择了在class文件中使用0xcafebabe
的人,这跟上边那几位一点关系都没有。
As far as I know, I'm the guilty party on this one. I was totally unaware of the NeXT connection. The small number of interesting HEX words is probably the source of the match. As for the derivation of the use of CAFEBABE in Java, it's somewhat circuitous:
当然,由这个magic number背后也可以看出Objective-C和Java一脉相承的关系。
8. 更多的魔术数
话说NeXT的这群工程师都好调皮。
0xbaaaaaad iOS错误日志代码,表示一个日志是全局日志快照,very bad
0xbad22222 iOS错误日志代码
0x8badf00d (Ate Bad Food) iOS错误日志代码,表示代码吃坏了肚子,被杀掉了
0xdeadfa11 (Dead Fall) iOS错误日志代码,用户强制退出
0xDEAD10CC (Dead Lock) iOS错误日志代码,这个很清晰,哎呀,死锁了
0xBAADF00D (Bad Food) Windows中的错误代码
0xCAFED00D (Cafe dude) Java中使用
0xCAFEBABE (Cafe babe) Java和Mach-o文件类型
0x0D15EA5E (Disease)
0x1BADB002 (1 bad boot) 启动失败
0xDEADDEAD Windows的蓝屏???
9. Integer类型中的-128和127
很多书、文章或者各种编程指南中都建议在Java中不要使用基本数据类型(int
,long
,char
,byte
等),Java也提供了自动装箱来尽可能的保证在JVM中奔跑的数据尽可能都是「对象」。然而,有时候这种习惯也会带来一些副作用。
for (int i = 0; i < 10; i++) {
System.out.println((Integer) i);
}
//正常情况下,此代码会循环输出0到9等十个数字
但是,我们可以通过一些小手段,使这个代码输出其他的数字。利用到Java的两个特性,反射和Integer
类的缓存。
大家都知道,当你在Java中获取-128到127的Integer
类型时,其实是不会新建对象的,很多面试题都喜欢考这个特性,其实没什么高深的,只是想给人挖陷阱而已。Integer
类有一个内部类叫IntegerCache
,在这个类中创建了256个数字的缓存,在JDK的源码中是这样写的:
public static Integer valueOf(int i) {
//这里的low和high就是-128和127,也就是一个字节的长度
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
针对上边这块代码,我只要把这个静态内部类IntegerCache
中的cache
数组的内容改掉,别人再调用valueOf()
就拿不到正确的数据了,代码如下:
Class<?> clazz = Class.forName("java.lang.Integer$IntegerCache");
Field field = clazz.getDeclaredField("cache");
field.setAccessible(true);
Integer[] cache = (Integer[]) field.get(clazz);
// 写入错误的值
for (int i = 0; i < cache.length; i++) {
cache[i] = new Integer(128 - i);
}
10. ArrayList
的默认大小
使用Java集合时,有时候因为不希望碰到null
的情况,所以一些List
会像这样赋值
List<String> list = new ArrayList<>();
这条语句的意思是建立一个默认为空的ArrayList
,但是Java的ArrayList
的空间是自增的,且ArrayList
底层是用数组实现的,所以就会有一个默认大小,作为第一次需要自增时初始化的大小,这个数字在ArrayList
而言是10
。所以如果你使用上边的语句建立List
,那么在第一次插入元素时,它就会生成一个空间为10
的数组。如果你这个List
最多只会插入4个元素,那么就浪费了空间。所以如果要建立的这个List
大小大约已知的话,在性能需求较紧张时,最好把你预测的空间填进去。
当然,不要过早去优化,但一个好习惯总是好的。