Why the code in an object expression can access va

2019-02-12 01:27发布

问题:

In Kotlin,the code in an object expression can access variables from the scope that contains it, just like the following code:

fun countClicks(window: JComponent) {
   var clickCount = 0
   var enterCount = 0
   window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
        clickCount++
    }

    override fun mouseEntered(e: MouseEvent) {
        enterCount++
    }
   })
}

But why? In Java, it's not allowed to do this, because the life cycle of the object is different from the local variables, so the enterCount or clickCount maybe be invalid when you try to access the object. Can someone tell me how Kotlin does this?

回答1:

In Java, you can only capture (effectively) final variables in anonymous classes and lambdas. In Kotlin, you can capture any variable, even if they are mutable.

This is done by wrapping any captured variables in instances of simple wrapper classes. These wrappers just have a single field that contains the captured variables. Since the instances of the wrappers can be final, they can be captured as usual.

So when you do this:

var counter = 0
{ counter++ }()   // definition and immediate invocation, very JavaScript

Something like this happens under the hood:

class Ref<T>(var value: T) // generic wrapper class somewhere in the runtime

val counter = Ref(0);      // wraps an Int of value 0
{ counter.value++ }()      // captures counter and increments its stored value

The actual implementation of the wrapper class is written in Java, and looks like this:

public static final class ObjectRef<T> implements Serializable {
    public T element;

    @Override
    public String toString() {
        return String.valueOf(element);
    }
}

There are also additional wrappers called ByteRef, ShortRef, etc. that wrap the various primitives so that they don't have to be boxed in order to be captured. You can find all the wrapper classes in this file.

Credits go to the Kotlin in Action book which contains the basics of this information, and the example used here.



回答2:

In Kotlin, unlike Java, lambda expressions or anonymous function (as well as local functions and object expressions) can access and modify their closure - variables declared in outer scope. This behavior is as-designed.

Higher order functions and lambdas - Closures

Why Java does not allow this and Kotlin does - capturing closures introduces additional run-time overhead. Java uses simple and fast approach at cost of functionality. Kotlin on the other hand gives you more features - functionality, but it also generates more code behind the scenes to support it.

At the end it is about writing less code to achieve something. If you want to translate above code to Java, it would be more complex.



回答3:

IF you dump the class by using javap you can found kotlin using a IntRef not a int for lambda accessing the mutable variable out of its scope, since in java variables out of the lambda scope are effectively-final or final which means you can't modify the variable at all, for example:

// Kotlin 
var value = 1; // kotlin compiler will using IntRef for mutable variable
val inc = { value++ }; 
inc();
println(value);// 2;

//Java
IntRef value = new IntRef();
value.element = 1;

Runnable inc=()-> value.element++;
inc.run();
println(value.element);// 2;

the code above are equality. so don't modify the mutable variables out of the lambda scope in multi-threads. it will resulting to the wrong result.

another good usage is you needn't modify the mutable variable out of the lambda scope and want to improve the performance optimization. you can use an additional immutable variable to achieve the performance for the lambda , for example:

var mutable = 1;
val immutable = mutable; // kotlin compiler will using int 

val inc = { immutable + 1 };


回答4:

The concept you're referring to is called "capturing".

There's a trick that can be applied in Java: Wrap the mutable value into a final wrapper such as AtomicReference<T> and the compiler will stop complaining:

AtomicReference<Integer> max = new AtomicReference<>(10);
if (System.currentTimeMillis() % 2 == 0) {
    max.set(11)
}
Predicate<Integer> pred = i -> i * 2 > max.get();

That's basically what's happening in Kotlin as well: If a final variable (val) is being captured, it simply gets copied into the lambda. But if, on the other hand, a mutable variable (var) is being captured, its value is wrapped in an instance of Ref:

class Ref<T>(var value: T)

The Ref variable is final and therefore can be captured without a problem. As a result, the mutable variable can be changed from within the lambda.



标签: kotlin