为什么在比较中要使用赋值?

12

阅读源代码时,我偶然发现了JDK源代码中的这种方法。请注意vnewValue的声明和初始化。我们这里有'好'的未定义值,赋值在比较中,这是'很好的',还有为了更糟的可读性而添加的额外括号。以及其他的代码坏味道。

default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
    Objects.requireNonNull(mappingFunction);
    V v;
    if ((v = get(key)) == null) {
        V newValue;
        if ((newValue = mappingFunction.apply(key)) != null) {
            put(key, newValue);
            return newValue;
        }
    }

    return v;
}

但是为什么呢?相对于简单的方式(最好使用否定的 v 比较),按照上述方式编写代码是否有实际好处呢?

default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
    Objects.requireNonNull(mappingFunction);
    V v  = get(key);
    if (v == null) {
        V newValue = mappingFunction.apply(key);
        if (newValue != null) {
            put(key, newValue);
            return newValue;
        }
    }

    return v;
}

除了炫耀Java构造,还有我不知道的实际好处吗?与“简单”方式相比,是不是更好一些?


3
如果v不是本地变量,它可以防止在设置和读取v之间更改v时出现竞争条件。但这里唯一的好处似乎只是吓退人们不去动这段代码 :-) - Michael Butscher
1
这是来自ConcurrentMap,对吧?你可能想要链接到确切的源代码和版本以使其更清晰。我在许多JDK类中都看到过这种情况;这可能是作者的习惯用法,但并不真正遵守严格的样式指南。无论如何,这是一个相关的问题。 - Mick Mnemonic
5
在风格上纯粹只是意见不同。 - Joe C
2
这只是一种奇特的代码风格(可能是Doug Lea的?它会违反他自己的约定:“避免在if和while条件内进行赋值(``='')。”)。出于你所列举的原因,我不建议在你自己的代码中使用这种风格。 - Petr Janeček
1
这个问题不应该被关闭为基于观点的。它并没有要求观点,而是客观地询问是否有益处或者是否只是一个观点问题。像往常一样,关闭投票者只是花费最少的精力无休止地点击问题,并试图感到重要。 - Boann
显示剩余5条评论
3个回答

10

#微优化(但在标准库的情况下可能很重要),以及:

#惯性:这种模式在90年代的C程序员中很常见,因此计算机科学的巨人可能仍然使用这种风格。

对于新的业务逻辑来说,除非性能真的很关键,否则没有必要编写这样的代码。


(微)优化:

javac(JDK 11)为原始(“糟糕”)版本生成的字节码比(更好的)代码少了一个JVM操作。为什么?JDK版本使用赋值运算符的返回值(而不是从变量加载值)进行if条件评估。

然而,这更多是由于javac的优化可能性的限制,而不是编写不易读的代码的原因。

以下是引用问题的JDK版本的字节码:

   0: aload_2
   1: invokestatic  #2                  // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
   4: pop
   5: aload_0
   6: aload_1
   7: invokevirtual #3                  // Method get:(Ljava/lang/Object;)Ljava/lang/Object;
  10: dup
  11: astore_3
  12: ifnonnull     39
  15: aload_2
  16: aload_1
  17: invokeinterface #4,  2            // InterfaceMethod java/util/function/Function.apply:(Ljava/lang/Object;)Ljava/lang/Object;
  22: dup
  23: astore        4
  25: ifnull        39
  28: aload_0
  29: aload_1
  30: aload         4
  32: invokevirtual #5                  // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
  35: pop
  36: aload         4
  38: areturn
  39: aload_3
  40: areturn

以下是更易读版本的字节码:
public V computeIfAbsent(K key,
                         Function<? super K, ? extends V> mappingFunction) {
    Objects.requireNonNull(mappingFunction);
    final V v = get(key);
    if (v == null) {
        final V newValue = mappingFunction.apply(key);
        if (newValue != null) {
            put(key, newValue);
            return newValue;
        }
    }

    return v;
}

...并且字节码是:

   0: aload_2
   1: invokestatic  #2                  // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
   4: pop
   5: aload_0
   6: aload_1
   7: invokevirtual #3                  // Method get:(Ljava/lang/Object;)Ljava/lang/Object;
  10: astore_3
  11: aload_3
  12: ifnonnull     40
  15: aload_2
  16: aload_1
  17: invokeinterface #4,  2            // InterfaceMethod java/util/function/Function.apply:(Ljava/lang/Object;)Ljava/lang/Object;
  22: astore        4
  24: aload         4
  26: ifnull        40
  29: aload_0
  30: aload_1
  31: aload         4
  33: invokevirtual #5                  // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
  36: pop
  37: aload         4
  39: areturn
  40: aload_3
  41: areturn

1
我相信这是正确的答案。我认为,JDK开发人员不能开车,至少不应该基于自己的喜好驱动工作,而应该基于测量的性能。像集合这样的核心库必须快速。因此,在这里进行微小优化是有意义的。我的眼睛仍然有点疼,但那只是我看惯了的习惯。所以,我再次相信这是正确的答案。感谢您帮助我理解它! - Martin Mucha
1
虽然这可能是一种优化,但我认为shmosel的答案更合理。只有作者知道真正的原因。 - Mick Mnemonic
1
“这更多是javac优化可能性的限制,而不是编写较不可读代码的原因” - 难道优化器的限制不是编写较不可读代码的可能原因吗? - Bernhard Barker
3
朋友们,仅通过查看字节码就不能对代码的“最优性”做出有意义的陈述。实际执行的是本机代码。即时编译器(JIT)将这些字节码收集起来,为它们收集解释器统计信息,然后生成优化的本机代码指令。如果您想基于编译器输出尝试分析代码性能,请查看本机代码。 - Stephen C
从最初编写的版本中生成的本地指令(或可能是字节码)可能会更有意义。 - jpmc26
显示剩余2条评论

7

我认为这主要是个人偏好的问题;我印象中Doug Lea更喜欢这种简洁的风格。但如果你看一下方法的原始版本,它会更有意义,因为它与需要内联赋值的newValue配对:

default V computeIfAbsent(K key,
        Function<? super K, ? extends V> mappingFunction) {
    V v, newValue;
    return ((v = get(key)) == null &&
            (newValue = mappingFunction.apply(key)) != null &&
            (v = putIfAbsent(key, newValue)) == null) ? newValue : v;
}

5

我同意:这是代码质量的问题,我更喜欢第二个版本。

在单个表达式中混用赋值和比较可能会使代码更加简短,例如,如果你使用循环:

V v;
while ((v = getNext()) != null) {
    // ... do something with v ...
}

为了消除代码异味,你需要更多的代码,并且需要在两个地方分配变量:
V v = getNext();
while (v != null) {
    // ...
    v = getNext();
}

或者您需要在赋值后移动循环退出条件:
while (true) {
    V v = getNext();
    if (v == null)
        break;
    // ...
}

对于一个if语句来说,这样做毫无意义。即使是在循环中,我也会避免使用它。


2
我不会说它是神秘的。也许是无处不在但不建议使用的? - Mad Physicist
这让我非常想起旧的C代码,比如 if (t = p->next)...,它还依赖于将非空指针隐式转换为true / 空指针转换为false。但你是对的:神秘 不是正确的词。 - Codo
这是一个很好的例子,说明这种结构确实可以使代码更易读/避免DRY原则的违反。 - R.. GitHub STOP HELPING ICE

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