82

考虑我有如下代码:

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

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

4

3 回答 3

90

对于无状态 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"but "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 是在构造函数/类初始化中创建的,因为稍后在使用站点将
    • 被多个线程同时调用
    • 遭受第一次调用的较低性能
于 2014-06-02T09:30:48.790 回答
11

不幸的是,一种理想的情况是,如果 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 会很好。

于 2015-09-28T21:38:24.087 回答
9

据我了解语言规范,即使它改变了可观察的行为,它也允许这种优化。请参阅JSL8 §15.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);
    }
}

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

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

于 2014-06-01T21:10:39.810 回答