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);
}
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
很久以前,当 CPU 只有少数寄存器时,人们发现可以通过利用异或操作符(^
)的属性 x ^ y ^ x == y
来避免使用临时变量:
java
x = x ^ y;
y = y ^ x;
x = y ^ x;
1
2
3
2
3
即使回到那个年代,这项技术也很少被证明是合理的。而且还增加了复杂度。在 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在 C 或 C++ 中,并没有指定表达式的计算顺序。当表达式 x^=expr
时,许多 C 和 C++ 编译器都是在计算 expr 之后才提取 x 的值,这使得上述惯用法可以正常运转。
为了突出其价值,还是可以写出不用临时变量就可以互换两个变量内容的 Java 表达式的。
java
y = (x ^= (y ^= x)) ^ y;
1
这个教训很简单:在单个表达式中不要对相同的变量赋值两次。
更一般的讲,要避免所谓聪明的编程技巧。它们都容易产生 bug 难以维护。