Novidades do Java 8: Lambda Expressions

For the english version, click here.

Saudações!

Depois de alguns meses afastado do blog, vamos tirar o “cheiro de mofo” com estilo :). Percebi que um dos meus artigos anteriores sobre a nova Date/Time API do Java 8 teve muitos acessos, então por isso vou dedicar esse post a uma outra novidade da nova versão do Java: Lambda Expressions.

Functional Programming

Lambda Expressions foi a maneira escolhida para que a linguagem Java finalmente possua “nuances” de programação funcional.
A definição de “programação funcional” é um pouco controversa. Não há muito consenso. O wikipedia nos diz o seguinte:

“Em ciência da computação, programação funcional é um paradigma de programação que trata a computação como uma avaliação de funções matemáticas e que evita estados ou dados mutáveis. Ela enfatiza a aplicação de funções, em contraste da programação imperativa, que enfatiza mudanças no estado do programa.”

Em resumo, utilizar lambda expressions vai permitir passar comportamentos, funções, como argumentos em uma chamada de método. É um paradigma realmente um pouco diferente do que programadores java estão acostumados, que desde então só escrevem métodos que aceitam objetos como parâmetro, e não outros métodos!

A plataforma Java na verdade chegou um pouco atrasada nessa festa. Outras linguagens como Scala, C#, Python e até Javascript já fazem isso há tempos. Há quem diga que apesar de ser possível “fazer mais com menos”, o uso de lambdas compromete a legibilidade do código. Esse é inclusive uma das alegações mais utilizadas por quem não concordava com a adição de lambdas na linguagem Java. Seguindo esse raciocínio, o próprio Martin Fowler disse certa vez:

“Qualquer tolo consegue escrever código para um computador entender. Bons programadores escrevem código que humanos consigam entender.”

Controvérsias à parte, existe pelo menos um ótimo argumento a favor das expressões lambda: Paralelismo. Com a proliferação de CPUs multicore, escrever código que possa facilmente se beneficiar de processamento concorrente é uma obrigação. Em Java por exemplo, não havia uma maneira simples de escrever código para percorrer grandes coleções de objetos em paralelo. E como veremos mais adiante, usando Streams vai ser possível fazer exatamente isso.

Lambdas vs Anonymous Inner Classes (Classes Anônimas)

Para quem já não consegue mais se conter de emoção, aqui vai o primeiro gostinho. O uso “clássico” de lambdas vai se dar em lugares onde normalmente você optaria por classes anônimas. Até porque, pense bem, é exatamente nesses lugares onde o que você realmente desejaria fazer seria passar um “comportamento”, e não um “estado” (objeto).

Como exemplo, vou usar a API Swing que a maioria de vocês já deve ter utilizado. Na verdade, casos como esse são praticamente idênticos em qualquer API de interface onde é preciso processar eventos do usuário: JavaFX, Apache Wicket, GWT, etc etc.

Em Swing, se você quer que uma determinada ação ocorra quando o usuário clica em um botão, você faria algo mais ou menos assim:

swing-action-listener3

Usando uma classe anônima, como mostra o exemplo acima, é uma das maneiras preferidas dos nossos colegas de profissão para escrever processamento de eventos. Mas percebam que nesse caso o nosso real desejo era simplesmente passar um comportamento, a ação do botão, para o método addActionListener(). E ao invés disso o que nós fizemos foi mandar um objeto (estado) como argumento, um ActionListener “anônimo”.

Como ficaria exatamente a mesma coisa usando lambda? Assim:

ActionListener usando lambda

ActionListener usando lambda

Como já havia mencionado, nós fazemos “mais com menos”. Como agumento do método addActionListener() agora temos somente a ação que realmente queríamos executar, somente o comportamento. Sem todo aquele blábláblá de usar uma clase anônima. Os detalhes da sintaxe iremos explorar depois, mas a expressão lambda nesse código se resume a:

(event) -> System.out.println("Clicou no botão 2");

Eu sei eu sei. Alguém agora deve estar pensando:
“Péra lá! Programo em Swing desde o primeiro episódio de ‘Caverna do Dragão’, e nunca vi a ação de um botão conter somente uma única instrução, como esse System.out! Ta pensando que sou besta?!”

É possível sim escrever uma lambda expression com n instruções. Claro que quanto maior for o código, menor é o custo-benefício se olharmos somente a “economia de código”:

