本文分类:
Java

概念

优先级

优先级,英文precedence,是指在一个包含多个运算符的表达式中,当没有括号明确指定计算顺序时,各个运算符计算的先后顺序,优先级是这三个概念中最容易理解的,比如有表达式a + b * c,由于 * 的优先级高于 + ,所以会先运算 b * c,再将结果和 a 相加,相当于 a + (b * c)

结合性

结合性,英文associative,指优先级相同的运算符连续运算时,如果没有括号明确指定计算顺序,是从左到右分组,还是从右到左分组。比如有表达式a + b + c,结合性解决的就是按 (a + b) + c 还是按 a + (b + c)计算的问题。我们把从左到右分组的操作符成为左结合性操作符,把从右到左分组的操作符成为右结合性操作符。

Java中大部分操作符是左结合性的,只有条件操作符、赋值和复合赋值操作符、!、~、++、–、一元操作符+、一元操作符-、()、(强制类型转换)、new是右结合性。

比如加法操作符 + 是左结合性,那么a + b + c 按 (a + b) + c 的方式计算。条件表达式右结合性,表达式 a ? b : c ? d : e 按 a ? b : (c ? d : e) 的方式计算。

求值顺序

求值顺序,英文evaluation order,指的是操作符涉及的操作数的求值顺序,比如 a=f() + g(),求值顺序决定的是先计算 f()还是先计算 g()。优先级和结合性只能决定表达式如何分组,而不决定操作数的求值顺序。

求值顺序是最容易出问题的地方,先看看下面这个C语音程序,但凡参加过C语言面试的人可能都遇到这样一个题目,要求用一行代码实现两个数交换,我们看一个简化版实现,代码如下:

#include <stdio.h>;

int main()
{
    int x = 1;
    int y = 2;
    printf("x = %d, y = %d\n", x, y);
    x ^= y ^= x ^= y;
    printf("x = %d, y = %d\n", x, y);
}

运行环境

# cat /proc/cpuinfo | grep "model name"
model name    : Intel(R) Xeon(R) CPU E5-2650 v2 @ 2.60GHz
# cat /etc/redhat-release
CentOS Linux release 7.2.1511 (Core)
# uname -i
x86_64
# gcc --version
gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-4)

编译运行结果如下,可见在C语言中该代码实现了两个数的交换。

# gcc test.c -o test
# ./test
x = 1, y = 2
x = 2, y = 1

但是这个技巧在Java中是不成立的,下面的代码

public class TestBidAndSwap {
    public static void main(String[] args) {
        int x = 1;
        int y = 2;
        System.out.println(String.format("x = %d, y = %d", x, y));
        x ^= y ^= x ^= y;
        System.out.println(String.format("x = %d, y = %d", x, y));
    }
}

并不能实现两个数交换,输出如下

x = 1, y = 2
x = 0, y = 1

这就是求值顺序不同造成的区别。根据Java语言规范,操作符的操作数按照从左到右的顺序求值,则代码 x ^= y ^= x ^= y; 相当于如下代码

int tmp1 = x;
int tmp2 = y;
int tmp3 = x;
x = tmp3 ^ y;
y = tmp2 ^ x;
x = tmp1 ^ y;

而不是C语言那样的

x = x ^ y;
y = y ^ x;
x = x ^ y;

那Java有没有办法实现一行代码互换两个数呢?答案是可以,看下面的代码:

x = (y ^= (x ^= y)) ^ x;

或者

y = (x ^= (y ^= x)) ^ y;

下面简单翻译了The Java Language Specification, Java SE 8 Edition的第15.7节,以供进一步参考:

15.7. 求值顺序

Java保证操作符的操作数按照从左到右的顺序求值。

建议代码不要依赖这个规范。

15.7.1. 首先求值左操作数

二元操作符的左操作数将在任何右操作数被求值前被完全计算出来,比如下面的代码输出是9。

class Test1 {
    public static void main(String[] args) {
        int i = 2;
        int j = (i=3) * i;
        System.out.println(j);
    }
}

对于二元操作符,左操作数的求值过程是,在右操作数被求值前,记住左操作数表示的变量,并获取该变量的值用于接下来的计算。下面的程序输出两行12:

