为什么 "a^=b^=a^=b;" 和 "a^=b; b^=a; a^=b;" 不同?

20

我试过一些用Java交换两个整数而不使用第三个变量的代码,使用异或。

这是我尝试过的两个交换函数:

package lang.numeric;

public class SwapVarsDemo {

    public static void main(String[] args) {
        int a = 2984;
        int b = 87593;
        swapDemo1(a,b);
        swapDemo2(a,b);
    }

    private static void swapDemo1(int a, int b) {
        a^=b^=a^=b;
        System.out.println("After swap: "+a+","+b);
    }

    private static void swapDemo2(int a, int b) {
        a^=b;
        b^=a;
        a^=b;
        System.out.println("After swap: "+a+","+b);
    }

}

这段代码产生的输出结果是这样的:

After swap: 0,2984
After swap: 87593,2984

我很好奇,为什么这个陈述是:

        a^=b^=a^=b;

与此不同的?

        a^=b;
        b^=a;
        a^=b;

我认为你正在受到Java的按值传递的影响。 - everton
3
我看不出按值传递与问题有任何关系。您认为按值传递在哪些方面导致了问题?请您具体说明。 - ApproachingDarknessFish
好的,我忽略了这个。 - everton
当你用这种方式“交换”值时,Java实际上是否会创建隐式中间对象?我认为使用临时变量应该可以提高性能。通常情况下,应该尝试使用位运算来提高性能,只有在没有自动GC的语言中才需要这样做。 - Mikko Rantalainen
6个回答

40

问题在于表达式的求值顺序:

参见JLS第15.26.2节

首先,对左操作数进行求值以产生一个变量。 如果此求值突然结束,则赋值表达式基于同样的原因完成突然结束;右操作数不会被求值且不会发生赋值。

否则,保存左操作数的值,然后对右操作数进行求值。 如果此求值突然结束,则赋值表达式基于同样的原因完成突然结束且不会发生赋值。

否则,使用左操作数的保存值和右操作数的值来执行由复合赋值运算符指示的二元运算。 如果此操作突然结束,则赋值表达式基于同样的原因完成突然结束且不会发生赋值。

否则,将二进制操作的结果转换为左操作数的类型, 经过值集转换(§5.1.13)为适当的标准值集(不是扩展指数值集), 并将转换的结果存储到变量中。

所以你的表达式做了以下操作:

a^=b^=a^=b;

  1. 求值 a
  2. 求值 b^=a^=b
  3. 将两个值异或(因此步骤一中的a尚未应用^= b
  4. 将结果存储在 a

换句话说,你的表达式等同于以下java代码:

    int a1 = a;
    int b2 = b;
    int a3 = a;
    a = a3 ^ b;
    b = b2 ^ a;
    a = a1 ^ b;
您可以从方法的反汇编版本中看到这一点:
  private static void swapDemo1(int, int);
    Code:
       0: iload_0       
       1: iload_1       
       2: iload_0       
       3: iload_1       
       4: ixor          
       5: dup           
       6: istore_0      
       7: ixor          
       8: dup           
       9: istore_1      
      10: ixor          
      11: istore_0  

3
+1 - 我倾向于批评使用反编译的字节码作为解释Java行为的答案。但是你以正确的方式做到了...你找到了JLS中的真正解释,并使用字节码来确认该解释。 - Stephen C
不错的答案,但我很好奇Java是否允许您在这种情况下使用括号来强制执行所需的行为。 a ^ =(b ^ =(a ^ = b));有效吗? - Patrick M
2
@Patrick_M 这并没有什么区别(你描述的已经是运算符优先级的工作方式),因为左侧仍然会在右侧之前被计算。 - Erwin Bolwidt

7
因为 a ^= b ^= a ^= b; 被解析成:
a ^= (b ^= (a ^= b));

这可以简化为:

a ^= (b ^= (a ^ b));

所以b的值将为b ^ (a ^ b),最后a将为a ^ (b ^ (a ^ b)

解释一下你所做的缩减是正确的原因 -- 引用语言规范和运算符优先级信息。提问者可能会遇到其他涉及运算符优先级的问题(在这种语言和其他语言中)。 - ErstwhileIII
缩减表明,在最内层的括号中,a ^= b 等于 a ^ b。这是正确的,因为变量 a 赋的值在其余的计算中没有被使用。相反,该值稍后会被外层括号计算的结果覆盖。对此不需要语言规范 - SebastianH

5
这与Bloch和Gafter的《Java Puzzlers》书中的一篇文章非常相似,见第2章,谜题7(“Swap Meat”)。我无法改进它。
解决方案中的解释是:
这种习惯用法在C编程语言中使用,在那里进入了C++,但不能保证在这些语言中都有效。在Java中保证不起作用。Java语言规范说,运算符的操作数从左到右进行评估[JLS 15.7]。要评估表达式x ^= expr,必须在评估expr之前对x的值进行取样,并将这两个值的异或结果赋给变量x[JLS 15.26.2]。在CleverSwap程序中,变量x被取样两次-每次出现在表达式中-但是两个取样都发生在任何赋值之前。下面的代码片段更详细地描述了破损的交换习惯用法的行为,并解释了我们观察到的输出:
引用上面引用的代码是:
// The actual behavior of x ^= y ^= x ^= y in Java
int tmp1 = x;     // First appearance of x in the expression
int tmp2 = y;     // First appearance of y
int tmp3 = x ^ y; // Compute x ^ y
x = tmp3;         // Last assignment: Store x ^ y in x
y = tmp2 ^ tmp3;  // 2nd assignment: Store original x value in y
x = tmp1 ^ y;     // First assignment: Store 0 in x

2

你应该指向适当的Java语言文档来解决这些基本问题。此外,你应该解释问题而不仅仅指向外部文档。 - SebastianH

1

我没有足够的声望点来评论Nathan的答案。

@Nathan Hughes的回答谈到了Java Puzzlers书籍,他的答案指出了一些见解,可以让您在1行中完成此操作。虽然不如OP的问题优雅。

正如Nathan所指出的:

在CleverSwap程序中,变量x被采样两次——每个表达式都采样一次,但是所有采样都在任何赋值之前发生。

除了使用https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html作为指南外,您还可以使用以下1行代码完成此操作:

a = ( b = (a = a ^ b) ^ b) ^ a;

关键是将变量值分配为第一个异或的一部分,以确保将其保留在第二个异或的左侧(有关示例,请参见上面的jls链接中左手操作数中的良好示例)。同样,使用第二个异或的结果设置b变量,同样保持在最终异或的左侧。

0

第二种变体等于

a=a^(b^(a^b));


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接