Analogias da API do Java 8 Stream no Kotlin

Analogias da API do Java 8 Stream no Kotlin

*1. Introdução *

O Java 8 introduziu o conceito de Streams na hierarquia de coleções. Isso permite um processamento muito poderoso de dados de uma maneira muito legível, utilizando alguns conceitos de programação funcional para fazer o processo funcionar.

Investigaremos como podemos obter a mesma funcionalidade usando os idiomas do Kotlin. Também veremos os recursos que não estão disponíveis em Java comum.

===* 2. Java vs. Kotlin *

*No Java 8, a nova API sofisticada pode ser usada apenas ao interagir com instâncias _java.util.stream.Stream_.*

O bom é que todas as coleções padrão - qualquer coisa que implemente java.util.Collection - têm um método específico stream () _ que pode produzir uma instância _Stream.

É importante lembrar que o Stream não é um Collection. Ele não implementa java.util.Collection e não implementa nenhuma das semânticas normais de Collections em Java. É mais semelhante a um único Iterator que é derivado de uma Collection e é usado para trabalhar com ele, executando operações em cada elemento que é visto.

*No Kotlin, todos os tipos de coleção já suportam essas operações* sem a necessidade de convertê-las primeiro. Uma conversão é necessária apenas se a semântica da coleção estiver incorreta. Por exemplo, um _Set_ possui elementos exclusivos, mas não é ordenado.

Um benefício disso é que não há necessidade de uma conversão inicial de um Collection em um Stream, _ e não há necessidade de uma conversão final de um _Stream de volta para uma coleção - usando as chamadas _collect () _.

Por exemplo, no Java 8, teríamos que escrever o seguinte:

someList
  .stream()
  .map()//some operations
  .collect(Collectors.toList());

O equivalente em Kotlin é muito simples:

someList
  .map()//some operations
*Além disso, o Java 8 _Streams_ também não é reutilizável.* Depois que o _Stream_ é consumido, ele não pode ser usado novamente.

Por exemplo, o seguinte não funcionará:

Stream<Integer> someIntegers = integers.stream();
someIntegers.forEach(...);
someIntegers.forEach(...);//an exception

Em Kotlin, o fato de que essas são apenas coleções normais significa que esse problema nunca surge. O estado intermediário pode ser atribuído a variáveis ​​e compartilhado rapidamente , e funciona como esperávamos.

*3. Sequências preguiçosas *

Uma das principais coisas sobre o Java 8 Streams é que eles são avaliados preguiçosamente. Isso significa que não será realizado mais trabalho do que o necessário.

Isso é especialmente útil se estivermos realizando operações potencialmente caras nos elementos do _Stream, _ ou possibilitar o trabalho com sequências infinitas.

Por exemplo, IntStream.generate produzirá um Stream potencialmente infinito de números inteiros. Se chamarmos _findFirst () _, obteremos o primeiro elemento e não executaremos um loop infinito.

*No Kotlin, as coleções são mais ansiosas do que preguiçosas* . A exceção aqui é _Sequence_, que avalia preguiçosamente.

Essa é uma distinção importante a ser observada, como mostra o exemplo a seguir:

val result = listOf(1, 2, 3, 4, 5)
  .map { n -> n * n }
  .filter { n -> n < 10 }
  .first()

A versão Kotlin disso executará cinco operações _map () _, cinco operações _filter () _ e extrairá o primeiro valor. A versão do Java 8 executará apenas um _map () _ e um _filter () _ porque, da perspectiva da última operação, nada mais é necessário.

*Todas as coleções no Kotlin podem ser convertidas em uma sequência lenta usando o método _asSequence () _* .

O uso de uma Sequence em vez de uma List no exemplo acima executa o mesmo número de operações que no Java 8.

*4. Operações Java Stream do Java 8 *

No Java 8, as operações Stream são divididas em duas categorias:

  • intermediário e *terminal

As operações intermediárias basicamente convertem um Stream em outro preguiçosamente - por exemplo, um Stream de todos os números inteiros em um Stream de todos os números pares.

As opções de terminal são a etapa final da cadeia de métodos Stream e acionam o processamento real.

Em Kotlin não existe tal distinção. Em vez disso,* essas são apenas funções que tomam a coleção como entrada e produzem uma nova saída. *

Observe que, se estivermos usando uma coleção ansiosa no Kotlin, essas operações serão avaliadas imediatamente, o que pode ser surpreendente quando comparado ao Java.* Se precisarmos que seja preguiçoso, lembre-se de converter primeiro em uma Sequence.

4.1 Operações Intermediárias

Quase todas as operações intermediárias da API Java 8 Streams têm equivalentes no Kotlin. Porém, essas não são operações intermediárias - exceto no caso da classe Sequence -, pois resultam em coleções totalmente preenchidas do processamento da coleção de entrada.

Dessas operações, existem várias que funcionam exatamente da mesma maneira - filter () _, _map () _, _flatMap () _, _distinct () _ e _sorted () _ - e algumas que funcionam da mesma maneira apenas com nomes diferentes - _limit () _ agora é _take e _skip () _ agora é _drop () _. Por exemplo:

val oddSquared = listOf(1, 2, 3, 4, 5)
  .filter { n -> n % 2 == 1 }//1, 3, 5
  .map { n -> n * n }//1, 9, 25
  .drop(1)//9, 25
  .take(1)//9

Isso retornará o valor único “9” - 3².

*Algumas dessas operações também possuem uma versão adicional - com o sufixo da palavra _ “Para” _* - que gera uma coleção fornecida em vez de produzir uma nova.

Isso pode ser útil para processar várias coleções de entrada na mesma coleção de saída, por exemplo:

