在 Java8中方法引用缓存是一个好主意吗?

考虑到我的代码如下:

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 方法引用的理解而言,当使用方法引用时,Virtual Machine 会创建一个匿名类的对象。因此,缓存引用将只创建该对象一次,而第一种方法将在每个函数调用上创建该对象。是这样吗?

是应该缓存出现在代码热点位置的方法引用,还是 VM 能够优化这一点,使缓存变得多余?是否存在这方面的通用最佳实践,或者这种高度 VM 实现特定的缓存是否有用?

14928 次浏览

就我对语言规范的理解而言,它允许这种优化,即使它改变了可观察到的行为。请参阅 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);
}
}

对于非静态函数,我无法重现同样的效果。然而,我在语言规范中没有发现任何抑制这种优化的内容。

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

对于无状态的 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 表达式或方法引用,编译器将发出一个 invokedynamic指令,该指令引用类 LambdaMetafactory中提供的 JRE 自助法,以及生成所需 lambda 实现类所需的静态参数。它留给实际的 JRE 元工厂生产什么,但是它是 invokedynamic指令的一个指定行为,记住并重用在第一次调用时创建的 CallSite实例。

当前的 JRE 为无状态 lambdas 生成一个 ConstantCallSite,其中包含一个 MethodHandle到一个常量对象的 ConstantCallSite(而且没有可以想象的理由要以不同的方式生成它)。而对 static方法的方法引用总是无状态的。因此,对于无状态 lambdas 和单个调用站点,答案必须是: 不要缓存,JVM 会这样做,如果它不这样做,那么它一定有很强的理由不应该中和。

对于具有参数的 lambda,并且 this::func是具有对 this实例的引用的 lambda,情况有所不同。JRE 允许缓存它们,但这意味着在实际参数值和生成的 lambda 之间维护某种类型的 Map,这可能比仅仅再次创建简单的结构化 lambda 实例成本更高。当前的 JRE 不缓存具有状态的 lambda 实例。

但这并不意味着每次都会创建 lambda 类。它只是意味着解析的调用站点的行为将类似于一个普通的对象构造,该构造实例化在第一次调用时生成的 lambda 类。

类似的情况也适用于由不同调用站点创建的对同一目标方法的方法引用。JRE 被允许在它们之间共享一个 lambda 实例,但是在当前版本中不允许,很可能是因为不清楚缓存维护是否有效。在这里,甚至生成的类也可能不同。


因此,像您的示例中这样的缓存可能会让您的程序执行与不执行缓存时不同的操作。但不一定更有效率。缓存对象并不总是比临时对象更有效率。除非您确实度量了 lambda 创建对性能的影响,否则不应该添加任何缓存。

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

  • 我们正在讨论许多不同的调用站点,它们使用相同的方法
  • Lambda 是在构造函数/类初始化中创建的,因为稍后 use-site 将
    • 被多个线程并发调用
    • 受到 第一调用性能较低的影响

不幸的是,在一种情况下,它是一个很好的理想状态,那就是如果 lambda 作为侦听器传递,而您希望在将来的某个时刻删除它。在传递另一个 this: : method 引用时,缓存的引用将不会被视为删除时的同一个对象,原始引用也不会被删除。例如:

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 就好了。