方法引用在Java 8中缓存一个好主意?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (29)

考虑我有如下代码:

class Foo {

   Y func(X x) {...} 

   void doSomethingWithAFunc(Function<X,Y> f){...}

   void hotFunction(){
        doSomethingWithAFunc(this::func);
   }

}

假设hotFunction经常被调用。那么缓存是否可取this::func,可能是这样的:

class Foo {
     Function<X,Y> f = this::func;
     ...
     void hotFunction(){
        doSomethingWithAFunc(f);
     }
}

就我对java方法引用的理解而言,虚拟机在使用方法引用时会创建一个匿名类的对象。因此,缓存引用只会创建一次该对象,而第一种方法会在每次函数调用时创建该对象。它是否正确?

应该在代码中的热点位置出现的方法引用被缓存,还是虚拟机能够优化这个并使缓存变得多余?有关于此的一般性最佳实践还是这种高度虚拟机实现特定的缓存是否有用?

提问于
用户回答回答于

您必须区分同一个调用站点的频繁执行,无状态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 meta工厂产生,但它是invokedynamic指令的指定行为,以记住和重用CallSite在第一次调用时创建的实例。

当前的JRE产生一个ConstantCallSite包含一个MethodHandle不变对象的无状态lambda表达式(并且没有可以想象的做法)。方法引用static方法总是无状态的。因此,对于无状态lambda表达式和单个调用站点,答案必须是:不缓存,JVM将执行,如果不执行,它必须有很强的理由,不应该抵消。

对于具有参数的lambda表达式,并且this::func是具有对this实例的引用的lambda 表达式,事情有点不同。JRE被允许缓存它们,但是这意味着Map在实际参数值和产生的lambda之间保持某种形式,这可能比再次创建简单的结构化lambda实例更昂贵。当前的JRE不缓存具有状态的lambda实例。

但这并不意味着每次都创建lambda类。它只是意味着已解析的调用站点将像普通对象构造一样实例化在第一次调用时生成的lambda类。

类似的东西适用于由不同呼叫站点创建的相同目标方法的方法引用。允许JRE在它们之间共享一个lambda实例,但在当前版本中它并不是这样,很可能是因为不清楚缓存维护是否会得到回报。在这里,即使生成的类也可能不同。

所以像你的例子那样缓存可能会让你的程序做不同的事情。但不一定更高效。缓存对象并不总是比临时对象更高效。除非您真的衡量由lambda创建引起的性能影响,否则不应该添加任何缓存。

我认为,只有一些特殊情况下缓存可能是有用的:

  • 我们正在谈论很多不同的呼叫站点,指的是同一个方法
  • lambda是在构造函数/类初始化中创建的,因为稍后会在使用站点上进行初始化
    • 被多个线程同时调用
    • 遭受第一次调用的较低性能

用户回答回答于

就我理解语言规范而言,即使它改变了可观察行为,它也允许这种优化。请参阅JSL815.13.3节中的以下引用:

§15.13.3方法参考的运行时评估 在运行时,方法引用表达式的评估类似于类实例创建表达式的评估,只要正常完成产生对对象的引用即可。[..] [..] 要么分配具有以下属性的类的新实例并进行初始化,要么引用具有以下属性的类的现有实例

一个简单的测试表明,静态方法的方法引用(可以)为每个评估产生相同的参考。以下程序打印三行,其中前两行是相同的:

public class Demo {
    public static void main(String... args) {
        foobar();
        foobar();
        System.out.println((Runnable) Demo::foobar);
    }
    public static void foobar() {
        System.out.println((Runnable) Demo::foobar);
    }
}

我无法为非静态函数重现相同的效果。但是,我还没有在语言规范中找到任何可以抑制这种优化的东西。

所以,只要没有性能分析来确定这个手动优化的价值,我强烈建议不要这样做。缓存影响代码的可读性,目前还不清楚它是否有任何价值。不成熟的优化是万恶之源。

扫码关注云+社区