As a Groovy enthusiast I was thrilled when Java 8 announced to introduce the lamda feature for functional programming – as Groovy provides the equivalent closure feature since its earliest releases. However, after taking a closer look at how Java 8 lambdas work and after some practical experience I have to conclude that Groovy’s closures are in every aspect, but most importantly from an “ease of development” point of view, superior to their Java 8 counterpart. Here comes my in-depth assessment.
View on GitHub: This article is accompanied by a GitHub project which contains all the code examples presented within the text, some additional lambda / closure examples and JUnit tests which prove equality of each lambda / closure implementation pair. Feel free to check it out!
Java lambdas vs. Groovy closures – some basics
Groovy is a (optionally) dynamic and static typed language for the JVM which perfectly integrates into Java – you can call Groovy code from within Java, and Java code from within Groovy. Since its earliest releases, Groovy supports the so-called closures which since Java 8 now have their Java counterpart known as lambda expressions.However, there are some basic differences between lambdas and closures:
- A Java lambda really is
a functional interface which behaves like an anonymous inner class whereas Groovy closures
are instances of the
Closure
class which provides additional methods for inspection and dynamic behavior. - Both APIs are optimized
for parallel collection processing. For lambdas, you use the Collection API’s
#parallelStream()
method instead of#stream()
; in Groovy, concurrent implementations of iterator methods are provided by the GPars extension. - Groovy’s syntax does not support lambda expressions directly; however, you can put a (syntactically oftentimes more concise) Groovy closure wherever an API expects a lambda expression.
- Lambdas can only access variables of the surrounding context if they are either explicitly or “effectively” final; Groovy closures have access to any variables of the surrounding context.
- Both languages support type inference to reduce the need to explicitly declare parameter types. When in “dynamic typing mode”, Groovy doesn’t evaluate any type information at compile time at all.
Syntax
Parameters matter
First of all, Groovy adds the implicit parameterit
to
closures which operate on a single element, referring to that single element in the current
iteration:INPUT_LIST.collect {it.toUpperCase()}In Java, there’s no such thing. You are forced to explicitly declare each parameter. The code line closest to above Groovy code would thus be (I’ll address the otherwise bloated collection API in a bit):
INPUT_LIST.stream().map(it -> it.toUpperCase()).collect(Collectors.toList());Also, Groovy closures don’t force you to declare a single parameter if you don’t need to refer to it at all.
For instance, this Groovy code would return a list of “default” Strings, one for each element in the collection (it’s a silly example for illustration purposes):
INPUT_LIST.collect {"default"}Whereas in Java, the equivalent would force you to still declare a parameter, e.g. for the
#map(…)
function,
even if you ignore it. The IDE will even complain about this.INPUT_LIST.stream().map(it -> "default").collect(Collectors.toList());Finally, having more than one parameter for lambda expressions enforces a syntax change.
While in Grovy, the syntax stays the same whether a lambda comes with one or more parameters, e.g. in
#inject(…)
:INPUT_LIST.inject("") { result, it -> result + it }in Java, you must remember to enclose lambda parameters in brackets if there’s more than one of them:
INPUT_LIST.stream().reduce("", ((result, it) -> result + it));c.f. the
#map(…)
method
where the single it
parameter
is not enclosed in brackets.Bracket and semicolon hell
As we just saw, the number of parameters changes the syntax of Java lambdas. Unfortunately, surprising syntactical changes do not stop there.Lambdas also apply a change in brackets rule when you give up type inference in favor of explicitly declared types. Let’s compare these two equivalent function calls in Groovy:
INPUT_LIST.collect {String myVar -> myVar.toUpperCase()} INPUT_LIST.collect {myVar -> myVar.toUpperCase()}Although the first line uses explict typing, the syntax stays the same. Here’s the respective Java code:
INPUT_LIST.stream().map((String it) -> it.toUpperCase()).collect(Collectors.toList()); INPUT_LIST.stream().map(it -> it.toUpperCase()).collect(Collectors.toList());Mind the extra brackets around the lambda parameter when explicit typing is used!
As for semicolon use: In Groovy, statements do in general end with a newline rather than with a semicolon which matches the syntax of many modern (JVM) languages. Therefore, it’s just natural that the need to set semicolons in closures is already reduced when compared with Java lambdas.
Apart from the obvious semicolon changes, Java lambdas mess up pretty much the whole syntax as soon as you use more than one statement in a lambda expression.
For instance, this Groovy code:
INPUT_LIST.collect {String ret = it.toUpperCase(); ret}uses the implict parameter
it
and
the closure body consists of two statements, separated by semicolon. The syntax is clear,
concise and in accord with previous examples. (Although the example code as such is silly, of
course.)However, in the lambda equivalent, lots of changes are enforced just by the presence of another expression in the lambda body:
INPUT_LIST.stream().map(it -> {String ret = it.toUpperCase(); return ret;}).collect(Collectors.toList());
- the body must be enclosed in angle brackets
- every statement, including the last one, must end with a semicolon
- the last statement must
be an explicit
return
statement
Go dynamic if you want to
Note that although Groovy can optionally be dynamic typed locally at your discretion, all the code samples in this article work with dynamic typing as well as with static typing.However, in some situations, it might be useful to take advantage of dynamic typing to reduce the need for boilerplate code. It turns out that this is especially true for collection iterations as covered by closures / lambdas.
To illustrate this, I will here repeat an example I created for a previous blog post about some of Groovy’s top features:
In Groovy:
private static final List<?> INVENTORY = [ new Car(), new Car(), new TV(), new TV(), new TV() ] private static class Car { public final int cost = 10000 } private static class TV { public final int cost = 1000 } @CompileDynamic public static int sumDynamic() { return INVENTORY.sum { it.cost } as int }In Java:
private static final List<?> INVENTORY = new ArrayList<>(Arrays.asList( new Car(), new Car(), new TV(), new TV(), new TV() )); private static class Car { public final int cost = 10000; } private static class TV { public final int cost = 1000; } public static int sumDynamic() { return INVENTORY.stream().mapToInt(it -> { if (it instanceof Car) { return ((Car)it).cost; } else if (it instanceof TV) { return ((TV)it).cost; } else { throw new IllegalArgumentException("Unexpected type: " + it); } }).sum(); }In Java, you are forced to check and cast an object even if you know that a given method is present; in Groovy, due to its dynamic type system known as “duck typing”, you are free to call any method on any object.
I still think it’s an architectural impediment to have the dynamic vs. static choice taken away globally by the programming language’s design.
…plus all the other Groovy features
Let’s not forget that when programming in Groovy, you also have a vast number of additional features and syntactic enhancements at hand which can dramatically increase ease of development, many of which are particularly useful when working with iterations and collections, most notably collection literals, String interpolation and JavaBean properties.And, if you really need to, Groovy also gives you the tools to modify existing types, e.g. add additional collection methods, which matches the closure / lambda functional programming style much better than implementing additional features in static helper methods.
Defining a function
Due to the different underlying mechanisms of Groovy closures and Java lambdas, defining your own function works quite different in those two languages.For Groovy, you always build a (return value-typed)
Closure
which
can be executed with #call(…)
or
by simply putting the arguments in brackets, just as a method call. Here’s an example of
defining, referencing and using a custom Groovy closure:public static int calculateWithFunction() { Closure<Integer> function = {int x -> x*2}; return useFunction(function, INPUT_VALUE); } private static int useFunction(Closure<Integer> function, int inputValue) { return function(inputValue); }Unfortunately, I didn’t find any official information about the maximum supported arity (number of parameters) of a closure, but I’ve empirically tested it with 7, and any more than that would result in unreadable code anyways.
Now in Java, there are actually 43 interfaces you can or rather must choose from in order to implement a lambda function, each one representing a specific case out of a variety of functional interface use cases such as very generic functions, functions for each primitive type, functions distinguished by their arity, functions with or without return value. This might sound like a quite flexible solution, but I don’t agree with that; rather, I’d like to cite here from the What's Wrong in Java 8 article (which is a fun read as well):
“What a mess! And furthermore, the methods of these interfaces have different names. Object functions have a method named apply, where methods returning numeric primitives have method name applyAsInt, applyAsLong, or applyAsDouble. Functions returning boolean have a method called test, and suppliers have methods called get, or getAsInt, getAsLong, getAsDouble, or getAsBoolean. (They did not dare calling BooleanSupplier “Predicate” with a test method taking no argument. I really wonder why!)
One thing to note is that there are no functions for byte, char, short and float. Nor are there functions for arity greater that two. Needless to say, this is totally ridiculous.”
For this example, I implemented the very generic
Function
interface:public static int calculateWithFunction() { Function<Integer, Integer> function = x -> x*2; return useFunction(function, INPUT_VALUE); } private static int useFunction(Function<Integer, Integer> function, int inputValue) { return function.apply(inputValue); }Note that if I would explicitly specify the function’s parameter’s type as
int
instead
of Integer
,
it wouldn’t compile as Autoboxing doesn’t work with functions due to the underlying generics
mechanisms. I would have to resort to the less generic IntFunction
interface.Method references
Both languages also support referencing methods as functions (lambdas or closures, respectively). Referencing a function as such works really similar in Java and Groovy, only the operator is different.Here’s the Groovy solution:
Closure<Character> function = INPUT_STRING.&charAt; return function(0) as char;And here’s the Java solution:
Function<Integer, Character> function = INPUT_STRING::charAt; return function.apply(0);