Desenvolvedores Groovy terão mais facilidade para adotar os conceitos e novas construções da linguagem oferecida pelo Java 8. Muitas das melhorias oferecidas na próxima versão do Java são características que o Groovy oferece há anos. Desde uma nova sintaxe até estilos de programação funcional, lambdas, coleção de streams, e referências de métodos como cidadãos de primeira classe. Os desenvolvedores Groovy terão uma vantagem ao escrever o código Java no futuro. Este artigo focará nas semelhanças entre o Groovy e o Java 8, e vai demonstrar o quão familiar os conceitos do Groovy se traduzem para Java 8.
Começaremos discutindo estilos de programação, como atualmente utilizamos a programação funcional no Groovy, e de que forma as construções em Java 8 oferecem um estilo de programação melhor.
Closures são, talvez, o melhor exemplo de programação funcional em Groovy. Por baixo dos panos, uma closure em Groovy é apenas a implementação de uma interface funcional. Uma interface funcional é qualquer interface que possua apenas um único método para implementar. Por padrão, as closures do Groovy são uma implementação da interface funcional Callable, implementando o método "call".
def closure = { "called" } assert closure instanceof java.util.concurrent.Callable assert closure() == "called"
Podemos fazer o Groovy implementar outras interfaces funcionais por meio da conversão de tipos closures.
public interface Function { def apply(); } def closure = { "applied" } as Function assert closure instanceof Function assert closure.apply() == "applied"
Closures e programação funcional se traduzem bem em Java 8. Interfaces funcionais são muito importantes na próxima versão do Java, pois o Java 8 oferece implementação implícita de interfaces funcionais com a introdução das funções Lambda.
As funções lambda podem ser pensadas, e utilizadas como as closures em Groovy. Implementar uma interface funcional em Java 8 oferece simplicidade semelhante a das closures em Groovy.
Callable callable = () -> "called"; assert callable.call() == "called";
É importante notar também que as funções lambda de uma só linha oferecem retorno implícito, assim como no Groovy.
No futuro, o Groovy também oferecerá implementação implícita de Interfaces Funcionais para as closures, semelhante a oferecida pelo Java 8. Esta característica da as closures a capacidade de aproveitar as variáveis de instância e métodos sem derivar totalmente uma subclasse concreta.
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"
Em Java 8, as Interfaces Funcionais com métodos default na interface oferecem uma boa aproximação deste conceito. Métodos default em interfaces são algo novo em Java. Eles foram concebidos para permitir melhorias nas APIs principais do Java sem violar os contratos de implementações feitas em versões anteriores da linguagem.
As funções lambda também terão acesso a métodos padrão da interface em que são definidas. Isto significa que APIs robustas podem ser construídas diretamente em uma interface, dando recursos aos programadores sem alterar a natureza do tipo ou do contrato no qual o tipo pode ser utilizado.
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";
No Java 8, métodos default de interface podem também ajudar-nos a implementar características do Groovy como memoization e trampoline. Memoization pode ser implementada simplesmente criando-se uma interface funcional com um método de interface default para computar deterministicamente um resultado ou obter o resultado do 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;
De maneira similar, podemos utilizar métodos default de interface em Java 8 para desenvolver implementação Trampoline. Trampoline é uma estratégia de recursão que não sobrecarrega a pilha de chamadas do Java, e é um recurso muito útil do Groovy quando uma recursão profunda é necessária.
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; } } } // Encapsula a chamada do TrampolineFunction para evitar um 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); };
Além dos recursos básicos das closures e dos recursos mais avançados como Memoization e Trampolining, alguns dos recursos mais práticos e úteis que o Groovy tem para oferecer estão relacionados às extensões da linguagem para a API Collections. Em Groovy, podemos utilizar estas extensões para escrever atalhos e realizar operações em listas usando o método 'each'.
def list = [1, 2, 3, 4] list.each { item -> println item }
Java 8 introduz um conceito similar ao do Groovy no que diz respeito a iterar coleções, disponibilizando o método 'forEach', que substitui a maneira convencional de percorrer listas.
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); );
Além da iteração simplificada, o Groovy dá aos desenvolvedores uma variedade de outros atalhos quando se trabalha com listas. O método "collect", por exemplo, é a abreviação para mapear elementos de uma lista para novos tipos ou valores, coletando os resultados em uma nova lista.
def list = [1, 2, 3, 4] def newList = list.collect { n -> n * 5 } assert newList == [5, 10, 15, 20]
Na implementação do Groovy, o método 'collect' recebe um mapeamento como argumento, enquanto o Java 8 oferece uma implementação um pouco mais verbosa. Usando a API Stream, os desenvolvedores podem realizar a mesma estratégia de mapeamento e coleta chamando o método 'map' no componente 'stream' da lista, em seguida, chamar o método 'collect' a partir do stream que é retornado na etapa de mapeamento. A API Stream dá aos desenvolvedores a capacidade de efetuar facilmente operações em cadeia em listas.
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 também oferece aos desenvolvedores atalhos para filtrar listas através do método 'findAll'.
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']
De maneira semelhante, em Java 8 os desenvolvedores podem filtrar uma lista utilizando a API Stream.
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";
As extensões da API Collections do Groovy tornam fácil a ordenação de listas fornecendo à API um método 'sort'. O método 'sort' também utilizará uma closure que converte para um comparador durante a ordenação da lista se uma lógica de ordenação especial for necessária. Adicionalmente, se uma simples reversão da ordem da lista é necessária, o método 'reverse' pode ser chamado e a ordem invertida.
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]
Trabalhando novamente com a API de Stream do Java 8, podemos ordenar a lista usando o método 'sorted' e obter o resultado usando o método 'toList' do Collectors. O método 'sorted' pode receber opcionalmente uma função de comparação como argumento (tal como uma função Lambda), então uma lógica de ordenação especial e reversão dos itens da lista são operações facilmente realizadas.
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;
Quando usando APIs fluentes, como stream de lista, isso pode rapidamente ser insustentável para tratar todos os processamentos dentro de uma função closure ou Lambda. Isso faz sentido, em alguns casos, como delegar o processamento para um método que é adaptado especialmente para essa unidade de trabalho.
No Groovy, podemos efetuar isso passando a referência de um método dentro da função. Uma vez que um método é referenciado usando o operador '.&', ele é convertido para uma função closure e pode ser passado para outro método como argumento. Intrinsecamente, isso proporciona flexibilidade na implementação, uma vez que o código de processamento pode ser introduzido de fontes externas. Os desenvolvedores podem agora organizar logicamente o processamento dos métodos obtendo uma arquitetura de aplicação mais fácil de ser mantida e sustentável.
def modifier(String item) { "edited_${item}" } def list = ['item1', 'item2', 'item3'] assert list.collect(this.&modifier) == ['edited_item1', 'edited_item2', 'edited_item3']
Os desenvolvedores no Java 8 dispõem da mesma flexibilidade através do uso do operador '::' para obter uma referência do método.
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));
Os métodos de referência podem ser passados como argumentos para qualquer método que necessita de uma interface funcional. Por sua vez, os métodos de referência terão a forma de interfaces funcionais e podem ser tratadas como tal.
public interface MyFunctionalInterface { boolean apply(); } void caller(MyFunctionalInterface functionalInterface) { assert functionalInterface.apply(); } boolean myTrueMethod() { return true; } caller(Streaming::myTrueMethod);
No Java 8, os desenvolvedores de bibliotecas podem fazer mudanças nos contratos das interfaces sem que os consumidores tenham que atualizar a forma de interagir com as bibliotecas.
A tradução perfeita dos conceitos e estilos de programação do Groovy para o Java 8 é uma ponte importante entre duas linguagens. O Groovy foi adotado em grande escala no domínio da JVM por causa das flexibilidades de herança e melhorias nas APIs existentes do Java. Com muitas dessas melhorias enraizando no Java 8, isso significa que a similaridade entre as duas linguagens estão começando a superar as diferenças, um fato que este artigo pretende delinear. Para finalizar, os desenvolvedores que possuem experiência em Groovy terão uma curva de aprendizado pequena para aprender e adaptar as novas APIs, funcionalidades e conceitos introduzidos no ecossistema Java.
Sobre o autor
Daniel Woods é consultor sênior na Object Partners, Inc. Ele é especialista em Arquitetura de Aplicações com Groovy e Grails, mantendo forte interesse em Java e outras linguagens baseadas na JVM. Daniel pode ser contatado através do email danielpwoods@gmail.com ou pelo Twitter @danveloper.