Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can method reference use non-final variables?

I had some confusion about inner classes and lambda expression, and I tried to ask a question about that, but then another doubt arose, and It's probable better posting another question than commenting the previous one.

Straight to the point: I know (thank you Jon) that something like this won't compile

public class Main {
    public static void main(String[] args) {
        One one = new One();

        F f = new F(){      //1
            public void foo(){one.bar();}   //compilation error
        };

        one = new One();
    }
}

class One { void bar() {} }
interface F { void foo(); }

due to how Java manages closures, because one is not [effectively] final and so on.

But then, how come is this allowed?

public class Main {
    public static void main(String[] args) {
        One one = new One();

        F f = one::bar; //2

        one = new One();
    }
}

class One { void bar() {} }
interface F { void foo(); }

Is not //2 equivalent to //1? Am I not, in the second case, facing the risks of "working with an out-of-date variable"?

I mean, in the latter case, after one = new One(); is executed f still have an out of date copy of one (i.e. references the old object). Isn't this the kind of ambiguity we're trying to avoid?

like image 560
Luigi Cortese Avatar asked Oct 10 '15 10:10

Luigi Cortese


People also ask

Can reference variables be final?

A reference variable declared final can never be reassigned to refer to an different object. However, the data within the object can be changed. So, the state of the object can be changed but not the reference. With variables, the final modifier often is used with static to make the constant a class variable.

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.

What is method reference and where can it be used?

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.

Why do we use method reference?

A method reference is an alternative to creating an instance of a reference type. Among several other uses, method references can be used to create a functional interface instance (that is, an instance of a functional interface type).


3 Answers

A method reference is not a lambda expression, although they can be used in the same way. I think that is what is causing the confusion. Below is a simplification of how Java works, it is not how it really works, but it is close enough.

Say we have a lambda expression:

Runnable f = () -> one.bar(); 

This is the equivalent of an anonymous class that implements Runnable:

Runnable f = new Runnable() {     public void run() {        one.bar();     } } 

Here the same rules apply as for an anonymous class (or method local class). This means that one needs to effectively final for it to work.

On the other hand the method handle:

Runnable f = one::bar; 

Is more like:

Runnable f = new MethodHandle(one, one.getClass().getMethod("bar")); 

With MethodHandle being:

public class MethodHandle implements Runnable {     private final Object object;     private final Method method;      public MethodHandle(Object object, java.lang.reflect.Method method) {         this.object = Object;         this.method = method;     }      @Override     public void run() {         method.invoke(object);     } } 

In this case, the object assigned to one is assigned as part of the method handle created, so one itself doesn't need to be effectively final for this to work.

like image 86
Mark Rotteveel Avatar answered Oct 14 '22 10:10

Mark Rotteveel


Your second example is simply not a lambda expression. It's a method reference. In this particular case, it chooses a method from a particular object, which is currently referenced by the variable one. But the reference is to the object, not to the variable one.

This is the same as the classical Java case:

One one = new One(); One two = one; one = new One();  two.bar(); 

So what if one changed? two references the object that one used to be, and can access its method.

Your first example, on the other hand, is an anonymous class, which is a classical Java structure that can refer to local variables around it. The code refers to the actual variable one, not the object to which it refers. This is restricted for the reasons that Jon mentioned in the answer you referred to. Note that the change in Java 8 is merely that the variable has to be effectively final. That is, it still can't be changed after initialization. The compiler simply became sophisticated enough to determine which cases will not be confusing even when the final modifier is not explicitly used.

like image 30
RealSkeptic Avatar answered Oct 14 '22 11:10

RealSkeptic


The consensus appears to be that this is because when you do it using an anonymous class, one refers to a variable, whereas when you do it using a method reference, the value of one is captured when the method handle is created. In fact, I think that in both cases one is a value rather than a variable. Let's consider anonymous classes, lambda expressions and method references in a bit more detail.

Anonymous classes

Consider the following example:

static Supplier<String> getStringSupplier() {
    final Object o = new Object();
    return new Supplier<String>() {
        @Override
        public String get() {
            return o.toString();
        }
    };
}

public static void main(String[] args) {
    Supplier<String> supplier = getStringSupplier();
    System.out.println(supplier.get());  // Use o after the getStringSupplier method returned.
}

In this example, we are calling toString on o after the method getStringSupplier has returned, so when it appears in the get method, o cannot refer to a local variable of the getStringSupplier method. In fact it is essentially equivalent to this:

static Supplier<String> getStringSupplier() {
    final Object o = new Object();
    return new StringSupplier(o);
}

private static class StringSupplier implements Supplier<String> {
    private final Object o;

    StringSupplier(Object o) {
        this.o = o;
    }

    @Override
    public String get() {
        return o.toString();
    }
} 

Anonymous classes make it look as if you are using local variables, when in fact the values of these variables are captured.

In contrast to this, if a method of an anonymous class references the fields of the enclosing instance, the values of these fields are not captured, and the instance of the anonymous class does not hold references to them; instead the anonymous class holds a reference to the enclosing instance and can access its fields (either directly or via synthetic accessors, depending on the visibility). One advantage is that an extra reference to just one object, rather than several, is required.

Lambda expressions

Lambda expressions also close over values, not variables. The reason given by Brian Goetz here is that

idioms like this:

int sum = 0;
list.forEach(e -> { sum += e.size(); }); // ERROR

are fundamentally serial; it is quite difficult to write lambda bodies like this that do not have race conditions. Unless we are willing to enforce -- preferably at compile time -- that such a function cannot escape its capturing thread, this feature may well cause more trouble than it solves.

Method references

The fact that method references capture the value of the variable when the method handle is created is easy to check.

For example, the following code prints "a" twice:

String s = "a";
Supplier<String> supplier = s::toString;
System.out.println(supplier.get());
s = "b";
System.out.println(supplier.get());

Summary

So in summary, lambda expressions and method references close over values, not variables. Anonymous classes also close over values in the case of local variables. In the case of fields, the situation is more complicated, but the behaviour is essentially the same as capturing the values because the fields must be effectively final.

In view of this, the question is, why do the rules that apply to anonymous classes and lambda expressions not apply to method references, i.e. why are you allowed to write o::toString when o is not effectively final? I do not know the answer to that, but it does seem to me to be an inconsistency. I guess it's because you can't do as much harm with a method reference; examples like the one quoted above for lambda expressions do not apply.

like image 36
9 revs Avatar answered Oct 14 '22 09:10

9 revs