Skip to content

Java 解惑 -07:互换内容

🏷️ 《Java 解惑》

下面的程序使用了复合的异或赋值操作符,它所展示的技术是一种编程习俗。

java
public class JavaPuzzlers007 {
    public static void main(String[] args) {
        int x = 2016;
        int y = 2021;
        x ^= y ^= x ^= y;
        System.out.println("x=" + x + "; y=" + y);
    }
}

很久以前,当 CPU 只有少数寄存器时,人们发现可以通过利用异或操作符(^)的属性 x ^ y ^ x == y 来避免使用临时变量:

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

即使回到那个年代,这项技术也很少被证明是合理的。而且还增加了复杂度。在 C 语言中曾经使用过这个惯用法,更进一步,在 C++ 中也使用了它,但是它并不保证在两者中都可以正确运行。

有一点可以确定,在 Java 中肯定是不能正确运行的。最上面的代码打印的结果是:x=0; y=2016

Java 语言规范描述了:操作符的操作数是从左向右求值的。 为了求表达式 x^=expr 的值,在计算 expr 之前提取 x 的值,并且将这两个值的异或结果赋给变量 x

下面的代码详细的描述了将互换惯用法分解之后的行为:

java
int x = 2016;
int y = 2021;
// x ^= y ^= x ^= y;
int tmp1 = x; // 在表达式执行前提取 x 的值 2016
int tmp2 = y; // 在表达式执行前提取 y 的值 2021
int tmp3 = tmp1 ^ y; // 使用提取的变量 tmp1 计算最左边的 x ^= y
x = tmp3; // 将计算结果赋给 x
System.out.println("x=" + x); // 5
// 使用提取的变量 tmp2 计算 y ^= x,此时的 x 使用的是实际的值,而不是提取的值,因为这里 x 在表达式的右边
int tmp4 = tmp2 ^ x; 
y = tmp4; // 将计算结果赋给 y
System.out.println("y=" + y); // 2016
int tmp5 = tmp1 ^ y; // 使用提取的变量 tmp1 计算最右边的 x ^= y
x = tmp5; // 将计算的结果赋给 
System.out.println("x=" + x); // 0

在 C 或 C++ 中,并没有指定表达式的计算顺序。当表达式 x^=expr 时,许多 C 和 C++ 编译器都是在计算 expr 之后才提取 x 的值,这使得上述惯用法可以正常运转。

为了突出其价值,还是可以写出不用临时变量就可以互换两个变量内容的 Java 表达式的。

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

这个教训很简单:在单个表达式中不要对相同的变量赋值两次。

更一般的讲,要避免所谓聪明的编程技巧。它们都容易产生 bug 难以维护。