在“如何编程实现2+2=5”一文中讨论了一个可行的方案,窃以为众人的焦点都被转移了。原文见http://www.oschina.net/news/52412/write-a-program-that-makes-2-2-5
1. 文中的纰漏问题
Integer a = new Integer(2); Integer b = new Integer(2); System.out.print(a == b);
这个在java中输出必然是false,因为两个地址是不一样的,两边类型对等都是对象时,==比较的是地址,而不是值。如果有一侧是原生类型,则尝试把另一边转换成为原生类型来进行比较。把基本类型封装拆成原始类型值的隐式转换,这就从JDK1.5开始引入的自动拆包(拆箱)。至于有评论说是true,应该是没有亲自去试试,或者不熟悉源码。new是产生一个新的对象,不是工厂模式,去取缓存中一个已经存在的。
2. 巧妙的障眼法
不觉得文中使用的打印方法有点不正常么?一般人打印的时候我想都是这样的:
System.out.println("2+2="+(2+2));
而文中给出的打印方式为:
System.out.printf("%d",2 + 2);
这是一个典型的障眼法,也是配合他的思想的。
printf这个方法其实在C中是常用的,在Java中的声明是这样的:
public PrintStream printf(String format, Object ... args)
这里给我们看到,参数是一个变长的数组类型,元素的类型是Object
2+2得到的是一个原生的数字类型4,并且这个结果不会变的。但原生类型4怎样转换成为Object呢?由于4是原始类型,对应的包装类型是Integer,因此首先自动装箱成为Integer,而这个过程是调用Integer.valueOf()方法来完成。谁来调用的呢?编译器。因此Java语法中许多的操作都是编译器来做繁琐的处理,而不是语法真的能实现这个功能。比如字符串和数字的相加,并非+有这样的能力,而是编译器来完成语法的解析和执行。+就只做数字的运算。
所以调用printf的过程中,原生的数字4被调用Integer.valueOf(4)来转换成为Object,这正中下怀,从Integer.valueOf的实现来看,通过修改缓存的数字序列,的确达到了移花接木的效果。如果不是使用printf,而使用print或者println,你都得不到这个结果。
3. 反射操作的技巧
在这个方法中,真正的技巧和难度其实不在于修改cache序列中的引用,当然这是一个绝佳的点子,并且很好的应用了自动装箱这个方式完成了从里面的取值,但真正应该学习的技巧却是关于反射的使用,尤其是这几句:
Class cache = Integer.class.getDeclaredClasses()[0]; Field c = cache.getDeclaredField("cache"); c.setAccessible(true); Integer[] array = (Integer[]) c.get(cache);
我估计学过Java的人可能大多只是知道这几句是什么意思,但也只是看着代码去解释而已,如果让你自己写,八成是写不出来的。
第一句是获取Java类中所定义的内部类,并且取了其中的第一个。这个只有读过源码才知道,Integer里面也只定义了一个静态内部类,就是用来做存储的,存储的成员变量名字叫cache,也是静态的。因此这里写死了取第一个([0])。
第二句是从这个类中取定义的成员变量(field)。由于这个成员变量是外部不可访问的(包级访问权限),因此第三句c.setAccessible(true)用来通知JVM当这个方法或成员变量被调用的时候,不要进行可访问性的检查(参见AccessableObject.java的setAccessible的JavaDoc)。然后就可以顺利的从cache这个类中取出其成员成员变量cache,其类型是Integer[],然后后面的处理就没有悬念了。
总结:
文章给出的2+2=5其实并不是真的计算结果是5,而是把计算结果打印成为5,所采用的方案是利用自动装箱时把4装成5。由5在缓存的区间里面,因此修改装箱会引用的那个值,加上自动装箱功能,就可以打印成为5了。由于表演要像一点,因此打印的时候需要选择装箱后打印,这里没有自己写一个Object类型的参数打印方法(Integer类型的,Number类型的都行,只要先装箱就好办了),而是选择了Java中不常用的printf方法。但是这个方法带来的副作用是,4的缓存被指向5了,因此只要是结果为4的自动装箱都将被包装成为5,因此不只是2+2=5了,1+3,6-2都是5了。
不管怎么说,这个还是很有创意的一个想法。而且,由于修改的是Integer类,因此不一定把修改缓存的这部分都写在main方法中,写在静态代码中可能更具有隐蔽性,并且如果静态代码块在引用的别的类中,那么这个隐蔽性就更强了。