文档章节

关于2+2=5的问题

于忠达
 于忠达
发布于 2014/06/06 10:32
字数 1381
阅读 125
收藏 0

在“如何编程实现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方法中,写在静态代码中可能更具有隐蔽性,并且如果静态代码块在引用的别的类中,那么这个隐蔽性就更强了。


© 著作权归作者所有

共有 人打赏支持
于忠达
粉丝 113
博文 13
码字总数 16484
作品 0
青岛
程序员
私信 提问
SQL*Plus中替换变量与定义变量

替换变量 SQL*Plus中的替换变量又叫替代变量,它一般用来临时存储相关数据;在SQL语句之间传递值。一般使用&或&&前缀来指定替换变量. 关于使用替换变量,一般是利用其创建通用的脚本或达到和...

breakawaylove
2014/10/24
26
0
蓝桥杯题目——瓷砖铺放

问题描述   有一长度为N(1<=N<=10)的地板,给定两种不同瓷砖:一种长度为1,另一种长度为2,数目不限。要将这个长度为N的地板铺满,一共有多少种不同的铺法?  例如,长度为4的地面一共...

孤单的狗
2017/03/10
0
0
python 是什么~

  Python的非正式介绍   在后面的例子中,区分输入和输出的方法是看是否有提示符(“>>> ”和“.. ”):想要重复这些例子的话,你 就要在提示符显示后输入所有的一切;没有以提示符开始的行...

python爱好者
2013/03/11
479
1
C++编程实现对车间产品生产周期的估算和生产调度方案的安排算法

花费二个多月的时间编写了可以估算产品生产周期和安排生产调度方案的程序,生产周期的估算,就是在生产过程中有订单插入的情况下计算在工艺文件所规定的工序下,不同种类的多件产品(同一类别...

Kukucao
06/09
0
0
[源码分析]AbstractStringBuilder

[源码分析]AbstractStringBuilder Java中, AbstractStringBuilder是 StringBuilder 和 StringBuffer 的父类. 所以了解StringBuilder和StringBuffer前, 有必要先了解一下这个抽象父类. value...

GoldArowana
08/05
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Git —— 创建版本库和提交回退版本

二、 创建版本库 版本库又叫做仓库,简单理解就是一个目录,这个目录里面所有的文件都可以被Git管理起来,每个文件的修改、删除,Git都可以跟踪,便于追踪历史与还原。找到一个合适的位置,创...

lwenhao
31分钟前
3
0
guava cache使用介绍

今天在项目中发现大量使用guava cache提供缓存,觉得不错。 jvm堆大小为5G /** * * 占用JVM内存,内部数据结构类似于ConcurrentHashMap。因为JVM堆大小的限制,guava cac...

jack_peng
35分钟前
3
0
崛起于Springboot2.X之投票活动排行榜项目(39)

简介:投票活动,用户只能一天投票一次,然后对参与投票的项目进行实时的排行功能。 架构:redis+mysql+springboot2.0.3+mybatis 不懂可以私信我哦 1、数据库建表 CREATE TABLE `t_dtb_prod...

木九天
44分钟前
2
0
logback源码分析-2、appender解析

源码基于logback 1.1.2 logback.xml文件内容如下 <?xml version="1.0"?><configuration scan="true" scanPeriod="30 seconds"> <property name="fileDir" value="/export/log/ingore......

924411018
50分钟前
2
0
【HAVENT原创】NodeJS 两个模块进行 RSA 加密解密(匹配Java RSA)

业务逻辑需要使用 NodeJS 进行公钥加密传输给 Java 后端进行私钥解密,但是默认 NodeJS 使用的 RSA padding 模式与 Java 的不一致,所以需要配置。 不啰嗦,上代码,分别用 crypto 和 node-r...

HAVENT
57分钟前
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部