Java 8 Map默认实现细节

时间:2017-05-24 07:09:46

标签: java java-8

我经历了像getOrDefault这样的新Java 8 Map方法的默认实现,并注意到一些有点奇怪的东西。例如,考虑getOrDefault方法。它的实现如下。

default V getOrDefault(Object key, V defaultValue) {
    V v;
    return ((v = get(key)) != null) || containsKey(key) ? v : defaultValue;
}

现在,这里的“奇怪”事件是((v = get(key)) != null中的“使用分配结果”模式。据我所知,这种特殊的模式是不鼓励的,因为它会妨碍可读性。一个IMO更简洁的版本将是

default V getOrDefault(Object key, V defaultValue) {
    V v = get(key);
    return v != null || containsKey(key) ? v : defaultValue;
}

我的问题是,除了编码标准/习惯之外,是否有任何特殊理由使用前者而不是后者模式。特别是,我想知道这两个版本是否跟踪和性能相当?

我唯一可以想到的是编译器可能会例如确定containsKey通常更快评估并因此首先评估它,但据我所知,短路必须保持执行顺序(至少是C的情况)。

编辑:在@ruakh建议之后,这里是两个字节码(由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

  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

我必须承认,即使经过多年的Java编码,我也不知道如何解释Java字节码。有人可以对这里的差异有所了解吗?

3 个答案:

答案 0 :(得分:10)

这只是一个风格问题。有些人更喜欢最紧凑的代码, 而其他人喜欢更长但更简单的代码。看来有些开发者 在Java核心库上工作属于前一组。

就效率而言,两种变体都是相同的。

让我们看看编译器对这两种变体实际做了些什么:

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

现在让我们使用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
}

如您所见,代码大致完全相同。唯一的小差异在于线条 两种方法均为图5和图6。一个只是复制堆栈的最高值 (记住,Java字节码假定基于堆栈的机器模型),而另一个 从实例变量加载(相同)值。

当Just-in-Time编译器从该字节生成真实的机器代码时 代码,它将执行各种优化,例如决定哪些值 写回RAM并保存在CPU寄存器中。我认为这是安全的 假设在这些优化发生后,没有区别 离开了什么。

答案 1 :(得分:3)

@ruakh在评论中指出,当containsKey不为空时,为了性能目的,不会调用v方法。

default V getOrDefault(Object key, V defaultValue) {
  V v;
  return ((v = get(key)) != null) || containsKey(key) ? v : defaultValue;
  //                                 ^-- short-circuit if get(key) != null.
}

@Eugene在答案中指出的原因。

答案 2 :(得分:2)

说实话,这也困扰了我很长一段时间。我找到了3个不同的,但在jdk源中有类似的场景。第一个是你问过的问题,得到了一个答案,所以不要说任何事情。第二个略有不同:

 ReentrantLock lock = new ReentrantLock();

 public void test(){
     ReentrantLock lock = this.lock;  
 }

这需要极端字节码优化。您可以对此进行测试,但在这种情况下会产生较小的字节码。而对于核心库来说,更小意味着更好。

第三个示例与前一个示例相近,但假设lockvolatile。好吧,因为volatile具有内存语义,它们与通常的变量(引入内存障碍)不同,可以为您提供一致的值。在这种情况下,一致性可能意味着只是这个变量,但是存储之前存储到此特定volatile的所有值(它是如何挥发性的......) )。