Múltiplas instruções

Múltiplas instruções

Ainda faço parte daqueles que acha que mesmo nesse caso, com várias instruções, o código fica bem fácil de ler sem toda aquela poluição visual das classes anônimas. Se desconsiderarmos a identação, a sintaxe exige somente acrescentar chaves para delimitar o bloco, “{}”, e cada instrução claro tem o seu próprio “;”. Resumindo:

(event) -> {System.out.println("Primeira"); System.out.println("Segunda");}

EDIT: Um colega no fórum do GUJ me chamou a atenção para uma maneira bem mais prática e legível de proceder nesses casos onde as ações do botão vão compreender várias instruções, uma alternativa àquela identação incomum que vimos acima:

public class MyFrame extends Frame {

    public MyFrame() {
        //criar o botão
        JButton button5 = new JButton("Botão 5");

        //"clicarBotao()" é um método privado nessa mesma classe
        button5.addActionListener(e -> clicarBotao(e));

        //etc etc etc
    }

    private void clicarBotao(ActionEvent event) {
        //ações do botão aqui dentro
    }
}

Ai está. O mesmo efeito sem toda a verbosidade de usar classes anônimas/internas. Simples.


@FunctionalInterface

Para escrever uma lambda expression, tudo começa com as chamadas “functional interfaces”. Uma “functional interface” é uma interface java que possui úm único método abstrato. Lembrem-se disso, “um único abstrato”. Isto porque, agora no Java 8, é possível declarar métodos com implementações concretas em interfaces, ou seja, métodos não abstratos. Os famigerados default methods além também de métodos estáticos.

Para fins de considerar uma interface como sendo “funcional”, esses métodos default e os estáticos não contam. A interface pode ter 10 métodos, mas se 9 deles forem default e somente 1 for abstrato, ela vai ser uma interface funcional. De brinde, existe ainda uma annotation que ajuda a identificar essas interfaces, @FunctionalInterface. O uso dessa annotation não é obrigatório, a exemplo da annotation @Override. Mas ela é muito útil sim para fins de legibilidade principalmente. Usem-a.
(obs: a interface ActionListener utilizada no nosso exemplo anterior se encaixa nesses critérios).

Vamos criar um exemplo simples a fim de reforçar a sintaxe das expressões lambda. Imaginemos que você quer criar uma API, uma classe, que funcione como uma calculadora de dois operadores do tipo Double. Isto é, uma classe que possua métodos para somar, subtrair, dividir e etc, dois objetos do tipo Double:

public class Calculadora {
    public static Double somar(Double a, Double b) {
        return a + b;
    }
	
    public static Double subtrair(Double a, Double b) {
        return a - b;
    }
	
    public static Double multiplicar(Double a, Double b) {
        return a * b;
    }
	
    //etc etc etc...
}

Para utilizar essa calculadora “da NASA”, os clientes dessa API iam simplesmente invocar qualquer dos métodos estáticos:

Double resultado = Calculadora.somar(200, 100); //300

Entretanto, essa abordagem tem alguns problemas. Ter que programar na classe Calculadora todas as possíveis operações que se pode fazer com dois Doubles é impossível. Logo logo os clientes iram querer operações menos comuns, como porcentagem de um operador com relação ao outro, etc etc etc. E você, dono dessa API, iria ficar escravo dela pra sempre.

Seria ótimo se nossa API fosse genérica o suficiente para possibilitar que o próprio cliente informe qual operação ele quer executar em cima dos dois operadores. Para atingir esse objetivo, vamos primeiro criar uma interface funcional chamada OperadorDouble:

@FunctionalInterface
public interface OperadorDouble {

    public Double aplicar(Double a, Double b);

}

Nossa interface define um contrato para operações com dois objetos do tipo Double que retornem também um Double. O que exatamente será feito com esses dois parâmetros não cabe a nós decidir.

A classe Calculadora agora teria um único método, recebendo os dois operadores e uma lambda expression que vai permitir definir a operação a ser executada:

public class Calculadora {

    public static Double calcular(Double op1, Double op2, OperadorDouble operador) {
        return operador.aplicar(op1, op2); //delegar para o operador
    }

}

E é dessa forma que os clientes iriam invocar nossa nova API:

//somar 
Double result1 = Calculadora.calcular(30d, 70d, (a, b) -> a + b);
System.out.println(result1); //100.0
		
