about 4 years ago

In my impression of the offical bimonthly "Java Manazine," many articles discussed the Lambda expression coming with Java 8, but the closure was not been discussed much. With a rough search, I just found the closure mentioned in the article of discussing the Lambda expression in the 2013 July-August issue. I think the phenomenon comes from two reasons: (1) without Lambda, the anonymous class still can capture the (effectively final) variables; (2) the Lambda expression captured variables are still effectively final. Since the free variables don't perfect work, the closure is not an important point to be advertised with the Java Lambda expression. In the description of the Closure on the Wikipeida (not formal enough, but easy to acess than the books of programming languages), the closure can manipulate the captured free variables just like normal variables. However, the supports of the free variables are not the same in all languages.

Objective C, the language I use much in my work as an example. Through the concept of the shared storage, Apple offical document gives some examples to discuss the variables captured by the code blocks. Well, the concept of the memory storage is an unavoidable flaw for the language sensitive to memory address. However, in this article, I try to use a more abstract description to discuss the variables captured by code blocks. By default, the code block only captures the value of the variable, so any change to the variable after the block is not seen by the block. Thus, in Code List 1, the result of the NSLog is 42, not 84.

Code List 1 - The code block that captures the "anInteger" variable without __block
int anInteger = 42;
SimpleCallback callback = ^{
    NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
callback();

To capture the variable, not its value, in the code block, a modifier __block should be placed on the variable declaration like Code List 2. With the modifier, the result of NSLog becomes 84 because when the code block executed (line 6), the captured variable anInteger has been changed to 84 (line 5).

Code List 2 - The code block that captures the "anInteger" variable with __block
__block int anInteger = 42;
SimpleCallback callback = ^{
    NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
callback();

With the __block modifier, the code block captured the variable, not its value, the value of the variable can be modified inside the code block. Therefore, in Code List 3, the NSLog is called after the execution of callback(), the displayed result of anInteger is 100. I think the design has both advantages and disadvantages. For the language abstraction, the need of the __block modifier forces the programmers be aware of the existence of the memory address that lowers the language abstraction (non-intuitive). However, Objective C can use the modifier to optimize the compilation for performance. Objective C can provide both read-only and read-write captured variables, but also compile-time check, e.g., without __block modifier, any change to the captured variable is seen as compile error. Therefore, I think the __block modifier is not a bad design .

Code List 3 - The code block that changes the value of the captured variable
__block int anInteger = 42;
SimpleCallback callback = ^{
    anInteger = 100;
};
callback();
NSLog(@"Value of original variable is now: %i", anInteger);

Well, it is time back to Java. As mentioned before, without Lambda, the anonymous class can capture the scope-visible vairable x like Code List 4, but the problem is that the captured variable x is effectively final. Therefore, to uncomment the line x = 48; will get a compile error.

Code List 4 - The captured variable in the anonymous class
int x = 24;
Runnable runnable = new Runnable() {

    @Override
    public void run() {
        // x = 48;

        System.out.println(String.format("captured x: %s", x));
    }
};
runnable.run();

Even with Lambda, Code List 5 as an example, the variable x in the Lambda is still a final variable, unable to modify the value. To uncomment the line x = 48; will still get a compile error

Code List 5 - The captured variable in the Lambda expression is still effectively final
int x = 24;
Runnable runnable = () -> {
    // x = 48;

    System.out.println(String.format("captured x: %s", x));
};
runnable.run();

However, the final modifier in Java only limits the change to the variable. If the variable is an object reference, calling the methods of the object is allowed, even that the method will change the status of the object. Thus, Code List 4 can be modified to Code List 6. The same, trying to uncomment the line x = new AtomicInteger(48); will get a compile error, but use x.set(48); can change the value of x. (This also applies to Objective C)

Code List 6 - The captured object in the anonymous class
AtomicInteger x = new AtomicInteger(24);
Runnable runnable = new Runnable() {

    @Override
    public void run() {
        System.out.println(String.format("original x: %s", x));
        // x = new AtomicInteger(48);

        x.set(48);
    }
};
runnable.run();
System.out.println(String.format("x after run(): %s", x));

Therefore, Code List 5 can be also modified to Code List 7. Unfortunately, the data types that support Autoboxing and Unboxing, e.g., Integer, are immutable data type. To use captured variables is not as convenient as JavaScript, or other languages that treat primitive types as objects. However, this way can provide some kinds of free variables similar.

Code List 7 - The captured object in the Lambda expression
AtomicInteger x = new AtomicInteger(24);
Runnable runnable = () -> {
    System.out.println(String.format("original x: %s", x));
    // x = new AtomicInteger(48);

    x.set(48);
};
runnable.run();
System.out.println(String.format("x after run(): %s", x));

Although Java 8 supports Lambda expression, I think the closure is still not the first-class citizen of Java. And as mentioned in the previous article, for testability, besides the one-line Lambda or the code too simple to break, I still like to write small and easy-to-test classes, not Lambda expression. To capture variables? Injecting variables through the constructor can be considered as a good way. As for when to write one-line Lambda or the code too simple to break, I think Stream, the new Collection API, opens the large possibility.

← Java 8 初探 - Closure Java 8 初探 - Stream →