Java Lambda Streams and Groovy Closures Comparisons
Want to learn more about the difference between in lambda streams in both Java and Groovy? Check out this post to learn more about the differences between them.
Join the DZone community and get the full member experience.
Join For FreeIn this blog post, we will look at some of the proverbial operations on a list data structure and make some comparisons between Java 8/9 and the Groovy syntax. First let's talk about the data structure. Think of it as just a simple Rugby player who has name and a rating.
Java
class RugbyPlayer {
private String name;
private Integer rating;
RugbyPlayer(String name, Integer rating) {
this.name = name;
this.rating = rating;
}
public String toString() {
return name + "," + rating;
}
public String getName() {
return name;
}
public Integer getRating() {
return rating;
}
}
//...
//...
List<RugbyPlayer> players = Arrays.asList(
new RugbyPlayer("Tadgh Furlong", 9),
new RugbyPlayer("Bundee AKi", 7),
new RugbyPlayer("Rory Best", 8),
new RugbyPlayer("Jacob StockDale", 8)
);
Groovy
@ToString
class RugbyPlayer {
String name
Integer rating
}
//...
//...
List<RugbyPlayer> players = [
new RugbyPlayer(name: "Tadgh Furlong", rating: 9),
new RugbyPlayer(name: "Bundee AKi", rating: 7),
new RugbyPlayer(name: "Rory Best", rating: 8),
new RugbyPlayer(name: "Jacob StockDale", rating: 8)
]
Find a Specific Record
Java
// Find Tadgh Furlong
Optional<RugbyPlayer> result = players.stream()
.filter(player -> player.getName().indexOf("Tadgh") >= 0)
.findFirst();
String outputMessage = result.isPresent() ? result.get().toString() : "not found";
Groovy
println players.find{it.name.indexOf("Tadgh") >= 0}
Comments
- The Java lambda has just one parameter — player. This doesn't need to be typed, since its type can be inferred. Note: this lambda only uses one parameter. If there were two parameters in the parameter list, the parenthesis would be needed around the parameter list.
- In Java, a stream must be created from the list first. A lambda is then used before performing a function that will then return an Optional.
- The lambda definition doesn't need a return statement. It also doesn't need {} braces or one of those semi-colons to complete a Java statement. However, you can use
{}
if you want. However, if you do include brackets, you must include the;
and the return statement. Note: if your lambda is more than one line, you don't have a choice — you must use{}
. It is a recommended, best practice to keep lambdas short and just one line. - Java 8 supports fluent APIs for pipeline stream operations. This is also supported in Groovy Collection operations.
- In Java a player variable that is specified for the Lambda, the Groovy closure doesn't need to specify a variable. It can just use "it," which is the implicit reference to the parameter (similar to _ in Scala).
- The Java filter API takes a parameters of the type predicate. A functional interface means that it can be used as the assignment target for a lambda expression or method reference. Along with that, predicate is a type of functional interface. It's one abstract method is the boolean test(T t). In this case, while using the lambda, the player corresponds to t. The body definition should evaluate if it is true or a false. In our case, the
player.getName().indexOf("Tadgh")
will always evaluate it as either true or false. True will correspond to a match. - Java 8 has other types of functional interfaces:
- Function — it takes one argument and returns a result
- Consumer — it takes one argument and returns no result (represents a side effect)
- Supplier — it takes no arguments and returns a result
- Predicate — it takes one argument and returns a boolean
- BiFunction — it takes two arguments and returns a result
- BinaryOperator — it is similar to a BiFunction, taking two arguments and returning a result. The two arguments and the result are all of the same types
- UnaryOperator – it is similar to a Function, taking a single argument and returning a result of the same type
- Java 8 can infer the type for the lambda input parameters. Note that if you have to specify the parameter type, the declaration must be in brackets. This adds further verbosity.
- Groovy can println directly. No
System.out
is needed, and there is no need for subsequent braces. - Like Java, Groovy doesn't need the return statement. However, this isn't just for closures. In Groovy, it extends to every method. Whatever is evaluated as the last line is automatically returned.
- Groovy has no concept of a functional interface. This means that if you forget to ensure your last expression as an appropriate boolean expression, you get unexpected results and bugs at runtime.
- The arrow operator is used in both Groovy and Java to mean essentially the same thing, separating the parameter list from the body definition. In Groovy, it is only needed if you need to declare the parameters (the default it, doesn't suffice). Note: In Scala,
=>
is used.
Java
// Find all players with a rating over 8
List<RugbyPlayer> ratedPlayers = players.stream()
.filter(player -> player.getRating() >= 8)
.collect(Collectors.toList());
ratedPlayers.forEach(System.out::println);
Groovy
println players.findAll{it.rating >= 8}
Comments
- In the Java version, the iterable object
ratedPlayers
has itsforEach
method invoked. This method takes a functional interface of the consumer (see Jdoc here). Consumer methods are a function that takes an input parameter and returns nothing — it is void. - In Java, the
stream.filter()
will return another stream.Stream.collect()
is one of Java 8's stream terminal methods. It performs mutable fold operations on the data elements held inside the stream instances returned by the filter method. -
Collectors.toList ()
returns a Collector, which collects all stream elements into a list. - When using the
toList()
collector, you can't assume the type of list that will be used. If you want more control, you need to use thetoCollection()
. For example:.collect(toCollection(LinkedList::new)
- Note: We could have omitted the .collect() operation and invoked forEach straight on the stream. This would make the Java code shorter.
players.stream()
.filter(player -> player.getRating() >= 8)
.forEach(System.out::println);
System.out::println
is a method reference and is a new feature in Java 8. It is syntactic sugar to reduce the verbosity of some lambdas. This is essentially saying that for every element in ratedPlayers
, execute the System.out.println
, passing in the the current element as a parameter.
- Again, we get less syntax from Groovy. The function can operate on the collection, and there is no need to create a stream.
- We could have just printed the entire list in the Java sample, but heck I wanted to demo the
forEach
and method reference.
Map From Object Type to Another
Java
// Map the Rugby players to just names.
// Note, the way we convert the list to a stream and then back again to a to a list using the collect API.
System.out.println("Names only...");
List<String> playerNames = players.stream().map(player -> player.getName()).collect(Collectors.toList());
playerNames.forEach(System.out::println);
Groovy
println players.collect{it.name}
Comments
- A stream is needed to be created first before executing the Lambda. Then, the
collect()
method is invoked on the stream. This is needed to convert it back to a list. This also makes code more verbose. - That said if all you are doing is printing the list, you can just do:
players.stream()
.map(player -> player.getName())
.forEach(System.out::println);
Perform a Reduction Calculation
Java
System.out.println("Max player rating only...");
Optional<Integer> maxRatingOptional = players.stream()
.map(RugbyPlayer::getRating)
.reduce(Integer::max);
String maxRating = maxRatingOptional.isPresent() ? maxRatingOptional.get().toString() : "No max";
System.out.println("Max rating=" + maxRating);
Groovy
def here = players.inject(null){
max, it ->
it.rating > max?.rating ? it : max
}
Comments
- In the Java version, the reduced operation is invoked on the stream. There are three different versions of this method. In this version, no initial value is specified, meaning that an optional type is returned. The input parameter of type
BinaryOperator
is a functional interface that means a lamda expression or method reference can be used to specify its value. In this case, the method referenceInteger.max()
is used. - The null safe operator is used in the Groovy inject closure so that the first comparsion will work.
- In Java, it is possible to avoid the isPresent check on the optional by just doing...
players.stream()
.map(RugbyPlayer::getRating())
.reduce(Integer::max)
.map(Object::toString())
.orElse("No Max");
Summary
- Groovy is still far more terse.
- However, some of the operations in Java are lazily run. For example,
map()
andfilter()
are considered intermediate. They won't execute unless a terminal function, e.g. forEach, collects and reduces on the stream. This made the code more verbose in some cases, but it also means that it can be more performant. - Groovy also offers some lazy functions.
The full Java code can be found here. And, the full Groovy code can be found here.
If you enjoyed this article and want to learn more about Java Streams, check out this collection of tutorials and articles on all things Java Streams.
Published at DZone with permission of Alex Staveley, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments