当前位置: 代码迷 >> J2SE >> java编译有关问题
  详细解决方案

java编译有关问题

热度:60   发布时间:2016-04-24 13:45:21.0
java编译问题
int   a=12;
a+=a-=a*=a;
在JAVA中a的结果是-120,而在C中,结果是0。
困惑中,谢谢。


------解决方案--------------------
java的代码不是你表面看到的这样.
a+=a-=a*=a;并不会拆分成
a*=a;
a-=a;
a+=a;
这三步.如果拆成这三步,结果当然是0.

可是,java的虚拟机执行的是编译器编译后的操作码
int a=12;
a+=a-=a*=a;这东西编译后变成了
0: bipush 12
2: istore_1
3: iload_1
4: iload_1
5: iload_1
6: iload_1
7: imul
8: dup
9: istore_1
10: isub
11: dup
12: istore_1
13: iadd
14: istore_1
这个样子.看看那4个连续的iload_1
说明了a作计算的时候同时把a=12的这个值读入了栈4次,尽管计算后改变的a的值,但却不能改变已经读入栈的值.

看看下面的示意图:
位置1是存a的位置.
操作码 局部变量区 栈
0: bipush 12 空 12
2: istore_1 位置1: 12 空
3: iload_1 位置1: 12 12
4: iload_1 位置1: 12 12 | 12
5: iload_1 位置1: 12 12 | 12 | 12
6: iload_1 位置1: 12 12 | 12 | 12 | 12
7: imul 位置1: 12 12 | 12 | 144
8: dup 位置1: 12 12 | 12 | 144 | 144
9: istore_1 位置1: 144 12 | 12 | 144
10: isub 位置1: 144 12 | -132
11: dup 位置1: 144 12 | -132 | -132
12: istore_1 位置1: -132 12 | -132
13: iadd 位置1: -132 -120
14: istore_1 位置1: -120 空
最后位置1的值变成了-120.
------解决方案--------------------
很佩服二楼对虚拟机class规范的熟悉。确实从编译后的VM指令可以看出为什么计算结果是-120。但对于为什么编译后的VM指令是那样的,以及为什么C++编译后的机器指令又是另一样的结果,还有补充的余地。

首先让我们更确切些地描述这个问题为: Java中总是得到-120,无论是SUN还是IBM还是其他什么的编译器。而C/C++中得到的值可能随不同公司不同版本的编译器而不同。如果我没有算错的话,C/C++中可能得到5个值:-120、12、-264、144、0,都是合理。

除了手算,我也具体试验了一下,结果如下:
SUN JDK1.6 ...........................-120
Borland Turbo C++ 3.0 ................0


那么现在有两个问题:
1. 怎么会出现这种不同值的,我说5种可能值的计算依据是什么。
2. 出现这些不同值的意义和影响是什么。

问题1
简而言之,出现不同值是由于不同的编译器编译出的执行代码计算顺序不同所致。
但编译器也不是能恣意妄为的,需要符合一定规范。Java编译器的规范是JSR901--Java Language Specification。C/C++的规范分别是ISO/IEC9899(最近的版本是99年的,常称C99)和ISO/IEC14882(最近的是2003勘误版,2007好像有个草案)。

在以上规范中,对运算符的优先级都做了规定,但其实这是数学逻辑上的优先级,其实称结合律更为贴切。另外还有一个取值顺序的问题,在Java中做了明确规定,而在C/C++中却没作明确规定。所谓取值顺序就是比如:a*b+c/d*e, 我们都知道先乘除后加减,这是运算符优先级,但对于a*b和c/d*e到底先算哪个呢?这就是取值顺序问题。可是,在学C时隐约记得在运算符优先级里也有个“同级从左向右”的规定(有些级别的规定从右向左)啊。其实这个规定的是c/d*e应该按(c/d)*e算而不是c/(d*e)。至于和a*b的先后关系,无论数学上还是C/C++规范里都没有确定。但对于数学计算或者纯加减乘除这些计算,先算哪个似乎没有影响;然而如果运算中有赋值或者++等运算符甚至表达式中混有一些引用传递的函数,称为有副作用,比如楼主这个问题,就会因不同计算顺序而得到不同值了。

我觉得ISO/IEC9899在6.5节(99版)中对于C中这种顺序的未定义说的最简洁: "The grouping of operators and operands is indicated by the syntax ...the order of evaluation
of subexpressions and the order in which side effects take place are both unspecified. " ISO/IEC14882也在第5章(2003版)中做了类似的“未定义说明”。相反Java规范在15.7(第三版)节对计算顺序做了明确规定,简而言之消除了副作用在不同的计算顺序下产生不同的值。

从编译机制来看这个问题,运算符优先级决定了表达式的计算树,决定了哪些运算符和操作数结合组成一个个树形枝杈,逐级递归。而取值顺序决定的则是一个遍历该计算树的顺序,可以证明没有副作用下不同遍历顺序计算结果是一样的.Java规定计算树总是前序遍历的,这样保证了即使有副作用计算结果也一样。而C/C++没有做这个规定,我们就可以用前序,后序,中序,按层,甚至一会儿前序一会变按层,总之各种遍历路径都是不被C/C++规范视错的,这就是我计算出5中可能值的依据。

问题2
这种不规定计算树访问次序次序是有利有弊的:
利: 便于编译器在不同情况下采用不同访问次序来优化代码。
弊: 源代码编译器相关,有损移植性。
所以在学C++钱能那本书里有对副作用专门论述的一节,他建议解决方案是把表达式拆开来写。但这样其实是减少了编译优化的机会。而Java的产生在C之后,硬件的发展使得性能和移植性的权衡天平发生了倾斜,所以Java宁可牺牲一些在大多数情况下微不足道的性能来换取移植性。其实Java建立在硬件廉价和快速发展的设定下,牺牲性能换取其他好处的策略有很多,比如gc机制,中间代码机制等等,所以刚开始Java程序确实感觉慢,但慢慢机器好了最近也确实不太觉得了。

我本来也不清楚这个问题,参考资料中http://blog.csdn.net/anyue417/archive/2006/05/16/740985.aspx是我一开始搜到并阅读的文章,讲的非常清楚,基本单看那个文章这个问题就可以解决了。当然读读语言规范,编译原理,计算理论也是很有好处的。
  相关解决方案