Quantcast
Channel: Hacker News
Viewing all articles
Browse latest Browse all 25817

Java 8 Method Reference Evaluation

$
0
0

Along with lambda expressions, Java SE 8 introduced method references as a shorthand notation. These are mostly used to reference static methods (e.g. Double::toString) or constructors (e.g. String[]::new), and these uses are straightforward. However, method references to instance methods can yield results that differ from lambda expressions in surprising ways. The basic reason is that the invocation target of a method reference – the part before the ::– is evaluated when its declaration is first encountered, whereas lambda expressions are only evaluated when actually called.

The following program calls instance methods in various ways, using both method references and lambda expressions, to demonstrate this different behavior. Below we’ll go through the output case by case to see what’s happening.

class MethodRefTest {

    public static void main(String[] args) {
        System.out.println("\nConstructor in method reference");
        final Runnable newRef = new Counter()::show;
        System.out.println("Running...");
        newRef.run(); newRef.run();

        System.out.println("\nConstructor in lambda expression");
        final Runnable newLambda = () -> new Counter().show();
        System.out.println("Running...");
        newLambda.run(); newLambda.run();

        System.out.println("\nFactory in method reference");
        final Runnable createRef = Counter.create()::show;
        System.out.println("Running...");
        createRef.run(); createRef.run();

        System.out.println("\nFactory in lambda expression");
        final Runnable createLambda = () -> Counter.create().show();
        System.out.println("Running...");
        createLambda.run(); createLambda.run();

        System.out.println("\nVariable in method reference");
        obj = new Counter(); // NPE if after method reference declaration! 
        final Runnable varRef = obj::show;
        System.out.println("Running...");
        varRef.run(); obj = new Counter(); varRef.run();

        System.out.println("\nVariable in lambda expression");
        obj = null; // no NPE, lambda expression declaration not evaluated
        final Runnable varLambda = () -> obj.show();
        System.out.println("Running...");
        obj = new Counter(); varLambda.run();
        obj = new Counter(); varLambda.run();

        System.out.println("\nGetter in method reference");
        final Runnable getRef = get()::show;
        System.out.println("Running...");
        getRef.run(); obj = new Counter(); getRef.run();

        System.out.println("\nGetter in lambda expression");
        final Runnable getLambda = () -> get().show();
        System.out.println("Running...");
        getLambda.run(); obj = new Counter(); getLambda.run();
    }

    static Counter obj;

    static Counter get() {
        System.out.print("get: ");
        return obj;
    }

    static class Counter {
        static int count;

        Counter() {
            ++count;
            System.out.println(String.format("new Counter(%d)", count));
        }

        static Counter create() {
            System.out.print("create: ");
            return new Counter();
        }

        void show() {
            System.out.println(String.format("Counter(%d).show()", count));
        }
    }
}

Constructor

The first block calls methods on a directly created new instance of the Counter class, which keeps track of the number of instances created during the current run. Here is the output:

Constructor in method reference
new Counter(1)
Running...
Counter(1).show()
Counter(1).show()

Constructor in lambda expression
Running...
new Counter(2)
Counter(2).show()
new Counter(3)
Counter(3).show()

Method reference and lambda expression are both called twice, correctly resulting in two show outputs. However, the constructor call specified in each case is only evaluated once for the method reference, upon its declaration. The created object is then reused. The lambda expression does nothing upon declaration and instead calls the constructor each time it is run.

Factory Method

The second block is equivalent to the first but uses factory methods rather than constructors to obtain new Counter objects. The results are the same as before, showing that the different evaluation order of method expressions is not related to directly using new in the invocation target expression.

Factory in method reference
create: new Counter(4)
Running...
Counter(4).show()
Counter(4).show()

Factory in lambda expression
Running...
create: new Counter(5)
Counter(5).show()
create: new Counter(6)
Counter(6).show()

Variable Access

The third block tests variable access, here using a static field because lambda expressions do not accept mutable local variables.

Variable in method reference
new Counter(7)
Running...
Counter(7).show()
new Counter(8)
Counter(8).show()

Variable in lambda expression
Running...
new Counter(9)
Counter(9).show()
new Counter(10)
Counter(10).show()

Both variants track new variable assignments! This is remarkable because the method reference’s invocation target is indeed once again evaluated on declaration: the field initialization must appear before the declaration, or a NullPointerException occurs. This is not the case for the lambda expression where we can reset the field to null before its declaration, just as long as the field is valid during invocation.

Getter Method

The last block should help solve the mystery. Here the same static field is accessed through a getter method, and then we change the backing field between invocations.

Getter in method reference
get: Running...
Counter(10).show()
new Counter(11)
Counter(11).show()

Getter in lambda expression
Running...
get: Counter(11).show()
new Counter(12)
get: Counter(12).show()

Note the get: appearing before Running... for the method expression, and then no more. The getter is clearly called only once upon declaration. Yet the changed backing field is correctly reflected in the second invocation – just as for the lambda expression which does in fact call the getter twice.

Some pertinent information appears in the end note to §15.13.3 “Run-time Evaluation of Method References” in the Java SE 8 Language Specification:

The timing of method reference expression evaluation is more complex than that of lambda expressions (§15.27.4). When a method reference expression has an expression (rather than a type) preceding the :: separator, that subexpression is evaluated immediately. The result of evaluation is stored until the method of the corresponding functional interface type is invoked; at that point, the result is used as the target reference for the invocation. This means the expression preceding the :: separator is evaluated only when the program encounters the method reference expression, and is not re-evaluated on subsequent invocations on the functional interface type.

What happens in the case of constructors and factory methods is clearly this: the new instance is created upon immediate evaluation, and a reference is stored internally to be reused on each invocation. However, the variable and getter cases show that method expressions do not always simply store and reuse references. As far as I can puzzle out, the distinction is this: when the initial evaluation of the invocation target results in an expression name rather than an unnamed instance, such as our field name obj, then that name is stored and reused – not the reference is currently holds.

ExpressionName is a syntactic element defined in the JLS and usually simply means an identifier. §15.12.4 “Run-Time Evaluation of Method Invocation” distinguishes between ExpressionName and Primary invocation targets, where Primary means constructor or method calls as well as any kind of compound expressions. For normal method calls (including within lambda expressions) there is no observable difference, as in each case the invocation target is fully evaluated on each call. For method references, on the other hand, a full evaluation is performed on declaration but then seemingly backtracks one step if an ExpressionName was found, storing that name for repeated evaluation on each future invocation.

That, at least, is my conclusion after trying to make sense of the strange behavior shown in the test program. A more authoritative documentation would be most welcome.


Viewing all articles
Browse latest Browse all 25817

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>