您必须区分同一调用站点的频繁执行(对于无状态 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");
}
}
在第二个示例中,执行同一调用站点两次,生成一个包含对实例的引用的 lambda,并且当前实现将打印但是 。Runtime
"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 表达式或方法引用,编译器将发出一条指令,该指令引用 LambdaMetafactory
类中 JRE 提供的引导方法以及生成所需 lambda 实现类所需的静态参数。元工厂生成的内容留给实际的 JRE,但它是指令的指定行为,用于记住和重用在第一次调用时创建的实例。invokedynamic
invokedynamic
CallSite
当前的 JRE 生成一个 ConstantCallSite
,其中包含一个用于无状态 lambda 的常量对象的 MethodHandle
(并且没有可以想象的理由来以不同的方式执行此操作)。对方法的方法引用始终是无状态的。因此,对于无状态 lambda 和单个调用站点,答案必须是:不缓存,JVM 会这样做,如果它不这样做,它必须有强烈的理由,你不应该抵消。static
对于具有参数的 lambda,并且是具有对实例引用的 lambda,情况略有不同。允许JRE缓存它们,但这意味着在实际参数值和生成的lambda之间维护某种方式,这可能比再次创建简单的结构化lambda实例更昂贵。当前 JRE 不缓存具有状态的 lambda 实例。this::func
this
Map
但这并不意味着每次都会创建 lambda 类。它只是意味着解析的调用站点将像一个普通的对象构造一样,实例化在第一次调用时生成的 lambda 类。
类似的事情也适用于对由不同调用站点创建的同一目标方法的方法引用。允许 JRE 在它们之间共享单个 lambda 实例,但在当前版本中则不允许,很可能是因为不清楚缓存维护是否会得到回报。在这里,甚至生成的类也可能不同。
因此,像您示例中的缓存可能会使您的程序执行与没有程序不同的操作。但不一定更有效率。缓存对象并不总是比临时对象更有效。除非您真正衡量 lambda 创建导致的性能影响,否则不应添加任何缓存。
我认为,只有一些特殊情况下缓存可能有用:
- 我们正在谈论许多不同的调用站点引用相同的方法
- lambda 是在构造函数/类初始化中创建的,因为稍后在使用站点上将