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字节码。有人可以稍微阐明一下这里的区别吗?

这只是一个风格问题。有些人更喜欢最紧凑的代码, 而其他人则更喜欢更长但更简单的代码。似乎一些开发人员 在 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 字节码假设基于堆栈的机器模型),而另一个 从实例变量加载(相同的)值。

当即时编译器从此字节生成真实的机器代码时 代码,它将执行各种优化,例如决定哪些值 写回 RAM 以及要保留在 CPU 寄存器中的哪些内容。我认为这是安全的 假设在这些优化发生后,没有区别 留下了什么。

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

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在他的回答中指出的原因。

老实说,这也困扰了我很长一段时间。我在 jdk 源中发现了 3 种与您的场景不同但类似的场景。第一个是你问过并得到答案的那个,所以不打算说什么。第二个略有不同:

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

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

第三个示例与前一个示例接近,但想象一下lockvolatile.好吧,由于易失性具有与通常变量不同的内存语义(它引入了内存障碍),因此会为您提供一致的值。在这种情况下,一致性可能意味着不仅仅是这个变量,而是存储之前存储的所有值到这个特定的易失性(这就是挥发性的工作方式......

最新更新