【问题标题】:Java, "a[1] *= a[1] = b[n + 1]" not working as expected?Java,“a[1] *= a[1] = b[n + 1]”没有按预期工作?
【发布时间】:2018-08-30 03:57:45
【问题描述】:

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

例子:

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.

既然如此,那岂不是说明我原来的例子应该可以工作?

谁能解释发生了什么以及为什么这段代码不能按预期工作?

【问题讨论】:

  • 乍一看,这似乎是那些新手问题之一,但仔细想想,你说得有道理。但是,我永远不想在生产代码中看到这一点。经验法则:写成a[0]=b[n]*b[n],这样更容易阅读。这样的代码只在编译器必须适应大约 30k 内存的时代才有用,并且由于编译器没有优化,您可能会通过一些棘手的摆弄来节省一些 cpu 周期。现在没有时间,但在 Java 中,它应该全部在规范中进行设置(而在 C 中,我认为它要么是未定义的,要么是实现定义的)。

标签: java variable-assignment multiplication assignment-operator


【解决方案1】:

我认为到目前为止的答案都不正确。起作用的是像a *= b 这样的复合表达式的求值。简而言之,左手边的值在右手边之前计算。来自JLS(强调我的):

在运行时,表达式以两种方式之一进行计算。

如果左侧操作数表达式不是数组访问表达式, 那么:

首先,计算左侧操作数以产生一个变量。如果 此评估突然完成,然后赋值表达式 出于同样的原因突然完成;右手操作数不是 评估并且没有分配发生。

否则,左侧操作数的值被保存,然后 评估右手操作数。如果此评估完成 突然,然后赋值表达式突然完成 同样的原因,没有分配发生。

否则,左侧变量的保存值和 右手操作数用于执行二元运算 由复合赋值运算符指示。如果这个操作 突然完成,然后赋值表达式突然完成 出于同样的原因,没有分配发生。

否则,二进制运算的结果被转换为类型 左侧变量的值,经过值集转换(第 5.1.13 节) 到适当的标准值集(不是扩展指数值 set),并将转换结果存入变量中。

在你的例子中:

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 教程中使用)。它被称为 right-**associative* assJLS 是这样说的:

有12个赋值运算符;都是语法上的 右关联(它们从右到左分组)。因此,a=b=c 意味着 a=(b=c),将c的值赋给b,然后赋值 从 b 到 a。

【讨论】:

  • 所以如果a *= b 的左侧被评估,评估拳头,无论a 的值当时是缓存,那么表达式的第二部分将运行,然后当解决该部分时,它使用先前缓存的a 版本来完成最终存储在实际a 变量中的赋值?
  • 我不明白的是,为什么这被称为“从右到左” - 评估。因为赋值左侧的值在右侧的值之前评估(并保存),不是吗?
  • @ArchLinuxTux 似乎 JLS 希望将右侧赋值视为表达式而不是实际赋值(因此保存了 LHS 值)。 “真正的”分配是最后发生的*=
  • @ArchLinuxTux 恕我直言,这在 Java 教程中是完全错误的。事实上,JLS 并没有说作业是“从右到左”评估的。它说“右结合”,这是正确的术语。
【解决方案2】:

参考Javadocumentation

除赋值运算符外的所有二元运算符都是从左到右计算的;赋值运算符从右到左求值。

所以在您的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
  • 我同意有一个简化,目前尚不清楚 LHS a[0] 与 RHS a[0] 有何不同,但 RHS a[0] = b[n] 显然首先被评估,然后是评估 @987654334 @。在表达式a = b = c 中,首先是b = c,然后是a = b
  • 第一个 a[0] 被评估并存储为 a[0](old)。其余的都是正确的。
  • 我明白你的意思。我试图强调分配的顺序,而不是存储 LHS 值这一事实。
【解决方案3】:

赋值运算符(与大多数其他运算符不同)在 Java 中从 rightleft 进行评估 (documentation)。这意味着以下内容:

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

实际上被评估为:

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

括号中的数量是赋值,返回值b[n],但不改变a[0]的值。然后,进行以下最终分配:

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

*== 都是赋值运算符,具有相同的优先级。所以在这种情况下,从右到左的规则适用。

【讨论】:

  • 我认为这并不能真正说明为什么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 是的,这就是我认为会发生的事情,我什至尝试了你提到的,在发布这个问题之前,“a[0] *= (a[0] = b[n])”看看括号是否有任何影响,当 .java 转换为 .class 文件时,它们最终被编译出来。目瞪口呆地让我摸不着头脑。
  • @Axel 我已经解释了会发生什么,但我可能会有点偏离。如果您想贡献,我可以将其设为 Wiki 帖子。基本上,RHS 本身的赋值返回b[n],然后对a[0] 进行另一个赋值。
  • @TimBiegeleisen 所以实际上发生的事情是 (a[0] = b[n]) 的值被保存在一个“不可见”的临时变量中,因为 *= 运算符基本上不能读写同时对自己?
  • 嘿,这一切都在规范中。阅读我的答案。首先计算 LHS 的值,因此对 RHS 的赋值对结果没有影响。
【解决方案4】:

答案中似乎还没有指出的一个关键点是*= 不仅仅是一个赋值运算符,而是一个复合 赋值运算符。

The language spec 表示以下是等价的:

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]) 之前被评估,因为left-to-right evaluation order

所以:

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

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

【讨论】:

    猜你喜欢
    • 2018-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-08-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多