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;
}
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?
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..
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With