Java中"a[1] *= a[1] = b[n + 1]"不能正常工作?

4

正如标题所示,我有一个函数使用了一个临时数组,并想从另一个数组中写入一个值到这个数组中,然后将这两个值相乘。

例子:

float[] a = {0, 0}

a[0] *= a[0] = b[n    ];
a[1] *= a[1] = b[n + 1];

我希望以上内容能够实现以下功能:
a[0] = b[n    ];
a[0] *= a[0]; //(aka: a[0] = a[0] * a[0])

a[1] = b[n + 1];
a[1] *= a[1];

尽管行为似乎并不是这样发生的。 相反,它似乎只是将“a”中原始值乘以“b”中所持有的任何值,如下所示:
a[0] = a[0] * b[n    ];
a[1] = a[1] * b[n + 1];

我一直认为 "=" 后面的内容会先进行计算,例如执行以下代码时:

float a, b;
a = b = 5;
//"a" and "b" both equal "5" now.

既然情况是这样的,那么我的原始示例不应该可以工作吗?

有人能解释一下发生了什么,以及为什么这段代码不能按预期工作吗?


1
乍一看这似乎是一个新手问题,但认真想想,你说得有道理。然而我从来不想在生产代码中看到这个。经验法则:将其编写为a[0]=b[n]*b[n],这样更容易阅读。像这样的代码只有在编译器需要适应约30k内存的日子里才有用,通过一些巧妙的处理可能可以节省一些CPU周期,因为当时的编译器并未进行优化。现在没时间了,但在Java中,所有这些都应该在规范中设置(而在C中,我认为它要么是未定义的,要么是实现定义的)。 - Axel
4个回答

7
我认为迄今为止的答案是不正确的。涉及复合表达式(例如a *= b)时,重要的是对其进行评估。简而言之,左侧的值在右侧之前计算出来。根据JLS(我强调了),在运行时,表达式将按以下两种方式之一进行评估:
如果左操作数表达式不是数组访问表达式,则:
首先,评估左操作数以产生一个变量。如果此评估突然完成,则赋值表达式因同样的原因而突然完成;不评估右操作数且未发生赋值。
否则,保存左操作数的值,然后评估右操作数。如果此评估突然完成,则赋值表达式因相同原因而突然完成,且未发生赋值。
否则,使用左操作数变量的保存值和右操作数的值来执行由复合赋值运算符指示的二元操作。如果此操作突然完成,则赋值表达式因相同原因而突然完成,且未发生赋值。
否则,将二进制操作的结果转换为左操作数变量的类型,经过值集转换(§5.1.13)转换为适当的标准值集(不是扩展指数值集),并将转换的结果存储在变量中。
在您的示例中:
a[0] *= a[0] = b[n    ];
  1. 计算并将a[0]的值存储在临时变量tmp
  2. 评估a[0]=b[n],给出b[n]的值(并将a[0]的值更改为b[n])
  3. 计算tmp * a[0]
  4. 将上一步的结果分配给a[0]

因此,实际得到的是a[0] *= b[n]

编辑:关于关于右向左赋值运算的混淆:我没有在JLS中找到这个术语,而且在我看来它是不正确的(尽管它在Java教程中使用)。它被称为右结合赋值运算符。JLS对赋值有以下规定:

共有12个赋值运算符;所有赋值运算符在语法上都是从右向左结合的。因此,a=b=c表示a=(b=c),它将c的值赋给b,然后将b的值赋给a。


如果在 a *= b 中左侧被评估,那么无论此时 a 的值是什么,都会被缓存,然后运行表达式的第二部分。当第二部分解析完成后,它使用先前缓存的 a 版本完成赋值,最终将其存储在实际的 a 变量中。 - Hex Crown
我不理解的是,为什么这被称为“从右到左”的赋值运算。因为在赋值运算符左侧的值先被计算(并保存),不是吗? - ArchLinuxTux
@ArchLinuxTux 看起来JLS希望将右侧赋值视为表达式而不是真正的赋值(因此LHS值被保存)。"真正的"赋值是最后发生的'*='。 - Tim Biegeleisen
@ArchLinuxTux 在我看来,Java教程中的这种说法是错误的。事实上,JLS并没有说赋值运算符是“从右到左”进行计算的,而是说它是“右结合”的,这才是正确的术语。 - Axel