//subtrair
Double result2 = Calculadora.calcular(200d, 50d, (a, b) -> a - b);
System.out.println(result2); // 150.0
		
//multiplicar
Double result3 = Calculadora.calcular(5d, 5d, (a, b) -> a * b);
System.out.println(result3); // 25.0
		
//encontrar o menor dos dois operadores (usando operador ternário)
Double result4 = Calculadora.calcular(666d, 777d, (a, b) -> a > b ? b : a);
System.out.println(result4); //666.0	

O céu agora é o limite. O cliente pode invocar o método calcular() com qualquer ideia maluca que der na telha. Basta passar uma expressão lambda válida.

Um lambda tem duas partes separadas pelo caracter ‘->’. A seção da esquerda é somente a declaração dos parâmetros. A da direita, é como se fosse a implementação do método em si:

lambda1

Note que a seção esquerda atua somente como a declaração dos parâmetros. O método OperadorDouble.aplicar(Double a, Double b) tem dois parâmetros, então precisamos criar um lambda que bata com essa assinatura. O tipo dos parâmetros é inferido e na maioria das vezes não precisa ser informado, assim como o nome dos parâmetros pode ser o que bem entendermos, não necessariamente ‘a’ e ‘b’ como está declarado na nossa interface funcional:

//somar informando os tipos
Double result1 = Calculadora.calcular(30d, 70d, (Double x, Double y) -> x + y);		
		
//outra maneira
OperadorDouble operador = (Double op1, Double op2) ->  op1 + op2;
Double result2 = Calculadora.calcular(30d, 70d, operador);


Quando a assinatura da interface funcional não possuir nenhum parâmetro, é só usar ‘()’ sem nada dentro. Usando a interface Runnable podemos reproduzir esse caso:

/* Variável r pode ser passada como argumento em qualquer método
 * que aceite um Runnable */
Runnable r = () -> System.out.println("Lambda sem parametros");

A título de curiosidade somente, vou mostrar aqui uma sintaxe que também pode ser usada em qualquer lugar considerado válido para uma lambda expression normal, conhecida como Method Reference. Não vou entrar em detalhes sobre ela pois tenho intenção de terminar de escrever esse artigo ainda esse século, então olhem o link para mais detalhes. Resumindo, é uma maneira mais clean de escrever a expressão quando só o que interessa é chamar um método qualquer:

JButton button4 = new JButton("Botão 4!");
		
//isso aqui
button4.addActionListener(ActionEvent::getSource);	
		
//é equivalente a
button4.addActionListener((event) -> event.getSource());	


Não Reinvente a Roda

Antes de prosseguir só uma pequena pausa para relembrar esse velho jargão que todo mundo conhece. Isto porque na API do Java 8 já são fornecidas várias interfaces funcionais que você pode vir a precisar no seu dia-a-dia. Inclusive uma que atende perfeitamente a nossa necessidade do exemplo da calculadora de Doubles.

Essas interfaces encontram-se no pacote java.util.function e as principais são:

Nome Parâmetros Retorno Exemplo
BinaryOperator (T, T) T Fazer qualquer tipo de operação em dois objetos do mesmo tipo.
Consumer T void Imprimir um valor.
Function T R Receber um objeto do tipo Double e retorna sua representação em String.
Predicate T boolean Fazer um teste qualquer no objeto recebido como parâmetro: umaString.endsWith(“sufixo”)
Supplier T Operação que não receba nenhum parâmetro mas tenha um retorno.

Todas as outras presentes são variações dessas que citei acima. Logo a seguir quando formos tratar de Streams vamos ter a oportunidade de ver a maioria dessas em ação, e vai ficar mais fácil entender onde cada uma se encaixa. Já podemos entretanto refatorar a nossa calculadora para usar a interface BinaryOperator, e eliminar a nossa OperadorDouble:

public class Calculadora {

    public static <T> T calcular(T op1, T op2, BinaryOperator<T> operador) {
        return operador.apply(op1, op2);
    }

}

Nos clientes pouca coisa iria mudar, exceto o fato de que como a interface BinaryOperator usa tipos parametrizados, Generics, nossa calculadora ficou ainda mais flexível e podemos executar operações em objetos de qualquer tipo, não somente em Doubles:

//somar inteiros
Integer result1 = Calculadora.calcular(5, 5, (x, y) -> x + y);


Collections & Streams

