对于无状态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"
。
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
指令,该指令引用该类中JRE提供的引导方法LambdaMetafactory
以及生成所需lambda实现类所需的静态参数。元工厂生成的内容将留给实际的JRE,但这是invokedynamic
指令的指定行为,可以记住并重新使用CallSite
在第一次调用时创建的实例。
当前的JRE为无状态的lambda生成一个ConstantCallSite
包含aMethodHandle
的常量对象(并且没有可想象的理由进行不同的处理)。并且对方法的方法引用static
始终是无状态的。因此,对于无状态的lambda和单个调用站点,答案必须是:不缓存,JVM会这样做,如果不缓存,则必须有强烈的理由,您不应该进行抵消。
对于具有参数this::func
的lambda ,并且是具有this
实例引用的lambda,情况有所不同。允许JRE缓存它们,但这意味着Map
在实际参数值和所得的lambda之间保持某种形式,这可能比再次创建该简单的结构化lambda实例要昂贵得多。当前的JRE不缓存具有状态的Lambda实例。
但这并不意味着每次都会创建lambda类。这仅意味着已解析的调用站点将像普通对象构造一样,实例化在第一次调用时生成的lambda类。
类似的情况适用于对由不同调用站点创建的相同目标方法的方法引用。允许JRE在它们之间共享一个Lambda实例,但是在当前版本中,它不允许共享,这很可能是因为不清楚缓存维护是否会奏效。在这里,即使生成的类也可能不同。
因此,像您的示例一样进行缓存可能会使您的程序执行不同的操作。但不一定更有效。缓存的对象并不总是比临时对象更有效。除非您真的测量了由lambda创建引起的性能影响,否则不应该添加任何缓存。
我认为,仅在某些特殊情况下缓存可能有用:
- 我们谈论的是涉及相同方法的许多不同呼叫站点
- lambda是在构造函数/类初始化中创建的,因为稍后在使用站点上将