Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Java API have seemingly strange assignments inside if statements?

Tags:

java

java-8

I'm new to programming and Java. I've noticed that, in the Java API, there are methods with strange assignments inside if statements.

Here is an example from the Map interface:

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

Is there some sort of benefit to nesting the assignment this way? Is this purely a style choice? Why not just do the assignment when curValue is first declared?

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

I've noticed this in a lot of the newly added Java 8 methods in the Map interface and elsewhere. This form of nesting the assignment seems unnecessary.

Edit: another example from the Map interface:

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;
}
like image 423
Ivan Avatar asked Feb 07 '18 00:02

Ivan


2 Answers

What this is doing is actually copying to a local variable, this is producing smaller byte code, and it is seen as an absolute extreme way of optimization, you will see this in numerous other places in the jdk code.

One other thing is that reading a local variable multiple times, implies reading a shared variable only once, if that for example would have been a volatile and you would read it only once and work with it within the method.

EDIT

The difference between the two approaches is a single read AS FAR AS I CAN TELL

Suppose we have these two methods:

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

The byte code for this is almost identical, except for: replaceSecond is going to have:

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

While the replace method is going to be:

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

In my understanding, dup does not count as yet another read, so I guess this is what is referred as an extreme optimization?

like image 185
Eugene Avatar answered Oct 23 '22 11:10

Eugene


There is a almost no difference in the generated bytecode (One instruction difference): https://www.diffchecker.com/okjPcBIb

I wrote this to generate the instructions and pretty print them:

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

Usage: new Acid().print();

Output Difference is a single DUP instruction vs. ALOAD instruction..

For those that say.. well your interface isn't the Java JDK's interface.. I also did a diff: https://www.diffchecker.com/zBVTu7jK .

I'm very confident JIT will see the them as the exact same code regardless of whether you initialize the variable outside the if-statement or within it..

All code above was ran on:

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

All in all, it's personal preference..

like image 2
Brandon Avatar answered Oct 23 '22 10:10

Brandon