3

提到Java 文档:

除了赋值运算符外,所有二进制运算符都从左向右计算;赋值运算符是从右向左计算的。

因此,在您的情况下a [0] * = a [0] = b [n]; 发生的是将a [0]赋值为b [n],然后将原始的a [0]乘以该新值。 因此,您的表达式实际上是a [0] * = b [n]

就个人而言,我不会在单个行上两次使用赋值运算符,这可能会让人感到困惑。


如果您不先评估LHS,那么以后如何使用LHS的原始值呢?我认为您链接的Java教程部分有点过于简化了实际情况。在复合赋值中,首先评估LHS,然后评估RHS,进行计算,然后将LHS上的变量分配为计算结果。我认为您引用的段落试图澄清的是a=b=c只会导致将c的值分配给ab - Axel
我同意这里有一个简化,不清楚LHS的a[0]和RHS的a[0]有什么不同,但是RHS的a[0] = b[n]显然会先被计算,接着是a[0](old) *= a[0](new)的计算。在表达式a = b = c中,首先执行b = c,然后再执行a = b - Matt
首先,a[0] 被评估并存储为 a[0](old)。其余部分是正确的。 - Axel
我明白你的意思。我试图强调赋值的顺序,而不是LHS值被存储的事实。 - Matt

2
赋值运算符(与大多数其他运算符不同)在Java中是从右到左进行评估的(文档)。这意味着以下内容:
a[0] *= a[0] = b[n];

实际上被评估为:
a[0] *= (a[0] = b[n]);

括号中的量是一个赋值语句,返回值为b[n],但不会改变a[0]的值。然后进行以下最终赋值:
a[0] = a[0] * b[n]

“*=”和“=”都是赋值运算符,具有相同的优先级。因此,在这种情况下,应用从右到左的规则。”

1
我认为这并没有很清楚地解释为什么 a[0] *= (a[0] = b[n]) 应该与 a[0] *= b[n] 相同,因为人们可能期望在评估 (a[0] = b[n]) 后,a[0] 的值被设置为 b[n],从而导致 a[0] = b[n]*b[n] - Axel
@Axel 是的,这正是我认为会发生的事情,甚至在发布这个问题之前,我尝试了你提到的" a[0] *= (a[0] = b[n]) ",以查看括号是否有任何效果,结果在将 .java 转换为 .class 文件时它们被编译掉了。我肯定感到困惑。 - Hex Crown
@Axel,我已经解释了发生了什么,但可能有点偏差。如果您想贡献,我可以将其制作为Wiki帖子。基本上,RHS上的赋值本身返回b[n],然后又对a[0]进行了另一个赋值。 - Tim Biegeleisen
@TimBiegeleisen,实际发生的是(a[0] = b[n])的值被保留在“隐形”的临时变量中,因为 *= 运算符本质上不能同时读取并写入自身? - Hex Crown
@HexCrown 我不够专业,无法肯定地回答。我的观察是,从测试中得出的结论是赋值本身返回 RHS 的值,但是赋值并没有“粘”在 LHS 的变量上。这对我来说似乎是违反直觉的,但是嘿,这是 Java。 - Tim Biegeleisen
1
嘿,这都在规范里。看看我的回答。LHS的值首先被计算,因此RHS上的赋值对结果没有影响。 - Axel

0
一个似乎还没有在答案中指出的关键点是,*= 不仅仅是一个赋值运算符,而是一个 复合 赋值运算符。 语言规范 指出以下内容是等价的:
E1 op= E2
    <=>
E1 = (T) ((E1) op (E2))

其中op=类似于*=+=等。

因此:

a[0] *= a[0] = b[n    ];

等同于:

a[0] = a[0] * (a[0] = b[n]);

由于从左到右的求值顺序a[0](a[0] = b[n])之前被计算。

所以:

  • 这读取了a[0]的值(称其为“A”)
  • 它读取了b[n]的值(称其为“B”)
  • 它将B分配给a[0] (*)
  • 然后将A * B相乘(称其为“C”)
  • 然后将C存储回a[0]

因此,是的,这等同于将a[0]中的任何内容乘以b[n],因为上面标记为(*)的步骤是多余的:在重新分配之前从未读取分配的值。


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