方法参考缓存在Java 8中是个好主意吗?

gex*_*ide 72 java caching java-8 method-reference

考虑我有以下代码:

class Foo {

   Y func(X x) {...} 

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

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

}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

Hol*_*ger 72

您必须区分频繁执行相同的调用站点,无状态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");
    }
Run Code Online (Sandbox Code Playgroud)

这里,同一个调用站点被执行两次,生成一个无状态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");
  }
}
Run Code Online (Sandbox Code Playgroud)

在第二个示例中,同一个调用站点执行两次,生成一个包含对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");
Run Code Online (Sandbox Code Playgroud)

相比之下,在最后一个例子中是两个不同的呼叫站点,产生一个等效的方法参考,但1.8.0_05它将打印"unshared""unshared class".


对于每个lambda表达式或方法引用,编译器将发出一个invokedynamic指令,该指令引用类中的JRE提供的引导方法LambdaMetafactory以及生成所需lambda实现类所必需的静态参数.实际的JRE由meta工厂生成,但它是invokedynamic指令的指定行为,用于记住和重用CallSite第一次调用时创建的实例.

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

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

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

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


因此,在您的示例中进行缓存可能会让您的程序执行与其他程序不同的操作.但不一定更有效率.缓存对象并不总是比临时对象更有效.除非您真的测量由lambda创建引起的性能影响,否则不应添加任何缓存.

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

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

  • 澄清:术语"调用站点"是指执行将创建lambda的`invokedynamic`指令.它不是执行功能接口方法的地方. (3认同)
  • @Marko Topolnik:那将是一个合规的编译策略,但不,从 Oracle 的 jdk 到 `1.8.0_40` 就不是这样了。这些 lambda 不会被记住,因此可以被垃圾收集。但请记住,一旦链接了 `invokedynamic` 调用站点,它可能会像普通代码一样得到优化,即逃逸分析适用于此类 lambda 实例。 (2认同)
  • 似乎没有一个名为`MethodReference`的标准库类.你的意思是"MethodHandle"吗? (2认同)
  • @Lii:你是对的,这是一个错字.有趣的是,似乎没有人注意过. (2认同)
  • @Artem Novikov:lambda 表达式实例与创建它的字节码指令(“invokedynamic”)相关联。对于无状态 lambda 表达式,指令应该始终生成相同的实例,因此可能只是有一个指向该实例的指针。为了捕获 lambda,相同的指令必须潜在地创建持有不同状态的不同实例,因此,普通指针是不够的。并且该指令不是特定于实例的,它可以针对任意“this”值执行,因此“this”在这方面并不特殊。 (2认同)

nos*_*sid 7

据我所知,语言规范允许这种优化,即使它改变了可观察的行为.请参阅JSL8§15.13.3部分中的以下引用:

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

在运行时,方法引用表达式的求值类似于类实例创建表达式的求值,只要正常完成产生对对象的引用即可.[..]

[..] 要么与下面的性质的类的新实例被分配和初始化,或现有的实例与下面的属性的类的被引用.

一个简单的测试表明,静态方法(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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我不能为非静态函数重现相同的效果.但是,我没有在语言规范中找到任何禁止此优化的内容.

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


use*_*808 7

遗憾的是,如果lambda作为一个你想要在将来某个时候删除的监听器传递,那么它是一个很好的理想的一种情况.将需要缓存的引用作为传递另一个:: 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下本来不需要lambdaRef会很好.