class Test2 {
    public static void main(String[] args) {
        int a = 9;
        a += (a = 3);  // first example
        System.out.println(a);
        int b = 9;
        b = b + (b = 3);  // second example
        System.out.println(b);
    }
}

如果二元操作符的左操作数在求值时被中断,则右操作数不会被求值,下面的代码输出抛出java.lang.Exception: I’m outta here!异常后打印Now j = 1。

class Test3 {
    public static void main(String[] args) {
        int j = 1;
        try {
            int i = forgetIt() / (j = 2);
        } catch (Exception e) {
            System.out.println(e);
            System.out.println("Now j = " + j);
        }
    }
    static int forgetIt() throws Exception {
        throw new Exception("I'm outta here!");
    }
}

15.7.2 在执行操作前为操作数求值

Java保证操作符的每个操作数(除了&&、||和? :)在执行操作前被完全地求值。

整除和取余运算可能抛出ArithmeticException异常,但异常只可能在两个操作数都被正常完整地求值后才能被抛出。下面的代码会抛出java.lang.Exception: Shuffle off to Buffalo!异常而不是java.lang.ArithmeticException: / by zero。

class Test {
    public static void main(String[] args) {
        int divisor = 0;
        try {
            int i = 1 / (divisor * loseBig());
        } catch (Exception e) {
            System.out.println(e);
        }
    }
    static int loseBig() throws Exception {
        throw new Exception("Shuffle off to Buffalo!");
    }
}

15.7.3. 求值遵循括号和优先级

Java遵循通过括号明确指定和通过优先级隐式指定的求值顺序。

Java实现不会利用代数恒等式,例如结合律,来将表达式重改写成更方便的计算的形式,除非对于可能涉及的所有可能的计算值,都可以证明改写后的表达式在值和可观察的副作用都与原表达式等价,即使是在多线程环境中。

在浮点运算中,该规则适用于无穷值(无穷大或无穷小)和非值(无意义的值)。

比如,!(x<y) 不能被重写为 x>=y,因为当x和y中有一个NAN时,值是不同的(译注:double x = Double.NaN; double y = 0.2; 则前者的结果是true,而后者是false)。

特别地,数学上看起来满足结合律的浮点数运算,在计算机上不可能是满足结合律的,这种计算不会被重排序。

例如,Java编译器将 4.0 * x * 0.5 重写为 2.0 * x 是不正确的; 虽然舍入在这里不是一个问题,但是如果存在很大的 x 值,第一个表达式产生无穷大(因为溢出),但是第二表达式产生有限结果。比如下面的代码输出 Infinity 和 1.6e308。

strictfp class Test {
    public static void main(String[] args) {
        double d = 8e+307;
        System.out.println(4.0 * d * 0.5);
        System.out.println(2.0 * d);
    }
}

和浮点数不同,整数加法和乘法在Java中已被证明是满足结合律的。

例如表达式a + b + c,其中a,b和c是局部变量(这个简化的假设可以避免涉及多线程和volatile变量的问题),不管是计算(a + b)+ c还是a + (b + c),总是产生相同的结果。如果另有表达式b + c出现在该表达式附近,智能的Java编译器可能会重复使用这个公共子表达式的值。

15.7.4.参数列表从左到右求值

在方法或构造器调用中,括号中可能出现逗号分隔的参数表达式,那么每个参数都会在它右边的表达式被计算之前完全求值。比如下面的代码输出going, going, gone。

class Test1 {
    public static void main(String[] args) {
        String s = "going, ";
        print3(s, s, s = "gone");
    }
    static void print3(String a, String b, String c) {
        System.out.println(a + b + c);
    }
}

如果一个参数的求值过程被中断,则右边的任何参数都不会被求值。例如下面的代码输出java.lang.Exception: oops, id=1。

class Test2 {
    static int id;
    public static void main(String[] args) {
        try {
            test(id = 1, oops(), id = 3);
        } catch (Exception e) {
            System.out.println(e + ", id=" + id);
        }
    }
    static int test(int a, int b, int c) {
        return a + b + c;
    }
    static int oops() throws Exception {
        throw new Exception("oops");
    }
}
本文来自 [时光记 - 王智超的个人空间](www.hiwzc.com),转载请注明出处。