Como desenvolvedores nós passamos a maior do tempo na verdade utilizando API’s de terceiros, e não fazendo as nossas próprias API’s. E até agora nesse artigo foi isso que focamos: como criar nossas próprias API’s usando lambdas.

Entretanto chegou a hora de analisarmos as alterações que foram feitas na API Java padrão para permitir o uso de lambdas na manipulação de coleções. Para ilustrar nossos exemplos vamos usar inicialmente uma classe Pessoa, que possui nome, idade, e sexo (“M” para masculino e “F” para feminino):

public class Pessoa {
	
    private String nome;
    private Integer idade;
    private String sexo; //M ou F
	
    //gets e sets
}

Os exemplos vão envolver manipulação de coleções, então imaginem também que teremos uma coleção de objetos Pessoa:

List<Pessoa> pessoas = umMetodoQueRetornaPessoas(); 

A brincadeira começa pelo novo método, stream() que foi acrescentado a interface Collection. Uma vez que Collection é a interface “pai” de todas as coleções, todas elas herdam então esse novo método:

List<Pessoa> pessoas = umMetodoQueRetornaPessoas();
Stream<Pessoa> stream = pessoas.stream(); //criando um stream de Pessoa

Apesar de no primeiro momento parecer que a interface Stream é apenas mais um tipo de coleção, não é bem esse o caso. Um Stream é uma abstração de um “fluxo de dados”, para permitir exclusivamente manipulações, transformações, em cima desses dados. Diferente das coleções que conhecemos, um Stream não permite por exemplo que você acesse diretamente seus elementos (para isso tem que transformar o Stream em uma Collection novamente).

Para efeito de comparação vamos olhar como seria o código se nós tivéssemos que contar o número de pessoas do sexo “F” (feminino) na nossa coleção de pessoas. Primeiro, sem usar streams:

long count = 0;
List<Pessoa> pessoas = umMetodoQualquerQueRetornaPessoas();
for (Pessoa p : pessoas) {
    if (p.getSexo().equals("F")) {
        count++; //mais um
    }
}

Usando um laço, criamos um contador que é incrementado a cada vez que uma pessoa é do sexo “F”. Código desse tipo todos nós já fizemos centenas de vezes.

A mesma coisa agora usando um stream:

List<Pessoa> pessoas = umMetodoQualquerQueRetornaPessoas();
long count = pessoas.stream().filter(pessoa -> pessoa.getSexo().equals("F")).count();

Bem mais limpo, não? Tudo começa no método stream(), e em seguida as outras chamadas são feitas de maneira encadeada pois maior parte dos métodos da interface Stream suportam o padrão Builder, então as chamadas encadeadas são possíveis. Pra quem nunca viu, talvez fique mais fácil visualizar assim, sem encadear todas as chamadas:

List<Pessoa> pessoas = umMetodoQualquerQueRetornaPessoas();
Stream<Pessoa> stream = pessoas.stream();
stream = stream.filter(pessoa -> pessoa.getSexo().equals("F"));
long count = stream.count();

Vamos focar nos dois métodos da interface Stream que nós usamos, o filter() e o count().

O método filter() aceita a condição que será aplicada para filtrar a coleção. E essa condição é representada por uma expressão lambda que recebe um parâmetro e retorna um boolean:

pessoa -> pessoa.getSexo().equals("F")

Não por acaso, a interface funcional usada para representar essa expressão, que é o parâmetro do método filter(), é a interface Predicate. Ela expõe um único método abstrato, boolean test(T t):

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    //outros métodos não abstratos aqui
}

O tipo parametrizado T vai representar o tipo do elemento do nosso stream, ou seja, objetos Pessoa. Então é como se nossa expressão lambda representasse a implementação do método test() dessa forma:

boolean test(Pessoa pessoa) {
    if (pessoa.getSexo().equals("F")) {
        return true;
    } else {
        return false;
    }
}

Depois de aplicada a filtragem é só chamar o método count(). Não tem mistério, ele simplesmente conta quantos elementos existem no nosso stream depois de aplicada todas as manipulações que nós desejamos (nesse caso foi só a filtragem, mas poderiam ser várias). O método count() é considerado uma operação “terminal” e depois que ele é executado aquela stream é considerada “consumida” e não poderá mais ser utilizada.

A seguir vamos ver exemplos de outros métodos da interface Stream.

collect()

