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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/23983832/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-14 09:33:00  来源:igfitidea点击:

Is method reference caching a good idea in Java 8?

javacachingjava-8method-reference

提问by gexicide

Consider I have code like the following:

考虑我有如下代码:

class Foo {

   Y func(X x) {...} 

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

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

}

Suppose that hotFunctionis called very often. Would it then be advisable to cache this::func, maybe like this:

假设它hotFunction经常被调用。那么是否建议缓存this::func,可能是这样的:

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

As far as my understanding of java method references goes, the Virtual Machine creates an object of an anonymous class when a method reference is used. Thus, caching the reference would create that object only once while the first approach creates it on each function call. Is this correct?

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

Should method references that appear at hot positions in the code be cached or is the VM able to optimize this and make the caching superfluous? Is there a general best practice about this or is this highly VM-implemenation specific whether such caching is of any use?

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

采纳答案by Holger

You have to make a distinction between frequent executions of the same call-site, for stateless lambda or stateful lambdas, and frequent uses of a method-referenceto the same method (by different call-sites).

对于无状态 lambda 或有状态 lambda,您必须区分同一调用站点的频繁执行,以及对同一方法的方法引用的频繁使用(通过不同的调用站点)。

Look at the following examples:

看看下面的例子:

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

Here, the same call-site is executed two times, producing a stateless lambda and the current implementation will print "shared".

在这里,同一个调用点被执行两次,产生一个无状态的 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");
  }
}

In this second example, the same call-site is executed two times, producing a lambda containing a reference to a Runtimeinstance and the current implementation will print "unshared"but "shared 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");

In contrast, in the last example are two different call-sites producing an equivalent method reference but as of 1.8.0_05it will print "unshared"and "unshared class".

相比之下,在最后一个例子中是两个不同的调用站点,它们产生了一个等效的方法引用,但1.8.0_05它会打印"unshared""unshared class"



For each lambda expression or method reference the compiler will emit an invokedynamicinstruction that refers to a JRE provided bootstrap method in the class LambdaMetafactoryand the static arguments necessary to produce the desired lambda implementation class. It is left to the actual JRE what the meta factory produces but it is a specified behavior of the invokedynamicinstruction to remember and re-use the CallSiteinstance created on the first invocation.

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

The current JRE produces a ConstantCallSitecontaining a MethodHandleto a constant object for stateless lambdas (and there's no imaginable reason to do it differently). And method references to staticmethod are always stateless. So for stateless lambdas and single call-sites the answer must be: don't cache, the JVM will do and if it doesn't, it must have strong reasons that you shouldn't counteract.

当前的 JRE为无状态 lambda生成一个ConstantCallSite包含一个MethodHandle到一个常量对象的对象(并且没有想象的理由以不同的方式来做)。对static方法的方法引用始终是无状态的。因此,对于无状态 lambdas 和单个调用站点,答案必须是:不要缓存,JVM 会做,如果不缓存,它必须有充分的理由不应该抵消。

For lambdas having parameters, and this::funcis a lambda that has a reference to the thisinstance, things are a bit different. The JRE is allowed to cache them but this would imply maintaining some sort of Mapbetween actual parameter values and the resulting lambda which could be more costly than just creating that simple structured lambda instance again. The current JRE does not cache lambda instances having a state.

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

But this does not mean that the lambda class is created every time. It just means that the resolved call-site will behave like an ordinary object construction instantiating the lambda class that has been generated on the first invocation.

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

Similar things apply to method references to the same target method created by different call-sites. The JRE is allowed to share a single lambda instance between them but in the current version it doesn't, most probably because it is not clear whether the cache maintenance will pay off. Here, even the generated classes might differ.

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



So caching like in your example might have your program do different things than without. But not necessarily more efficient. A cached object is not always more efficient than a temporary object. Unless you really measure a performance impact caused by a lambda creation, you should not add any caching.

所以像你的例子中的缓存可能会让你的程序做不同的事情。但不一定更有效。缓存对象并不总是比临时对象更有效。除非您真正衡量由 lambda 创建引起的性能影响,否则不应添加任何缓存。

I think, there are only some special cases where caching might be useful:

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

  • we are talking about lots of different call-sites referring to the same method
  • the lambda is created in the constructor/class initialize because later on the use-site will
    • be called by multiple threads concurrently
    • suffer from the lower performance of the firstinvocation
  • 我们正在谈论许多不同的调用站点,它们指的是相同的方法
  • lambda 是在构造函数/类初始化中创建的,因为稍后使用站点将
    • 被多个线程并发调用
    • 遭受第一次调用的较低性能

回答by nosid

As far as I understand the language specification, it allows this kind of optimization even if it changes the observable behaviour. See the following quotes from section JSL8 §15.13.3:

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

§15.13.3 Run-time Evaluation of Method References

At run time, evaluation of a method reference expression is similar to evaluation of a class instance creation expression, insofar as normal completion producesa reference to an object. [..]

[..] Eithera new instance of a class with the properties below is allocated and initialized, or an existing instanceof a class with the properties below is referenced.

§15.13.3 方法引用的运行时评估

在运行时,方法引用表达式的求值类似于类实例创建表达式的求值,因为正常完成会生成对对象的引用。[..]

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

A simple test shows, that method references for static methods (can) result in the same reference for each evaluation. The following program prints three lines, of which the first two are identical:

一个简单的测试表明,静态方法的方法引用(可以)为每个评估产生相同的引用。以下程序打印三行,其中前两行相同:

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

I can't reproduce the same effect for non-static functions. However, I haven't found anything in the language specification, that inhibits this optimization.

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

So, as long as there is no performance analysisto determine the value of this manual optimization, I strongly advise against it. The caching affects the readability of the code, and it's unclear whether it has any value. Premature optimization is the root of all evil.

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

回答by user2219808

One situation where it is a good ideal, unfortunately, is if the lambda is passed as a listener that you want to remove at some point in the future. The cached reference will be needed as passing another this::method reference will not be seen as the same object in the removal and the original won't be removed. For example:

不幸的是,它是一个很好的理想的一种情况是,如果 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);
        }
    }
}

Would have been nice to not need lambdaRef in this case.

在这种情况下不需要 lambdaRef 就好了。