Groovy developers will have a head-start in adopting the concepts and new language constructs offered by Java 8. Many of the enhancements offered in the upcoming version of Java are features that Groovy has offered for years. From new syntax for functional programming styles, to lambdas, collection streaming, and method references as first class citizens, Groovy developers will have an edge when writing Java code in the future. This article will focus on the commonalities between Groovy and Java 8, and will demonstrate how familiar Groovy concepts translate to Java 8.
We'll begin by discussing functional programming styles, how we use functional programming today in Groovy, and how the constructs of Java 8 offer a better functional programming style.
Closures are perhaps the best example of functional programming in Groovy. Under the hood, a closure in Groovy is really just an implementation of a functional interface. A functional interface is any interface that has only a single method to implement. By default, Groovy closures are an implementation of the functional Callable interface, implementing the "call" method.
def closure = {
"called"
}
assert closure instanceof java.util.concurrent.Callable
assert closure() == "called"
We can make Groovy implement other functional interfaces by type casting a closure.
public interface Function {
def apply();
}
def closure = {
"applied"
} as Function
assert closure instanceof Function
assert closure.apply() == "applied"
Closures and functional programming translate well in Java 8. Functional interfaces are very important in the upcoming Java release because Java 8 offers implicit implementation of functional interfaces with the introduction of Lambda functions.
Lambda functions can be thought of, and used, in the same way as closures in Groovy. Implementing a callable interface in Java 8 offers similar simplicity to closures in Groovy.
Callable callable = () -> "called";
assert callable.call() == "called";
It's important to also note that single-line lambda functions in Java 8 offer implicit return statements, a concept shared with Groovy.
In the future, Groovy will also offer an implicit implementation of Single Abstract Methods for closures similar to those offered by Java 8. This feature gives closures the ability to leverage instance properties and methods without fully deriving a concrete subclass.
abstract class WebFlowScope {
private static final Map scopeMap = [:]
abstract def getAttribute(def name);
public def put(key, val) {
scopeMap[key] = val
getAttribute(key)
}
protected Map getScope() {
scopeMap
}
}
WebFlowScope closure = { name ->
"edited_${scope[name]}"
}
assert closure instanceof WebFlowScope
assert closure.put("attribute", "val") == "edited_val"
In Java 8, functional interfaces with interface default methods offer a near approximation of this same concept. Interface default methods are a new concept in Java. They were conceived to allow improvements to core APIs without violating contracts for implementations that were built on prior versions of Java.
Lambda functions will also have access to default methods of the interface to which they are coerced. This means that robust APIs can be built directly into an interface, giving features to application developers without changing the nature of the type, or the contract in which that type may be used.
public interface WebFlowScope {
static final Map scopeMap = new HashMap();
Object getAttribute(Object key);
default public Object put(Object key, Object val) {
scopeMap.put(key, val);
return getAttribute(key);
}
default Map getScope() {
return scopeMap;
}
}
static final WebFlowScope scope = (Object key) -> "edited_" + scope.getScope().get(key);
assert scope.put("attribute", "val") == "val";
Interface default methods in Java 8 can also help us implement features of Groovy such as memoization and trampolining. Memoization can be implemented simply by creating a functional interface with an interface default method to deterministically compute a result or retrieve the result from cache.
public interface MemoizedFunction<T, R> {
static final Map cache = new HashMap();
R calc(T t);
public default R apply(T t) {
if (!cache.containsKey(t)) {
cache.put(t, calc(t));
}
return (R)cache.get(t);
}
}
static final MemoizedFunction<Integer, Integer> fib = (Integer n) -> {
if (n == 0 || n == 1) return n;
return fib.apply(n - 1)+fib.apply(n-2); };
assert fib.apply(20) == 6765;
Similarly, we can utilize interface default methods to develop a Trampoline implementation in Java 8. Trampolining in Groovy is a strategy of recursion that won't overwhelm Java's call stack, and is a very useful feature of Groovy when deep recursion is necessary.
interface TrampolineFunction<T, R> {
R apply(T...obj);
public default Object trampoline(T...objs) {
Object result = apply(objs);
if (!(result instanceof TrampolineFunction)) {
return result;
} else {
return this;
}
}
}
// Wrap the call in a TrampolineFunction so that we can avoid StackOverflowError
static TrampolineFunction<Integer, Object> fibTrampoline = (Integer...objs) -> {
Integer n = objs[0];
Integer a = objs.length >= 2 ? objs[1] : 0;
Integer b = objs.length >= 3 ? objs[2] : 1;
if (n == 0) return a;
else return fibTrampoline.trampoline(n-1, b, a+b);
};
Beyond basic features of closures, and the more advanced features of Memoization and Trampolining, some of the most practical and useful features that Groovy has to offer are related to the language's extensions to the Collections API. In Groovy, we can leverage these extensions to shortcut writing operations against lists by using the 'each' method.
def list = [1, 2, 3, 4]
list.each { item ->
println item
}
Java 8 introduces a concept similar to Groovy for iterating a collection, making available a 'forEach' method, which replaces the conventional manner of iterating a list.
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.forEach( (Integer item) -> System.out.println(item); );
In addition to simplified list iteration, Groovy gives application developers a variety of other shortcuts when working with lists. The 'collect' method, for example, is the shorthand for mapping list elements to new types or values, and collecting the results into a new list.
def list = [1, 2, 3, 4]
def newList = list.collect { n -> n * 5 }
assert newList == [5, 10, 15, 20]
Groovy's implementation of 'collect' passes the mapper as an argument to the collect method, whilst Java 8 offers a slightly more verbose implementation. Using the Stream API, developers can accomplish the same mapping and collecting strategy by calling the 'map' method on the list's 'stream' component, and then calling the 'collect' method from the stream that is returned from the mapping step. The Stream API gives developers the ability to fluently chain operations against the list.
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
List<Integer> newList = list.stream().map((Integer n) -> n * 5).collect(Collectors.toList());
assert newList.get(0) == 5 && newList.get(1) == 10 && newList.get(2) == 15 && newList.get(3) == 20;
Groovy also gives developers shortcuts for filtering lists using the 'findAll' method.
def emails = ['danielpwoods@gmail.com', 'nemnesic@gmail.com', 'daniel.woods@objectpartners.com', 'nemnesic@nemnesic.com']
def gmails = emails.findAll { it.endsWith('@gmail.com') }
assert gmails = ['danielpwoods@gmail.com', 'nemnesic@gmail.com']
Similarly, Java 8 developers can filter a list with the Stream API.
List<String> emails = new ArrayList<>();
emails.add("danielpwoods@gmail.com");
emails.add("nemnesic@gmail.com");
emails.add("daniel.woods@objectpartners.com");
emails.add("nemnesic@nemnesic.com");
List<String> gmails = emails.stream().filter( (String email) -> email.endsWith("@gmail.com") ).collect(Collectors.toList());
assert gmails.get(0) == "danielpwoods@gmail.com" && gmails.get(1) == "nemnesic@gmail.com";
The Groovy Collections API extensions make it easy to sort lists by giving Collections a 'sort' method. The 'sort' method will also take a closure that will be cast to a comparator during list sorting if special sorting logic is required. Additionally, if simply reversing the order of a list is necessary, the 'reverse' method can be called and the order is swapped.
def list = [2, 3, 4, 1]
assert list.sort() == [1, 2, 3, 4]
assert list.sort { a, b -> a-b <=> b } == [1, 4, 3, 2]
assert list.reverse() == [2, 3, 4, 1]
Working again with Java 8's Stream API, we can sort a list using the 'sorted' method, and collect those results using the 'toList' Collector. The 'sorted' method will optionally take a functional argument (such as a Lambda function) as a comparator, so special sorting logic and reversing the list items are operations easily achieved.
List<Integer> list = new ArrayList<>();
list.add(2);
list.add(3);
list.add(4);
list.add(1);
list = list.stream().sorted().collect(Collectors.toList());
assert list.get(0) == 1 && list.get(3) == 4;
list = list.stream().sorted((Integer a, Integer b) -> Integer.valueOf(a- b).compareTo(b)).collect(Collectors.toList());
assert list.get(0) == 1 && list.get(1) == 4 && list.get(2) == 3 && list.get(3) == 2;
list = list.stream().sorted((Integer a, Integer b) -> b.compareTo(a)).collect(Collectors.toList());
assert list.get(0) == 2 && list.get(3) == 1;
When using fluent APIs, like list streaming, it can quickly become unmaintainable to try to handle all of the processing inside of a closure or Lambda function. It may make sense, in those cases, to delegate the processing to a method that is specifically suited for that unit of work.
In Groovy, we've been able to accomplish this by passing method references into functions. Once a method is referenced using the '.&' operator, it is coerced to a closure and can be passed to another method as an argument. Inherently, this affords flexibility in implementation, since the processing code can be introduced from external sources. Developers can now logically organize processing methods, and achieve a more maintainable and sustainable application architecture.
def modifier(String item) {
"edited_${item}"
}
def list = ['item1', 'item2', 'item3']
assert list.collect(this.&modifier) == ['edited_item1', 'edited_item2', 'edited_item3']
Developers in Java 8 will be afforded the same flexibilities by making use of the '::' operator to get a reference to a method.
List<String> strings = new ArrayList<>();
strings.add("item1");
strings.add("item2");
strings.add("item3");
strings = strings.stream().map(Helpers::modifier).collect(Collectors.toList());
assert "edited_item1".equals(strings.get(0));
assert "edited_item2".equals(strings.get(1));
assert "edited_item3".equals(strings.get(2));
Method references can be passed as arguments to any method that requires a functional interface. In turn, the method reference will take the form of the functional interface, and can be treated as such.
public interface MyFunctionalInterface {
boolean apply();
}
void caller(MyFunctionalInterface functionalInterface) {
assert functionalInterface.apply();
}
boolean myTrueMethod() {
return true;
}
caller(Streaming::myTrueMethod);
In Java 8, library developers can make changes to interface contracts without consumers having to update the way they interface with the library.
The seamless translation of concepts and programming styles from Groovy to Java 8 is an important bridge between the two languages. Groovy was adopted so heavily in the JVM space because of its inherent flexibilities and improvements to existing Java APIs. With a lot of these improvements taking root in Java 8, it means that the similarities between the two languages are beginning to outweigh the differences, a fact that this article intended to outline. To that end, experienced Groovy developers will have a much smaller learning curve when learning and adapting to the new APIs, features, and concepts that will be introduced to the Java ecosystem with Java 8.
About the Author
Daniel Woods is a Senior Consultant with Object Partners, Inc. He specializes in Application Architecture with Groovy and Grails, while keeping a strong interest in Java and other JVM-based languages. He is a contributor to open source, and will be presenting at this year's Gr8Conf and SpringOne 2GX conferences. Daniel can be reached via email at danielpwoods@gmail.com or through Twitter @danveloper.