我经历了像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字节码。有人可以对这里的差异有所了解吗?
答案 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;
}
这需要极端字节码优化。您可以对此进行测试,但在这种情况下会产生较小的字节码。而对于核心库来说,更小意味着更好。
第三个示例与前一个示例相近,但假设lock
为volatile
。好吧,因为volatile具有内存语义,它们与通常的变量(引入内存障碍)不同,可以为您提供一致的值。在这种情况下,一致性可能意味着只是这个变量,但是存储之前存储到此特定volatile的所有值(它是如何挥发性的......) )。