Java 8中的方法引用caching一个好主意?

考虑我有如下的代码:

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

假设hotFunction经常被调用。 那么cachingthis::func可能是这样的:

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

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

是否应该caching代码中的热位置的方法引用,或者是能够优化这个并使caching变得多余的虚拟机? 有没有关于这个的一般的最佳实践,或者是这个高度虚拟机实现特定的caching是否有用?

您必须区分同一个调用站点的频繁执行,无状态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实例的引用的lambdaexpression式,当前实现将打印"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"


对于每个lambdaexpression式或方法引用,编译器将发出一个invokedynamic指令,该指令引用LambdaMetafactory类中的JRE提供的引导方法以及产生所需的lambda实现类所需的静态参数。 它留给实际的JRE meta工厂产生,但它是invokedynamic指令的一个特定行为,用于记忆和重用在第一次调用时创build的CallSite实例。

当前JRE生成一个ConstantCallSite其中包含一个MethodHandle对象,用于无状态lambdaexpression式的常量对象(并且没有可以想象的做法)。 而且对static方法的引用永远是无状态的。 因此,对于无状态的lambdaexpression式和单个调用站点,答案必须是:不要caching,JVM会做,如果没有,它必须有很强的理由,你不应该抵消。

对于具有参数的lambdaexpression式来说, this::func是一个对this实例有引用的lambdaexpression式,事情有点不一样。 JRE被允许caching它们,但是这意味着在实际参数值和生成的lambda之间保持某种Map ,这可能比再次创build简单的结构化lambda实例更昂贵。 当前的JRE不caching具有状态的lambda实例。

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

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


所以像你的例子中的caching可能会让你的程序做不同的事情。 但不一定更高效。 caching对象并不总是比临时对象更高效。 除非你确实测量了由lambda创build引起的性能影响,否则不应该添加任何caching。

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

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

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

§15.13.3方法参考的运行时评估

在运行时,方法引用expression式的评估与类实例创buildexpression式的评估类似,只要正常完成产生对对象的引用即可。 [..]

[..]具有以下属性的类的新实例被分配并初始化,或者引用具有下面属性的类的现有实例

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

 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); } } 

我无法为非静态函数重现相同的效果。 但是,我还没有发现任何语言规范,这抑制了这种优化。

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

不幸的是,一种好的理想是,如果lambda作为一个你想在将来某个时候删除的监听器来传递的话。 caching的引用将是需要的,因为传递另一个this ::方法引用将不会被看作是在删除中的同一个对象,原始不会被删除。 例如:

 public class Example { public void main( String[] args ) { new SingleChangeListenerFail().listenForASingleChange(); SingleChangeListenerFail.observableValue.set( "Here be a change." ); SingleChangeListenerFail.observableValue.set( "Here be another change that you probably don't want." ); new SingleChangeListenerCorrect().listenForASingleChange(); SingleChangeListenerCorrect.observableValue.set( "Here be a change." ); SingleChangeListenerCorrect.observableValue.set( "Here be another change but you'll never know." ); } static class SingleChangeListenerFail { static SimpleStringProperty observableValue = new SimpleStringProperty(); public void listenForASingleChange() { observableValue.addListener(this::changed); } private<T> void changed( ObservableValue<? extends T> observable, T oldValue, T newValue ) { System.out.println( "New Value: " + newValue ); observableValue.removeListener(this::changed); } } static class SingleChangeListenerCorrect { static SimpleStringProperty observableValue = new SimpleStringProperty(); ChangeListener<String> lambdaRef = this::changed; public void listenForASingleChange() { observableValue.addListener(lambdaRef); } private<T> void changed( ObservableValue<? extends T> observable, T oldValue, T newValue ) { System.out.println( "New Value: " + newValue ); observableValue.removeListener(lambdaRef); } } } 

在这种情况下不会需要lambdaRef。