val target = mutableList<Int>()
listOf(1, 2, 3, 4, 5)
  .filterTo(target) { n -> n % 2 == 0 }

Isto irá inserir os valores "2" e "4" na lista "alvo".

*A única operação que normalmente não possui substituição direta é _peek () _* - usada no Java 8 para iterar sobre as entradas no _Stream_ no meio de um pipeline de processamento sem interromper o fluxo.

Se estivermos usando uma Sequence lenta em vez de uma coleção ansiosa, há uma função onEach () _ que substitui diretamente a função _peek. Porém, isso existe apenas nessa classe e, portanto, precisamos estar cientes do tipo que estamos usando para que ela funcione.

*Existem também algumas variações adicionais nas operações intermediárias padrão que facilitam a vida* . Por exemplo, a operação _filter_ possui versões adicionais _filterNotNull () _, _filterIsInstance () _, _filterNot () _ e _filterIndexed () _.

Por exemplo:

listOf(1, 2, 3, 4, 5)
  .map { n -> n *(n + 1)/2 }
  .mapIndexed { (i, n) -> "Triangular number $i: $n" }

Isso produzirá os cinco primeiros números triangulares, na forma "Número triangular 3: 6"

Outra diferença importante está na maneira como a operação flatMap funciona. No Java 8, esta operação é necessária para retornar uma instância Stream, enquanto no Kotlin, ele pode retornar qualquer tipo de coleção. Isso facilita o trabalho.

Por exemplo:

val letters = listOf("This", "Is", "An", "Example")
  .flatMap { w -> w.toCharArray() }//Produces a List<Char>
  .filter { c -> Character.isUpperCase(c) }

No Java 8, a segunda linha precisaria ser agrupada em _Arrays.toStream () _ para que isso funcionasse.

====* 4.2 Operações de terminal *

*Todas as operações de terminal padrão da API Java 8 Streams têm substituições diretas no Kotlin, com a única exceção de _collect _.*

Alguns deles têm nomes diferentes:

  • _anyMatch () _ → _any () _

  • _allMatch () _ → _all () _

  • _noneMatch () _ → _none () _

Alguns deles têm variações adicionais para trabalhar com a forma como o Kotlin tem diferenças - existem first () _ e _firstOrNull () _, onde _first lança se a coleção estiver vazia, mas retorna um tipo não anulável.

O caso interessante é collect. O Java 8 usa isso para poder coletar todos os elementos Stream para alguma coleção usando uma estratégia fornecida.

Isso permite que um Collector arbitrário seja fornecido, que será fornecido com todos os elementos da coleção e produzirá uma saída de algum tipo. Eles são usados ​​na classe auxiliar Collectors, mas podemos escrever os nossos, se necessário.

*No Kotlin, existem substituições diretas para quase todos os coletores padrão disponíveis diretamente como membros no próprio objeto de coleta* - não há necessidade de uma etapa adicional com o coletor sendo fornecido.

A única exceção aqui é os métodos summarizingDouble _/ summaryizingInt / summaryizingLong_ - que produzem média, contagem, min, max e soma de uma só vez. Cada um deles pode ser produzido individualmente - embora isso obviamente tenha um custo mais alto.

Como alternativa, podemos gerenciá-lo usando um loop for-each e manipulá-lo manualmente, se necessário - é improvável que precisaremos de todos esses 5 valores ao mesmo tempo, portanto, precisamos apenas implementar os que são importantes.

*5. Operações adicionais no Kotlin *

O Kotlin adiciona algumas operações adicionais às coleções que não são possíveis no Java 8 sem implementá-las.

Alguns deles são simplesmente extensões às operações padrão, conforme descrito acima. Por exemplo, é possível executar todas as operações para que o resultado seja adicionado a uma coleção existente em vez de retornar uma nova coleção.

Em muitos casos, também é possível ter o lambda fornecido não apenas com o elemento em questão, mas também com o índice do elemento - para coleções ordenadas e, portanto, os índices fazem sentido.

Existem também algumas operações que tiram vantagem explícita da segurança nula do Kotlin - por exemplo; podemos executar um _filterNotNull () _ em um _List <String?> _ para retornar um _List <String> _, onde todos os nulos são removidos.

As operações adicionais reais que podem ser realizadas no Kotlin, mas não no Java 8 Streams, incluem:

  • zip () _ e _unzip () _ - são usados ​​para combinar duas coleções em uma sequência de pares e, inversamente, para converter uma coleção de pares em duas coleções *_associate - é usado para converter uma coleção em um mapa, fornecendo um lambda para converter cada entrada da coleção em um par de chave/valor no mapa resultante

Por exemplo:

val numbers = listOf(1, 2, 3)
val words = listOf("one", "two", "three")
numbers.zip(words)

Isso produz uma _List <Pair <Int, String >> _, com os valores _1 a "um", 2 a "dois" _ e _3 a "três" _.

val squares = listOf(1, 2, 3, 4,5)
  .associate { n -> n to n* n }

Isso produz um _Map <Int, Int> _, onde as chaves são os números de 1 a 5 e os valores são os quadrados desses valores.

*6. Resumo *

A maioria das operações de fluxo com as quais estamos acostumados no Java 8 são diretamente utilizáveis ​​no Kotlin nas classes Collection padrão, sem a necessidade de converter primeiro um Stream.

Além disso, o Kotlin adiciona mais flexibilidade à maneira como isso funciona, adicionando mais operações que podem ser usadas e mais variação nas operações existentes.

No entanto, Kotlin está ansioso por padrão, não preguiçoso. Isso pode causar trabalho adicional a ser executado se não tomarmos cuidado com os tipos de coleção que estão sendo usados.