Recentemente, foi lançada a versão 8 do Java, com mudanças significativas na linguagem.
Mas qual é a razão pra tanta algazarra na Internet?
Com Java 8, é possível escrever código com menos cerimônia e mais essência.
Filtrando com Classes Anônimas no Java 7 ou anterior
No post anterior, vimos como melhorar nosso código utilizando o molde Strategy e classes anônimas.
Abstraímos, em uma classe chamada FiltroDeDesenhos
, as operações de percorrer a lista, chamar a comparação e acumular os elementos uma nova lista. Já o contrato da lógica de comparação foi definido em uma interface chamada ComparacaoDeDesenhos
.
O código resultante, que utiliza a modelagem acima para filtrar uma lista de desenhos, ficou assim:
FiltroDeDesenhos filtro = new FiltroDeDesenhos(); List<Desenho> antesDe1960 = filtro.filtra(desenhos, new ComparacaoDeDesenhos() { public boolean valePara(Desenho desenho) { return desenho.getDecadaDeCriacao() < 1960; } }); List<Desenho> comecamComS = filtro.filtra(desenhos, new ComparacaoDeDesenhos() { public boolean valePara(Desenho desenho) { return desenho.getNome().startsWith("S"); } });
Focando no essencial com Lambdas do Java 8
As linhas destacadas no código acima são o realmente essencial: definem a lógica de filtragem. O resto todo faz parte da sintaxe assustadora de classes anônimas do Java.
A partir do Java 8, é possível simplificarmos nosso código com o uso de lambdas:
List<Desenho> antesDe1960 = filtro.filtra(desenhos, (Desenho desenho) -> { return desenho.getDecadaDeCriacao() < 1960; } ); List<Desenho> comecamComS = filtro.filtra(desenhos, (Desenho desenho) -> { return desenho.getNome().startsWith("S"); } );
Lambdas são métodos anônimos que podem ser usados no lugar de classes anônimas, com uma sintaxe bem mais enxuta. Internamente, são implementadas com um mecanismo diferente das classes anônimas. Estão presentes na maioria das linguagens e são uma ótima nova adição à linguagem Java.
Só foi possível utilizar lambdas no lugar de classes anônimas no nosso caso porque nossa interface ComparacaoDeDesenhos é uma interface funcional: possui apenas um método abstrato.
Veja:
interface ComparacaoDeDesenhos { boolean valePara(Desenho desenho); }
É possível marcar nossa interface com a anotação @FunctionalInterface
, porém não é algo obrigatório. Essa anotação é usada pelo compilador para lançar um erro assim que um segundo método abstrato for declarado.
Podemos ainda omitir as chaves que definem o bloco e o return
, já que o nosso lambda possui apenas uma expressão. Podemos também omitir a classe do parâmetro, por causa do mecanismo de inferência de tipos do Java 8, além dos parênteses:
List antesDe1960 = filtro.filtra(desenhos, desenho -> desenho.getDecadaDeCriacao() < 1960 ); List comecamComS = filtro.filtra(desenhos, desenho -> desenho.getNome().startsWith("S") );
Evitando código desnecessário com Streams do Java 8
Podemos ir além e remover a nossa classe FiltroDeDesenhos
e a interface ComparacaoDeDesenhos
, utilizando um Stream, um novo recurso do Java 8:
Stream<Desenho> streamDeDesenhos = desenhos.stream(); Stream<Desenho> streamDosAntesDe1960 = streamDeDesenhos .filter(desenho -> desenho.getDecadaDeCriacao() < 1960); Stream<Desenho> streamDosQueComecamComS = streamDeDesenhos .filter(desenho -> desenho.getNome().startsWith("S"));
Criamos um Stream
a partir da nossa lista de desenhos invocando o método stream
.
Um Stream
é uma sequência de elementos que permitem a execução de várias operações. Uma delas é o filter
, que recebe um lambda que tem a comparação e retorna um outro Stream
que contém apenas os elementos em que a condição da comparação é válida.
O lambda (ou classe anônima) recebido pelo filter
deve respeitar o contrato definido pela interface funcional Predicate
. Essa interface recebe um objeto e retorna um booleano, de maneira parecida com a nossa interface ComparacaoDeDesenhos
, mas mais genérica.
Mas o que podemos fazer com o Stream
dos elementos filtrados? Podemos transformá-lo numa lista com .collect(Collectors.toList())
ou imprimir os elementos com:
streamDosAntesDe1960 .forEach(System.out::println); streamDosQueComecamComS .forEach(System.out::println);
Invocamos o método forEach
passando um referência ao método println
de System.out. Isso é equivalente ao lambda desenho -> System.out.println(desenho)
.
Juntando tudo
Pegando o código acima, explicitando a criação da lista e omitindo os streams intermediários, temos:
class ProgramaComStreams { public static void main(String[] args) { Desenho popeye = new Desenho("Popeye", 1920); Desenho picaPau = new Desenho("Pica-pau", 1940); Desenho flintstones = new Desenho("Flintstones", 1960); Desenho scoobyDoo = new Desenho("Scooby-Doo", 1970); Desenho simpsons = new Desenho("Simpsons", 1990); List<Desenho> desenhos = Arrays.asList(popeye, picaPau, flintstones, scoobyDoo, simpsons); desenhos.stream() .filter( desenho -> desenho.getDecadaDeCriacao() < 1960 ) .forEach(System.out::println); System.out.println("---------------"); desenhos.stream() .filter( desenho -> desenho.getNome().startsWith("S") ) .forEach(System.out::println); } }
Após executarmos o código acima, teríamos a seguinte saída:
Popeye (1920) Pica-pau (1940) --------------- Scooby-Doo (1970) Simpsons (1990)
Comparando as implementações
Focando na filtragem dos desenhos anteriores a 1960, vamos comparar as implementações.
Primeiro, vamos lembrar da primeira versão, sem Strategy nem classes anônimas:
List<Desenho> antesDe1960 = new ArrayList<>(); for (Desenho desenho : desenhos) { if(desenho.getDecadaDeCriacao() < 1960){ antesDe1960.add(desenho); } } for (Desenho desenho : antesDe1960) { System.out.println(desenho); }
Agora, o código que cria uma implementação anônima da interface ComparacaoDeDesenhos
:
List<Desenho> antesDe1960 = filtro.filtra(desenhos, new ComparacaoDeDesenhos() { public boolean valePara(Desenho desenho) { return desenho.getDecadaDeCriacao() < 1960; } }); for (Desenho desenho : antesDe1960) { System.out.println(desenho); }
Então, usando lambas, temos uma sintaxe mais sucinta para definir a lógica de comparação:
List antesDe1960 = filtro.filtra(desenhos, desenho -> desenho.getDecadaDeCriacao() < 1960 ); for (Desenho desenho : antesDe1960) { System.out.println(desenho); }
Finalmente, usando streams, deixamos de lado classes desnecessárias, além de imprimir os desenhos de maneira concisa:
desenhos.stream() .filter( desenho -> desenho.getDecadaDeCriacao() < 1960 ) .forEach(System.out::println);
Compare os códigos e observe que, com lambdas e streams, a cerimônia do Java foi radicalmente diminuída. Podemos focar na essência da nossa tarefa: a lógica de filtragem.
Puxa… Achei confuso. E agora?
Realmente, utilizar lambdas, streams e as outras mudanças do Java 8 não é algo fácil. É preciso um bocado de estudo e de prática para entender e saber quando usar esses novos recursos.
Aqui, os conceitos foram vistos de maneira bem superficial. É importante estudá-los de maneira aprofundada. Há ainda muito mais coisas interessantes no Java 8: métodos default e estáticos em interfaces, optionals, streams paralelos, a nova API de datas, etc.
O livro Java 8 Prático, da Casa do Código é uma ótima referência. As novidades do Java 8 são estudadas com detalhes. Tive o prazer de participar da revisão técnica e recomendo! Os autores do livro também escreveram um post em que passeiam pelas funcionalidades mais importantes dessa nova versão do Java.
Não deixe de baixar a JDK 8.
O código desse post pode ser encontrado em: https://gist.github.com/alexandreaquiles/9649033