优先级

As the Java programmer’s beginning


优先级从上到下递减,不要问为什么,因为这就像在问1加1为什么等于2一样,it’s rules, OK. 规则由它所在的环境决定。在二进制的世界里,1加1等于10。

Description Separators/Operators Associativity
分隔符 ( )、, 、. 、[ ]、{ }、… 、@、:: 、; 左=>右
单目运算符 !、~、+(正)、-(负)、+ +、- -、(Type) 右=>左
算术 *、/、% 左=>右
算术 +、- 左=>右
移位 <<、>>、>>> 左=>右
关系 >、<、>=、<=、instanceof 左=>右
关系 ==、!= 左=>右
按位与 & 左=>右
按位异或 ^ 左=>右
按位或 | 左=>右
条件 && 左=>右
条件 | | 左=>右
三目 ? : 右=>左
赋值 =、~=、*=、/=、+=、-=、&=、^=、|=、%=、<<=、>>=、>>>= 右=>左

说实话,我到现在也记不住这些个东西,但同样能够“征战沙场”、“攻城略池”。但是,要想高效Coding,必须完全摸清这些基本的东西。

总式:单目 > 双目 > 三目 > 多目(赋值),优先级低的先考虑

从表格中可以看到,优先级的区分重点集中在双目运算。最后需要注意一下,条件与/或会短路,移位/位运算的数字是以二进制作为操作数的,还有那三类从右向左的结合性。

//短路
if(23 > 24 && 100/0 == 0){
    System.out.println("true");
}

//移位
int bitmask = 0x000F;
int val = 0x2222;
System.out.println(Integer.toBinaryString(val));
System.out.println(Integer.toBinaryString(bitmask));
System.out.println(Integer.toBinaryString(val & bitmask));
System.out.println(Integer.toBinaryString(val | bitmask));
System.out.println(Integer.toBinaryString(~val));

//位
int n = -10;
System.out.println(Integer.toBinaryString(n));
System.out.println(Integer.toBinaryString(n >> 3));
System.out.println(Integer.toBinaryString(n >>> 3));

遵循“优先级低的先考虑”原则,运算才有意义。这里先考虑的意思不一定等于先计算,这相当于先根据运算符的优先级从低到高找出来并根据结合性和后算的先入栈的方法将操作数按顺序放入栈中,然后再一个一个弹出来运算。下面if包含的代码块中,添加了小括号方便阅读,去掉后结果不变。

//Test one
int num = 0;
StringBuilder sb = new StringBuilder();
if (((sb instanceof StringBuilder == num++ > 0) && (++num > 1)) || (num != 2)) {
    System.out.println("true");
}
System.out.println("num = " + num);

//Test two
int num = 0;
StringBuilder sb = new StringBuilder();
if (((sb instanceof StringBuilder == num++ > 0) || (++num > 1)) && (num != 2)) {
    System.out.println("true");
}
System.out.println("num = " + num);

//Test three
int num = 0;
StringBuilder sb = new StringBuilder();
if (sb instanceof StringBuilder ? false : (false == (((num++ > 0) && (++num > 1)) || (num != 2)))) {
    int a = 4;
    int b = 3;
    int c = a > b ? ++b == b ? a++ : b : a;
    System.out.println("a = " + a);
    System.out.println("b = " + b);
    System.out.println("c = " + c);
}
System.out.println("num = " + num);

先看Test one与Test two,它们只是条件与和或符号对调。if语句中优先级最低的都是条件或,以次为界,将表达式左右分开,并确认先算左边的式子(结合性)。Test one中左边的式子优先级最低的是条件与,同样以次为界,将表达式左右分开,并确认先算左边的式子(结合性),之后的同理即可。Test two同样以此类推。


Test three同样先在表达式中找出优先级最低的运算符,这里为三目。跟上面以结合性确认先算哪边的方式不同,这里多了一步,需要先算出?左边的真假,才能进一步确认接下来要进入冒号:左边的分支还是右边的分支。

基于栈的字节码指令集

float a = 1.234f;
int b = (int) -++a, c = -b + -(int) a++;
b = ++b;
c = c++;
System.out.println(b);
System.out.println(c);

如果能够算出正确答案,那么恭喜你,成功入门了。但是,我相信,大部分人靠的是“熟能生巧”。也就是说,只是记住了运算形式。像这种“虚无缥缈”的东西,转瞬即逝。我们需要找到一种可靠的、看得见的一种概念模型来进行辅助。Fortunately,Java编译器提供了这样的东西。用javap -v <class文件名>命令查看。

到每行代表一条指令,包含操作码(一个字节),可能还有操作数(n个字节)。由于都是以字节为单位进行存储的,这些指令也叫“字节码”。

//对应b = ++b
22: iinc          2, 1
25: iload_2
26: istore_2

//对应c = c++
27: iload_3
28: iinc          3, 1
31: istore_3

上面的字节码结合下图👇可以很容易理解。上下两部分主要是对整数前后自增的分析。我们都知道,自增符号在前的会先加完再参与运算;反之则不参与。因此,b会加1,c保持不变。由字节码可以看出,两者的差异主要在“加载(iload,i表示int)变量入栈”的时机,即在自增(iinc)前还是自增后入栈的问题。入栈即表示参与运算,而在自增前入栈,那么自增的值不会影响之后的运算结果。


栈帧


...
//对应(int) -++a
3: fload_1
4: fconst_1
5: fadd
6: dup
7: fstore_1
8: fneg
9: f2i
...
//对应-b + -(int) a++
11: iload_2
12: ineg
13: fload_1
14: dup
15: fconst_1
16: fadd
17: fstore_1
18: f2i
19: ineg
20: iadd
...

浮点数与整数自增区别:前者通过fconst_1在栈中生成需要增加的浮点型常量,而后者直接在变量槽中自增。


虽然浮点数与整数的自增在实现方式上有区别,但前后自增的效果/差异与整数相同。唯一需要注意的地方是dup指令。前自增:先加载(fload_1)到栈中,栈中生成(fconst_1)需要增加常量,将栈中的这两个数相加(fadd),再用(dup)生成跟这个相加结果一样的数并同样压入栈中。此时栈里有两个相同的数,栈顶的将弹出并存储到对应的变量槽中;剩下的将继续参与运算。后自增同理可知。

其实这些字节码流也真的只是个概念模型,到最后真正运行时并非如此,会经过优化,转成汇编代码来提高运行效率,只是结果都一样而已。

参考资料

  1. Java Language Specification 8
  2. 深入理解Java虚拟机(第三版)
  3. Java运算符
  4. 一篇文章弄懂左移、右移位运算

留言评论
推荐阅读