Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 Map default implementation details

Tags:

java

java-8

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?

like image 253
incaseoftrouble Avatar asked May 24 '17 07:05

incaseoftrouble


People also ask

What is the default implementation of map in Java?

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.

What are the implementation of map in Java?

The three general-purpose Map implementations are HashMap , TreeMap and LinkedHashMap .

What is default method in Java 8?

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.

Is HashMap implementation of map?

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.


1 Answers

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.

like image 181
Rolf Schäuble Avatar answered Nov 15 '22 17:11

Rolf Schäuble