I went through the default implementation of the new Java 8 Map methods like getOrDefault
and noticed something slightly weird. Consider for example the getOrDefault
method. It is implemented as follows.
default V getOrDefault(Object key, V defaultValue) {
V v;
return ((v = get(key)) != null) || containsKey(key) ? v : defaultValue;
}
Now, the "weird" thing here is the "Result of assignment used" pattern in ((v = get(key)) != null
. To my knowledge, this particular pattern is discouraged, since it rather obstructs readability. An IMO more concise version would be something along the lines of
default V getOrDefault(Object key, V defaultValue) {
V v = get(key);
return v != null || containsKey(key) ? v : defaultValue;
}
My question is if there is any particular reason to use the former over the latter pattern aside from coding standards / habits. In particular, I wonder if these two versions are trace and performance equivalent?
The only thing I could imagine is that the compiler might e.g. determine that containsKey
is usually faster to evaluate and thus evaluates it first, but as far as I know short-circuiting has to preserve order of execution (this is the case for C at least).
EDIT: Following @ruakh suggestion, here are the two bytecodes (as generated by javap -c
)
public V getOrDefault(java.lang.Object, V);
Code:
0: aload_0
1: aload_1
2: invokeinterface #1, 2 // InterfaceMethod get:(Ljava/lang/Object;)Ljava/lang/Object;
7: dup // <-- difference here
8: astore_3
9: ifnonnull 22
12: aload_0
13: aload_1
14: invokeinterface #2, 2 // InterfaceMethod containsKey:(Ljava/lang/Object;)Z
19: ifeq 26
22: aload_3
23: goto 27
26: aload_2
27: areturn
and
public V getOrDefault(java.lang.Object, V);
Code:
0: aload_0
1: aload_1
2: invokeinterface #1, 2 // InterfaceMethod get:(Ljava/lang/Object;)Ljava/lang/Object;
7: astore_3
8: aload_3 // <-- difference here
9: ifnonnull 22
12: aload_0
13: aload_1
14: invokeinterface #2, 2 // InterfaceMethod containsKey:(Ljava/lang/Object;)Z
19: ifeq 26
22: aload_3
23: goto 27
26: aload_2
27: areturn
I have to admit that even after years and years of Java coding I have no idea how to interpret Java bytecode. Could someone kindly shed some light on the difference here?
The most common class that implements the Java Map interface is the HashMap. It is a hash table based implementation of the Map interface. It implements all of the Map operations and allows null values and one null key. Also, this class does not maintain any order among its elements.
The three general-purpose Map implementations are HashMap , TreeMap and LinkedHashMap .
Default methods enable you to add new functionality to existing interfaces and ensure binary compatibility with code written for older versions of those interfaces. In particular, default methods enable you to add methods that accept lambda expressions as parameters to existing interfaces.
HashMap is a part of the Java collection framework. It uses a technique called Hashing. It implements the map interface. It stores the data in the pair of Key and Value.
This is only a style issue. Some people prefer the most compact code possible, while others prefer longer but simpler code. It seems some of the developers working on the Java core library belong to the former group.
In terms of efficiency, both variants are identical.
Let's have a look at what the compiler actually does with these two variants:
public class ExampleMap<K, V> extends HashMap<K, V> {
V getOrDefault1(Object key, V defaultValue) {
V v;
return ((v = get(key)) != null) || containsKey(key) ? v : defaultValue;
}
V getOrDefault2(Object key, V defaultValue) {
V v = get(key);
return v != null || containsKey(key) ? v : defaultValue;
}
}
Now let's dump the generated bytecode, using javap -c ExampleMap
:
Compiled from "ExampleMap.java"
public class ExampleMap<K, V> extends java.util.HashMap<K, V> {
public ExampleMap();
Code:
0: aload_0
1: invokespecial #1 // Method java/util/HashMap."<init>":()V
4: return
V getOrDefault1(java.lang.Object, V);
Code:
0: aload_0
1: aload_1
2: invokevirtual #2 // Method get:(Ljava/lang/Object;)Ljava/lang/Object;
5: dup
6: astore_3
7: ifnonnull 18
10: aload_0
11: aload_1
12: invokevirtual #3 // Method containsKey:(Ljava/lang/Object;)Z
15: ifeq 22
18: aload_3
19: goto 23
22: aload_2
23: areturn
V getOrDefault2(java.lang.Object, V);
Code:
0: aload_0
1: aload_1
2: invokevirtual #2 // Method get:(Ljava/lang/Object;)Ljava/lang/Object;
5: astore_3
6: aload_3
7: ifnonnull 18
10: aload_0
11: aload_1
12: invokevirtual #3 // Method containsKey:(Ljava/lang/Object;)Z
15: ifeq 22
18: aload_3
19: goto 23
22: aload_2
23: areturn
}
As you can see, the code is mostly identical. The only small difference is in lines 5 and 6 of both methods. One just duplicates the top value of the stack (remember, Java bytecode assumes a stack-based machine model), while the other loads the (identical) value from an instance variable.
When the Just-in-Time compiler generates real machine code out of this byte code, it will perform various optimizations, like deciding which values to write back to RAM and which to keep in CPU registers. I think it is safe to assume that after these optimizations have happened, there is no difference left whatsoever.
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