O método collect() é usado para efetuar uma mutable reduction em um stream (leia no link para detalhes). Normalmente isso vai significar transformar um stream de volta em uma coleção. Notar que assim como o método count() o collect() também é uma operação terminal!

Vamos supor uma pequena variação do exemplo anterior, onde há uma coleção de objetos Pessoa onde se deseja filtrar pelo sexo feminino. A diferença é que dessa vez nós não vamos filtrar as pessoas do sexo feminino (filter()) e depois contar quantas são (count()), e sim separar os objetos encontrados em uma coleção separada, que vai conter somente pessoas do sexo feminino:

List<Pessoa> pessoas = umMetodoQualquerQueRetornaPessoas();
		
//transformando em um List
List<Pessoa> listPessoas = pessoas.stream()
        .filter(p -> p.getSexo().equals("F"))
        .collect(Collectors.toList());
		
//transformando em um Set
Set<Pessoa> setPessoas = pessoas.stream()
        .filter(p -> p.getSexo().equals("F"))
        .collect(Collectors.toSet());

A parte da filtragem permanece a mesma, o que muda é a chamada ao collect() no final. Como dá pra perceber ele na verdade recebe um parâmetro, um objeto do tipo Collector.

Criar um objeto Collector dá um pouco de trabalho, então felizmente existe uma classe que de maneira mais conveniente fornece os objetos do tipo Collector mais comuns. A classe Collectors (no plural), vide as duas chamadas Collectors.toList() e Collectors.toSet() acima. Mais alguns exemplos interessantes:

//É possível escolher o tipo específico da coleção
//que será usada na transformação, com o método Collectors.toCollection()!
		
//mais uma maneira de criar um Stream
Stream<String> myStream = Stream.of("a", "b", "c", "d");		
		
//transformar em um LinkedList (usando a notação method reference)
LinkedList<String> linkedList = myStream.collect(Collectors.toCollection(LinkedList::new));
				
//transformar em um TreeSet
Stream<String> s1 = Stream.of("a", "b", "c", "d");
TreeSet<String> t1 = s1.collect(Collectors.toCollection( () -> new TreeSet<String>() ));
		
//usando method referente, a mesma coisa acima ficaria assim
Stream<String> s2 = Stream.of("a", "b", "c", "d");
TreeSet<String> t2 = s2.collect(Collectors.toCollection( TreeSet::new ));

Observem que o método Collectors.toCollection() aceita uma lambda expression como parâmetro, do tipo funcional Supplier.

A interface funcional Supplier disponibiliza o método abstrato T get(), que não recebe nenhum parâmetro e retorna um objeto. Por isso que a expressão que nós passamos foi simplesmente
a chamada ao construtor da coleção que queríamos usar:

() -> new TreeSet<String>()


map()

O map() é até bastante intuitivo. Ele deve ser usado nos casos onde você quer transformar cada elemento de uma coleção em algum outro objeto, ou seja, mapear elementos de uma coleção para um outro tipo de elemento.

Complementando o exemplo anterior da coleção de pessoas, vamos tentar o seguinte cenário: Dada a coleção de pessoas, obter uma coleção separada da inicial, que contenha somente o nome em letras maiúsculas das pessoas do sexo feminino. Resumindo, além de usar filter() e collect() para separar todos as pessoas do sexo feminino em uma lista, vamos ter que usar o map() para transformar cada objeto pessoa em uma String que represente o nome somente em letras maiúsculas:

Esboço da transformação usando map()

Esboço da transformação usando map()

E o código:

List<Pessoa> pessoas = umMetodoQualquerQueRetornaPessoas();
		
List<String> nomes = pessoas.stream()
        .filter(p -> p.getSexo().equals("F"))
        .map(p -> p.getNome().toUpperCase())
        .collect(Collectors.toList());

A interface funcional usada como parâmetro no método map() é a Function, cujo único método abstrato, R apply(T t), recebe um objeto como parâmetro e retorna um outro objeto diferente. Exatamente o que o map() precisa: receber uma Pessoa e devolver uma String.

forEach() & forEachOrdered()

Talvez os mais simples de todos, forEach() e forEachOrdered podem ser usados para aplicar uma ação em cada elemento de um stream, por exemplo, imprimir no console cada elemento que for encontrado. A diferença entre os dois como o nome já diz é que um garante a “ordem de encontro dos elementos”, e o outro não.

