Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is method reference caching a good idea in Java 8?

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 hotFunction is called very often. Would it then be advisable to cache this::func, maybe like this:

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?

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?

like image 842
gexicide Avatar asked Jun 01 '14 19:06

gexicide


People also ask

What is the benefit of method reference?

The biggest benefit of the method reference or constructor reference is that they make the code even shorter by eliminating lambda expression, which makes the code more readable.

What is the use of method reference in Java 8?

Java provides a new feature called method reference in Java 8. Method reference is used to refer method of functional interface. It is compact and easy form of lambda expression. Each time when you are using lambda expression to just referring a method, you can replace your lambda expression with method reference.

When can we use method reference?

The method references can only be used to replace a single method of the lambda expression. A code is more clear and short if one uses a lambda expression rather than using an anonymous class and one can use method reference rather than using a single function lambda expression to achieve the same.

Which class is used in Java for storing caches?

JCache is a Java API for caching. It provides a set of common interfaces and classes that can be used to temporarily store Java objects in memory. It is a JCP standard represented by JSR 107.

How does caching work with method references in Java?

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?

What is method reference in Java 8?

Java provides a new feature called method reference in Java 8. Method reference is used to refer method of functional interface. It is compact and easy form of lambda expression.

What's new in Java 8 and why should I Care?

Another, really useful feature added to Java 8, are method references, which make Lambda Expressions just that much more concise and simple, by invoking (referencing) the methods using a method name when the Lambda Expression would've been used simply to call a method.

Does JVM cache lambdas?

And method references to static method 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.


2 Answers

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-reference to the same method (by different call-sites).

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".

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 Runtime instance and the current implementation will print "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_05 it will print "unshared" and "unshared class".


For each lambda expression or method reference the compiler will emit an invokedynamic instruction that refers to a JRE provided bootstrap method in the class LambdaMetafactory and 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 invokedynamic instruction to remember and re-use the CallSite instance created on the first invocation.

The current JRE produces a ConstantCallSite containing a MethodHandle to a constant object for stateless lambdas (and there’s no imaginable reason to do it differently). And method references to static method 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.

For lambdas having parameters, and this::func is a lambda that has a reference to the this instance, things are a bit different. The JRE is allowed to cache them but this would imply maintaining some sort of Map between 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.

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.

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.


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.

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 first invocation
like image 156
Holger Avatar answered Sep 20 '22 00:09

Holger


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:

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.

like image 29
user2219808 Avatar answered Sep 18 '22 00:09

user2219808