Finally, we start to look at lambda/closure in Java, but I'll discuss an old proposal in 2009. Discussing the old proposal helps us understanding why lambda/closure evolved into the current state in JDK8.
If you want to sort a list of integers, you may write the following code with JDK versions prior to JDK8.
// The asList and sort methods have been
// static imported from Arrays and Collections
List<Integer> numbers = asList(3, 2, 6, 4);
sort(numbers, new Comparator<Integer>() {
public int compare(Integer n1, Integer n2) {
return -n1.compareTo(n2);
}
});
// static imported from Arrays and Collections
List<Integer> numbers = asList(3, 2, 6, 4);
sort(numbers, new Comparator<Integer>() {
public int compare(Integer n1, Integer n2) {
return -n1.compareTo(n2);
}
});
You have to tell the sort method what's the order of two compared numbers. Java, however, doesn't have first-class functions, so you have to provide an instance of Comparator. The above example creates the instance from an anonymous class, but the syntax makes us hard to figure out what we want the sort method to do. We may use a proper variable name to improve the readability. For example:
List<Integer> numbers = asList(3, 2, 6, 4);
Comparator<Integer> descending = new Comparator<Integer>() {
public int compare(Integer n1, Integer n2) {
return -n1.compareTo(n2);
}
};
sort(numbers, descending);
Comparator<Integer> descending = new Comparator<Integer>() {
public int compare(Integer n1, Integer n2) {
return -n1.compareTo(n2);
}
};
sort(numbers, descending);
Now, we can clearly see what the sort method does, but the anonymous class is still annoying. If we can use lambda/closure, the code will be short and clear. For example:
List<Integer> numbers = asList(3, 2, 6, 4);
sort(numbers, (n1, n2) -> -n1.compareTo(n2));
sort(numbers, (n1, n2) -> -n1.compareTo(n2));
In Java, an anonymous class is a similar thing to lambda/closure, and that's why some guys said Java doesn't need lambda/closure. Basically, this is true. We just have to write more code in some scenarios. In recent years, writing clean code is becoming more and more important. Even though Java without lambda/closure can write everything you want, using lambda/closure can write clean code read by you or someone later and improve productivity. Just as Uncle Bob Martin said in Clean Code:
The code you are trying to write today will be hard or easy to write depending on how hard or easy the surrounding code is to read.
The length syntax of anonymous classes is not the only problem. If an anonymous class wants to catch a local variable, the variable must be final. For example:
public static FactorProducer createFactorProducer(max) {
final int[] primes = ...;
FactorProducer producer = new FactorProducer() {
public int factor() {
...
while(pow(primes[i], 2)) {
...
}
}
};
return producer;
}
final int[] primes = ...;
FactorProducer producer = new FactorProducer() {
public int factor() {
...
while(pow(primes[i], 2)) {
...
}
}
};
return producer;
}
In Java, a local variable has a different lifecycle from an object. Once a method is executed, all local variables end their lifecycles. If an instance of an anonymous class really catches a local variable and is returned from the method, what will happen when accessing that dead variable? To avoid the problem, Java compiler forces you to add a modifier final in front of the local variable used by an anonymous class. The caught variable (not the referred object) will be read-only. Under the table, Java compiler creates a new variable in the anonymous class, copies the value of the final variable to it. You don't really catch the outer local variable; you just get a new variable and a copied value.
Does a final variable in anonymous classes cause a problem? Or, does a read-only free variable in closure cause a problem? That depends on what you want a closure to do. In Understanding Lambda/Closure, Part 2, we’ve seen closures may simulate private feature in JavaScript, so writable free variables are necessary in that condition. Writable free variables, however, are fundamentally serial, such as:
var sum = 0;
[1, 2, 3, 4, 5].forEach(function(elem) {
sum += elem;
});
[1, 2, 3, 4, 5].forEach(function(elem) {
sum += elem;
});
Writable free variables also mean mutable states, and they'll cause locking problems in concurrent programming. To avoid complex variable lifecycles and concurrent problems, as we'll see in later articles, prohibiting capture of mutable local variables is the intent of JDK8.
In 2009,defining a lambda and declaring a variable which accepts a lambda can be as follow:
#int(int) doubler = #(int x)(2 * x);
doubler.(3) // calling a lambda
doubler.(3) // calling a lambda
The above example is similar to the following code:
int doubler(int x) {
return 2 * x;
}
doubler(3);
return 2 * x;
}
doubler(3);
A lambda with two int parameters and one returned int value is as follow:
#int(int, int) sum = #(int x, int y)(x + y)
The above code like defining a method as follow:
int sum(int x, int y) {
return x + y;
}
return x + y;
}
Writing code similar to the bubbleSort function in Understanding Lambda/Closure, Part 4 is as follow:
def bubbleSort(int[] arr, #boolean(int, int) order) {
...
boolean o = order.(a, b);
...
}
int[] arr = new int[] {2, 5, 1, 7, 8};
bubbleSort(arr, #(int a, int b)(a > b));
...
boolean o = order.(a, b);
...
}
int[] arr = new int[] {2, 5, 1, 7, 8};
bubbleSort(arr, #(int a, int b)(a > b));
The point here is that, the old proposal needs a function type when declaring a variable which accepts a lambda. The syntax of declaring a function type places the returned type in the left side, but the syntax of defining a lambda places the function body in the right side. Think about a question: if we need a lambda to return a lambda, what will it looks?
##int(int)(int) sum = #(int x)(#(int y)(x + y));
Oh...is this a C/C++ pointer? Another question is that, if we need to declare function types for lambda/closure, we'll need to create a bunch of new APIs specific for lambda/closure. The existing API will not benefit directly from lambda/closure. Solving complex syntax problems involving generics is also necessary.
Fortunately, JDK8 doesn't adopt the syntax of specific function types. It uses a single abstract method type, lately called a functional interface, instead. We'll look at that in later articles.