Desenvolvimento

4 dez, 2018

Streams e Lambdas em Java

816 visualizações
Publicidade

A minha primeira pergunta sobre streams no Stackoverflow foi há exatos dois anos e seis meses atrás. O IntelliJ possui um recurso que sugere trechos de código que podem ser convertidos para streams e lambdas e os converte sozinho.

Lembro que na época fiquei muito confusa sobre um trecho específico (como usar lambdas e streams?). O recurso chegou em Java há anos atrás em Java 8 e a sintaxe ainda causa estranheza. Eis alguns exemplos de como utilizar!

Filters

Imagine que temos uma lista de nomes e queremos encontrar todos aqueles que começam com a letra J.

List<String> names = Arrays.asList("John", "Jack", "Hamilton", "George");
for (String name : names) {
	if (name.startsWith("J")) {
		System.out.println(name);
	}
}

Os filters são exatamente a regra da busca.

List<String> result = names.stream()
				.filter(name -> name.startsWith("J"))
				.collect(Collectors.toList());

result.forEach(element -> System.out.println(element));

Note que, como filters retornam streams, consequentemente podemos ter filter logo depois de outro filter. Veja também que com Collectors conseguimos transformar a stream em uma lista de Strings que obedecem a regra do nome começar com J. Ou seja, a saída é:

John
Jack

Podemos também alterar a saída com Collectors:

String result = names.stream()
			.filter(name -> name.startsWith("J"))
			.collect(Collectors.joining(", "));
System.out.println(result);                

Saída

John, Jack

Veja mais exemplos de Collectors no Javadoc.

Method reference

Na lista de strings que tivemos como resultado, temos uma expressão lambda apenas para chamar o método:

result.forEach(element -> System.out.println(element));

Agora Java já deduz que obviamente queremos repassar cada elemento para o método println().

result.forEach(System.out::println);

Optionals

Optionals surgiram para evitar nullPointerExceptions e antes de tentar obter algo, podemos validar se realmente existe.

Ainda para a lista de nomes, vamos tentar encontrar qualquer nome que comece com W.

List<String> names = Arrays.asList("John", "Jack", "Hamilton", "George");
Optional<String> result = names.stream()
				.filter(name -> name.startsWith("W"))
				.findAny();

if (!result.isPresent()) {
	System.out.println("Not found");
}

Podemos reescrever esse trecho também lançando direto uma exceção, caso o nome não exista:

names.stream()
	.filter(name -> name.startsWith("W"))
	.findAny()
	.orElseThrow(IllegalStateException::new);

Map e reduce

Note que cada “operação executada” na stream é sequencial, e a ordem importa. Isto é, por exemplo, colocar um filter antes de um findAny é diferente de um findAny primeiro e depois o filter.

Um exemplo disso é como obter todas as pessoas que possuem mais de 20 anos e para estas alterar o nome para upperCase (caixa alta):

List<Person> people = Arrays.asList(new Person("John", 26),
				new Person("Jack", 40),
				new Person("Hamilton", 14),
				new Person("George", 63));

people.stream()
	.filter(p -> p.getAge() > 20)
	.map(p -> p.getName().toUpperCase())
	.forEach(System.out::println);

A função map é a responsável por “transformar” a saída.

Saída

JOHN
JACK
GEORGE

Para todos aqueles que possuem idade maior que 20, podemos obter essas idades através do map:

people.stream()
.filter(p -> p.getAge() > 20)
.mapToInt(Person::getAge)
.forEach(System.out::println);

Note que o processo consiste nas fases:

  • Buscar pessoas com mais de 20 anos
  • Para cada uma pessoa encontrada na busca, obter a idade

Através do reduce podemos fazer operações com cada resultado da stream. Por exemplo, somar todas as idades maiores que 20.

OptionalInt result = people.stream()
				.filter(p -> p.getAge() > 20)
				.mapToInt(Person::getAge)
				.reduce((a, b) -> a + b);

if (result.isPresent()) {
    System.out.println(result.getAsInt());
}

O método reduce obtém a idade de John, soma com a idade de Jack, e por fim, soma com a idade de George.

Note que a maioria das funções mais comuns já estão implementadas como obter a média, obter o valor mínimo/máximo, etc.

Por exemplo, temos que o trecho anterior é equivalente a:

int result = people.stream()
                    .filter(p -> p.getAge() > 20)
                    .mapToInt(Person::getAge)
                    .sum();

System.out.println(result);

Saída

129

Boas práticas

Esses conceitos implementados em Java surgiram na programação funcional há muito tempo. As ideias centrais da programação funcional são os conceitos de pureza, idempotência e side effect.

Resumidamente, temos que:

  • A operação executada para determinar o valor retornado pelo método deve ser baseada apenas nas entradas e não em atributos/variáveis externos.
  • Além disso, o método não deve alterar o valor de atributos (atribuindo algum outro valor)
  • Para cada chamada ao método (repassando os mesmos parâmetros) devemos obter sempre o mesmo valor.

Para entender mais sobre conceitos de programação funcional com exemplos, veja aqui.

Note que Java nos obriga escrever expressões entre { } somente em casos em que há mais de uma linha. Exemplo:

people.stream()
		.filter(p -> p.getAge() > 20)
		.forEach(p -> {
			System.out.println(p.getAge());
			System.out.println(p.getName());
		});

Trechos de código que usam lambdas e possuem várias linhas dificilmente não estão violando os conceitos de programação funcional. Portanto, evite.

Além disso, uma boa prática para facilitar a leitura é sempre quebrar em uma nova linha o pipeline das streams, ao invés de tudo em uma só linha.

Portanto, ao invés de:

people.stream().filter(p -> p.getAge() > 20).forEach(System.out::println);

Escreva:

people.stream()
	.filter(p -> p.getAge() > 20)
	.forEach(System.out::println);

Off topic

Apesar de Java ser uma linguagem estática, lambdas em Java foi possível graças a Duck Typing. Isto é, em tempo de execução é inferido qual é sua classe através de InvokeDynamic.

Referências