Se o stream possui ou não uma ordem estabelecida vai depender da coleção que o deu origem, assim também como operações intermediárias que são efetuadas em cima dele. Streams criados a partir de um List possuem ordem definida, como era de se esperar.

A interface funcional da vez é a Consumer, cujo método abstrato void accept(T t) recebe um parâmetro e não retorna nada:

List<Pessoa> pessoas = umMetodoQualquerQueRetornaPessoas();
		
//imprimir sem garantia de "ordem de encontro"
pessoas.stream().forEach(p -> System.out.println(p.getNome()));
		
//imprime na ordem se for possível
pessoas.stream().forEachOrdered(p -> System.out.println(p.getNome()));

Lembrem-se, o forEach()/forEachOrdered() são também operações terminais! Depois delas, nenhuma outra operação pode ser efetuada no mesmo Stream. (não precisa decorar essas coisas, basta ler o javadoc de cada método em caso de dúvida)

min() & max()

Encontrar o mínimo e o máximo em um conjunto de elementos também ficou mais fácil com lambda expressions. É o tipo da rotina que apesar de simples, da maneira antiga seria bem chata de fazer.

Na nossa coleção de objetos Pessoa, vamos encontrar quais as duas pessoas que possuem a menor e maior idade, respectivamente:

List<Pessoa> pessoas = umMetodoQualquerQueRetornaPessoas();
		
//pessoa mais nova com min()
Optional<Pessoa> nova = pessoas.stream()
        .min((p1, p2) -> p1.getIdade().compareTo(p2.getIdade()));
		
//mais velha com max()
Optional<Pessoa> velha = pessoas.stream()
        .max((p1, p2) -> p1.getIdade().compareTo(p2.getIdade()));
		
//imprimir as idades no console
System.out.println(nova.get().getIdade());
System.out.println(velha.get().getIdade());

Os métodos min() e max() também aceitam uma interface funcional como parâmetro, só que essa não é novidade: Comparator. (obs: Se você está lendo esse artigo e não sabe o que é um “Comparator”, sugiro voltar alguns passos e entender mais o java básico antes de se aventurar com lambdas)

Tem mais uma novidade ali no código acima, o uso da classe Optional. Essa é uma idéia também introduzida no Java 8, mas como não é o foco do nosso artigo não vou entrar em detalhes. Caso não conheça é só dar uma pesquisada.

O mesmo objetivo poderia ser também alcançado usando o novo método estático Comparator.comparing(), que recebe uma Function e atua como um utilitário para criar comparators:

//min()
Optional<Pessoa> nova = pessoas.stream().min(Comparator.comparing(p -> p.getIdade()));
		
//max()
Optional<Pessoa> velha = pessoas.stream().max(Comparator.comparing(p -> p.getIdade()));


Um pouco mais sobre collect() e Collectors

Usando o método collect() é possível fazer ainda operações bem interessantes, usando mais alguns dos Collectors “pré-prontos”.

É possível por exemplo calcular a média de idade de todas as pessoas na nossa coleção:

List<Pessoa> pessoas = umMetodoQualquerQueRetornaPessoas();
		
Double media = pessoas.stream().collect(Collectors.averagingDouble(p -> p.getIdade()));
		
System.out.println("A média é: " + media);

Existem 3 métodos na classe Collectors que pode nos ajudar nesse sentido, cada um faz a média de um tipo de dado:

Todos esses métodos acima retornam um Collector válido que pode ser passado ao método collect().

Outra possibilidade interessante é poder dividir uma coleção, particionar, em duas coleções diferentes. Nós já fizemos algo parecido criando uma coleção separada somente de pessoas do sexo feminino, entretanto a nossa coleção original continuou com todos os registros misturados, homens e mulheres. Mas se nos quiséssemos dividir a coleção original em duas novas, uma só com homens e outra só com mulheres?

Para tal, usamos o Collectors.partitioningBy():

List<Pessoa> pessoas = umMetodoQualquerQueRetornaPessoas();
		
//um Map Boolean -> List<Pessoa>
Map<Boolean, List<Pessoa>> result = pessoas.stream()
        .collect(Collectors.partitioningBy(p -> p.getSexo().equals("M")));
		
//homens armazenados na chave 'true'
List<Pessoa> homens = result.get(Boolean.TRUE);
		
//mulheres na chave 'false'
List<Pessoa> mulheres = result.get(Boolean.FALSE);	

