对于无状态lambda或有状态lambda的相同调用点的频繁执行,以及对于多个调用点使用相同方法的方法引用的频繁使用,你需要进行区分。
请看以下示例:
Runnable r1=null;
for(int i=0; i<2; i++) {
Runnable r2=System::gc;
if(r1==null) r1=r2;
else System.out.println(r1==r2? "shared": "unshared");
}
这里,相同的调用点被执行了两次,产生了一个无状态的 lambda 表达式,当前实现将打印 "shared"
。
Runnable r1=null;
for(int i=0; i<2; i++) {
Runnable r2=Runtime.getRuntime()::gc;
if(r1==null) r1=r2;
else {
System.out.println(r1==r2? "shared": "unshared");
System.out.println(
r1.getClass()==r2.getClass()? "shared class": "unshared class");
}
}
在这个第二个例子中,相同的调用点被执行了两次,生成一个包含对
Runtime
实例的引用的lambda表达式。当前的实现会打印
"unshared"
和
"shared class"
。
在这个第二个例子中,相同的调用点被执行了两次,生成一个包含对Runtime
实例的引用的lambda表达式。当前的实现会打印"unshared"
和"shared class"
。
Runnable r1=System::gc, r2=System::gc
System.out.println(r1==r2? "shared": "unshared")
System.out.println(
r1.getClass()==r2.getClass()? "shared class": "unshared class")
相反,在最后一个示例中,有两个不同的调用站点产生了等效的方法引用,但在 1.8.0_05
中它将打印 "unshared"
和 "unshared class"
。
对于每个 lambda 表达式或方法引用,编译器都会发出一个 invokedynamic
指令,该指令引用位于类 LambdaMetafactory
中的 JRE 提供的引导方法,并提供静态参数以生成所需的 lambda 实现类。实际的 JRE 会决定元工厂生产什么,但是记住并重复使用在第一次调用时创建的 CallSite
实例是 invokedynamic
指令的指定行为。
当前 JRE 会生成一个包含常量对象的
ConstantCallSite
和一个到该常量对象的
MethodHandle
,用于无状态 lambda(没有想象中的原因来进行不同处理)。而对于
static
方法的方法引用始终是无状态的。因此,对于无状态 lambda 和单个调用站点的回答必须是:不要缓存,JVM 将执行此操作,如果它没有执行,则必须有很强的理由不进行干预。
对于具有参数的 lambda(例如
this::func
是具有对
this
实例的引用的 lambda),情况有所不同。JRE 可以缓存它们,但这意味着需要维护某种实际参数值与生成的 lambda 之间的
Map
,这可能比仅再次创建那个简单结构化 lambda 实例更昂贵。当前的 JRE 不会缓存带有状态的 lambda 实例。
但这并不意味着 lambda 类每次都会被创建。这只是意味着解析的调用站点会像普通对象构造一样行事,即在第一次调用时生成的 lambda 类的实例化。
相同目标方法的方法引用由不同的调用站点创建时,类似的事情也适用。JRE 允许它们共享单个 lambda 实例,但在当前版本中它并没有这样做,最可能是因为不清楚缓存维护是否会得到回报。在这里,甚至生成的类也可能不同。
因此,像您的示例中那样进行缓存可能会使您的程序与没有缓存时执行不同的操作。但不一定更有效率。缓存对象并不总是比临时对象更高效。除非您真正测量了 lambda 创建所导致的性能影响,否则不应添加任何缓存。
我认为,只有一些特殊情况下缓存才可能有用:
- 我们谈论许多不同的调用站点引用相同的方法
- lambda 在构造函数/类初始化中创建,因为后续使用站点将
invokedynamic
实现的。我怀疑通过缓存函数对象会提高性能,相反可能会抑制编译器的优化。你比较过这两种方式的性能吗? - nosid