为什么Java API在if语句中有看似奇怪的赋值?

时间:2018-02-07 00:21:50

标签: java java-8

我是编程和Java的新手。我注意到,在Java API中,if语句中有一些奇怪的赋值方法。

以下是Map界面的示例:

default V replace(K key, V value) {
    V curValue;
    if (((curValue = get(key)) != null) || containsKey(key)) {
        curValue = put(key, value);
    }
    return curValue;
}

以这种方式嵌套作业是否有某种好处?这纯粹是一种风格选择吗?为什么不在首次声明curValue时进行分配?

// why not do it like this?
default V replace(K key, V value) {
    V curValue = get(key); // not nested
    if (curValue != null || containsKey(key)) {
        curValue = put(key, value);
    }
    return curValue;
}

我在Map界面和其他地方的许多新添加的Java 8方法中都注意到了这一点。这种嵌套作业的形式似乎是不必要的。

编辑:Map界面的另一个例子:

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;
}

2 个答案:

答案 0 :(得分:3)

这样做实际上是复制到本地变量,这会生成更小的字节代码,并且它被视为绝对极端优化方式,你将在jdk代码中的许多其他地方看到这个。

另一个原因是多次读取局部变量,意味着只读取一次共享变量,例如,如果它只是一个volatile而你只读一次并在方法中使用它。

修改

这两种方法之间的差异是单次读取 AS FAR AS I TOT

假设我们有这两种方法:

V replace(K key, V value) {
    V curValue;
    if ((curValue = map.get(key)) != null || map.containsKey(key)) {
        curValue = map.put(key, value);
    }
    return curValue;
} 

V replaceSecond(K key, V value) {
    V curValue = map.get(key); // write
    if (curValue != null || map.containsKey(key)) { // read
        curValue = map.put(key, value); // write
    }
    return curValue;
}

这个字节代码几乎相同,除了:replaceSecond将会:

 astore_3 // V curValue = map.get(key); store to curValue
 aload_3  // curValue != null; read the value from curValue

虽然replace方法将是:

 dup      // duplicate whatever value came from map.get(key)
 astore_3 // store the value, thus "consuming" it form the stack

根据我的理解,dup不算再读一遍,所以我猜这就是所谓的极端优化

答案 1 :(得分:2)

生成的字节码几乎没有区别(一个指令差异):https://www.diffchecker.com/okjPcBIb

我写了这个来生成说明并打印出来:

package acid;

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.tree.ClassNode;
import jdk.internal.org.objectweb.asm.tree.InsnList;
import jdk.internal.org.objectweb.asm.util.Printer;
import jdk.internal.org.objectweb.asm.util.Textifier;
import jdk.internal.org.objectweb.asm.util.TraceMethodVisitor;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;

public class Acid {
    public interface Map<K,V> {
        default V replace(K key, V value) {
            V curValue;
            if (((curValue = get(key)) != null) || containsKey(key)) {
                curValue = put(key, value);
            }
            return curValue;
        }

        boolean containsKey(Object key);
        V get(Object key);
        V put(K key, V value);
    }


    public void print() {

        try {
            ClassNode node = loadRelativeClassNode(Map.class.getName());
            node.methods.stream().filter(m -> m.name.equals("replace")).forEach(m -> {

                System.out.println("\n\nMethod: " + m.name + "" + m.desc + "\n");
                System.out.println("-------------------------------\n");

                Printer printer = new Textifier();
                TraceMethodVisitor visitor = new TraceMethodVisitor(printer);
                Arrays.stream(m.instructions.toArray()).forEachOrdered(instruction -> {
                    instruction.accept(visitor);
                    StringWriter writer = new StringWriter();
                    printer.print(new PrintWriter(writer));
                    printer.getText().clear();
                    System.out.print(writer.toString());
                });
            });

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //Usage: `loadJVMClassNode("java.util.Map")`
    private static ClassNode loadJVMClassNode(String cls) throws IOException, ClassNotFoundException {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        Class clz = loader.loadClass(cls);
        InputStream url = clz.getResourceAsStream(clz.getSimpleName() + ".class");
        ClassNode node = new ClassNode();
        ClassReader reader = new ClassReader(url);
        reader.accept(node, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
        return node;
    }

    //Usage: `loadJVMClassNode(Acid.Map.class.getName())`
    private static ClassNode loadRelativeClassNode(String cls) throws IOException, ClassNotFoundException {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        Class clz = loader.loadClass(cls);
        InputStream url = clz.getResourceAsStream(("./" + clz.getName() + ".class").replace(clz.getPackage().getName() + ".", ""));
        ClassNode node = new ClassNode();
        ClassReader reader = new ClassReader(url);
        reader.accept(node, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
        return node;
    }
}

用法:new Acid().print();

输出差异是单个DUP指令与ALOAD指令..

对于那些说..那么你的界面不是Java JDK的界面..我也做了一个差异:https://www.diffchecker.com/zBVTu7jK

我非常有信心JIT会将它们视为完全相同的代码,无论您是在if语句之外还是在其中初始化变量。

上面的所有代码都在运行:

java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

OSX High-Sierra 10.13.3
IDE: Intelli-J

总而言之,这是个人喜好..