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

2022-08-31 12:17:06

考虑我有如下代码:

class Foo {

   Y func(X x) {...} 

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

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

}

假设经常调用它。那么建议缓存,也许像这样:hotFunctionthis::func

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

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

应该缓存出现在代码中处于热位置的方法引用,还是 VM 能够对此进行优化并使缓存变得多余?是否有关于此的一般最佳实践,或者这种高度虚拟机实施是否特定于此类缓存是否有任何用处?


答案 1

您必须区分同一调用站点的频繁执行(对于无状态 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,但它是指令的指定行为,用于记住和重用在第一次调用时创建的实例。invokedynamicinvokedynamicCallSite

当前的 JRE 生成一个 ConstantCallSite,其中包含一个用于无状态 lambda 的常量对象的 MethodHandle(并且没有可以想象的理由来以不同的方式执行此操作)。对方法的方法引用始终是无状态的。因此,对于无状态 lambda 和单个调用站点,答案必须是:不缓存,JVM 会这样做,如果它不这样做,它必须有强烈的理由,你不应该抵消。static

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

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

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


因此,像您示例中的缓存可能会使您的程序执行与没有程序不同的操作。但不一定更有效率。缓存对象并不总是比临时对象更有效。除非您真正衡量 lambda 创建导致的性能影响,否则不应添加任何缓存。

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

  • 我们正在谈论许多不同的调用站点引用相同的方法
  • lambda 是在构造函数/类初始化中创建的,因为稍后在使用站点上将
    • 由多个线程同时调用
    • 遭受第一次调用性能较低的影响

答案 2

不幸的是,这是一个很好的理想的一种情况是,如果lambda作为侦听器传递,您希望在将来的某个时候删除它。缓存的引用将是必需的,因为传递另一个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 就好了。