O Collectors.partitioningBy() demonstrado acima funciona criando um Map com dois elementos, um que possui a chave ‘true’ e outro a chave ‘false’. Uma vez que ele recebe como interface funcional o tipo Predicate, que retorna um boolean, o elemento cujo resultado da expressão for ‘true’ vai para a coleção de ‘true’, e os ‘false’ para a coleção de ‘false’.

Para finalizar, vamos imaginar só mais um caso onde nós pensássemos em agrupar todas as pessoas da nossa coleção por idade. É parecido com o Collectors.partitioningBy(), só que não vamos agrupar somente por duas condições “true/false”, e sim por uma condição determinada por nós: a idade.

Fácil como fritar um ovo, é só usar o Collectors.groupingBy():

//Map "Idade" -> "List<Pessoa>"
Map<Integer, List<Pessoa>> result = pessoas.stream()
        .collect(Collectors.groupingBy(p -> p.getIdade()));

Imaginem fazer isso ai sem usar lambdas, à moda antiga? Dá dor de cabeça só de pensar.

Performance e paralelismo

No começo do artigo eu mencionei que uma das vantagens de usar lambda expressions seria a possibilidade de manipular coleções de maneira paralela, e chegou a hora de mostrar aqui. Na verdade não há muito o que dizer, pois para transformar todos os exemplos que vimos até aqui de “sequencial” para “paralelo”, basta mudar uma única chamada:

List<Pessoa> pessoas = umMetodoQualquerQueRetornaPessoas();
		
//normal		
Stream<Pessoa> s1 = pessoas.stream();
		
//paralelo
Stream<Pessoa> s2 = pessoas.parallelStream();

Só isso. Basta mudar a chamada ao método stream() e substituir por parallelStream() e o processamento será feito em paralelo. Todas as outras chamadas podem permanecer da mesma forma.

Para demonstrar a diferença em executar ou não em paralelo, fiz um teste usando o último exemplo como base, onde agrupamos todas as pessoas da coleção por idade. Levando em consideração uma massa de dados de 20 milhões de objetos Pessoa, o resultado foi esse:

grafico

Sem usar lambda, como faríamos “à moda antiga”, ficou em empate técnico com uso do stream() normal. 18 e 16 segundos respectivamente. Com parallelStream() entretanto, esse tempo caiu para 4 segundos! Diferença de 300%!

ATENÇÃO: Isso NÃO significa de maneira alguma que se deva processar tudo em paralelo!

Além do motivo óbvio que esse meu teste foi simples demais para ser considerado cegamente, é importante levar em conta antes de optar pelo processamento em paralelo que existe um overhead inerente ao paralelismo: a coleção é decomposta em várias e depois mesclada novamente para formar o resultado final.

Isto posto, se não houver um número realmente grande de elementos o custo desse processamento em paralelo provavelmente não vai se pagar. Então analisem com muito cuidado antes de sair usando parallelStream() indiscriminadamente.

É isso! Não deu claro para cobrir tudo, ia ser necessário um livro. Mas muitos pontos importantes estão aqui, e já serve como ponto de partida. Dúvidas, críticas e sugestões deixem nos comentários.

Happy coding!

31 thoughts on “Novidades do Java 8: Lambda Expressions

  1. Muito legal o artigo. Estava me debatendo com a documentação da Oracle, mas esse artigo dá um start mais do que claro. Parabéns.

  2. Muito obrigado, aprendi muito com o seu artigo, você explica muito bem e tem uma didática excelente.

  3. Pingback: Novidades do Java 8: Lambda Expressions | Vinícius P. Freire

  4. Ótimo artigo!
    Moro na alemanha e estava precisando de uma base pra comecar a desenvolver routers em cluster distruidos usando akka framework.
    Me ajudou bastante e projeto quase concluido!
    Parabens cara!

  5. Ótimo trabalho este artigo, muito esclarecedor! Estava com dificuldades para entender o funcionamento deste operador lambda introduzido no Java 8, mas agora ficou bem claro. Abraços

    • Obrigado, Renato!
      Cara na epoca desse post eu pesquisei em varias fontes. Mas próximo mês já vai fazer 3 anos que fiz esse post, então imagino que a essa altura deva ter bastante livro por ai de Java 8 🙂 Infelizmente não sei nenhum. Talvez seja bom já comprar algo sobre Java 9 haha 🙂
      Abraço!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s