Streams no Java 8 e em outras Linguagens 

Apr/15
15

Uma das novidades dos Java 8 é o conceito de Stream de objetos e sua conceptualização na interface Stream.

O conceito de Stream √© bastante simples. A ideia √© iterar um conjunto de objetos que est√£o presentes em alguma fonte e enquanto o elemento √© iterado v√°rias opera√ß√Ķes podem ser realizadas, como filtrar o elemento, modificar o elemento, transformar o elemento em outro elemento, etc.¬†O conceito de Stream √© uma extens√£o do conceito de Iterator, misturada com o conceito de Builder. A ideia √© que ao invocar os m√©todos o objeto vai memorizando o que precisa ser feito, como faria um builder, e apenas quando os elementos s√£o iterados √© que realmente olhamos os elementos na fonte e os passamos pelo fluxo de opera√ß√Ķes. Isto tem duas vantagens simples a) encapsula a invoca√ß√£o da¬†diretiva for-each e b) assegura que a fonte s√≥ √© iterada uma vez (for is evil).

O conceito de Stream não é novo. Ele advém do conceito de Monad e está presente em linguagens como Lisp, Scala e C# ( o famoso Linq, é baseado neste conceito).

A fonte de elementos √© normalmente uma cole√ß√£o da API de cole√ß√Ķes do java, mas n√£o necessariamente. A classe Random,por exemplo, permite acessar diferentes Streans de¬†n√ļmeros aleat√≥rios sem partir de uma cole√ß√£o original. Este exemplo, mostra tamb√©m, que uma Stream n√£o precisa ser finita. A itera√ß√£o dos elementos pode ser infinita.Para fontes finitas, a import√Ęncia do Stream ¬†est√° em que todas as transforma√ß√Ķes¬†s√£o¬†feitas em uma √ļnica itera√ß√£o e portanto todo o conjunto de opera√ß√Ķes executa em tempo proporcional ao numero de elementos na fonte.

O fato do stream esconder a invoca√ß√£o √† diretiva for-each tem outra vantagem que √† partida pode n√£o ser √≥bvia. Isto permite que a execu√ß√£o dos comandos n√£o seja sequencial o que abre as portas para introduzir mais performance quando a stream tem muitos elementos.¬†¬†Estas opera√ß√Ķes paralelas j√° eram poss√≠veis¬†desde o java 7 com o framework de fork-join, mas ele √© demasiado complexo para coisas simples. Logo, a ideia era prover essa funcionalidade encapsulada em algo mais intuitivo e “hands-off” de forma que a pr√≥pria API fa√ßa uso do framework automaticamente, seguindo o principio de design que uma API n√£o deve for√ßa o programador a fazer algo que ela sabe fazer sozinha.

A ideia de Stream é muito interessante, mas havia um problema. Para que este conceito vingasse em java era necessário que fosse possível obter uma stream de fontes comuns como listas e mapas. Seria possível incluir um conjunto de métodos estáticos do tipo que existem na classe Collections, mas isto pareceria estranho ao usar. Por outro lado, como a API é baseada no conceito de builder que vai concatenando comandos seria necessário criar classes para estes comandos. Isto poderia ser resolvido com classes internas (inner classes) , mas seria muito verboso. Uma das experiencias que fiz no MiddleHeaven foi exatamente esta de incluir uma API de stream, e a coisa mais chata era o uso de inner classes toda a hora. Funciona, mas não é bonito. Depois de um tempo descobri que isto gerava memory leak pois as inner classes não são estáticas e acabam guardando referencias aos objetos pai o que baralha o Garbage Collector.

Para resolver estes problemas (não só, mas também) duas novas capacidades da plataforma foram adicionadas : Default Methods ( Métodos Padrão), e lambda-expressions.

Express√Ķes lambda permitem escrever literais para Functors (Objetos que s√£o apenas fun√ß√Ķes, normalmente apenas uma fun√ß√£o. Estes objetos s√£o normalmente concebidos como interfaces que s√≥ t√™m um m√©todo). O pessoal da Oracle envolvido com lambdas gosta de chamar os Functors¬†de tipos SAM (Single Abstrat Method Types). N√£o apenas temos uma simplifica√ß√£o na sintaxa, mas a maquinaria interna da JVM e do compilador fazem com que o peso na performance destes tipos seja quase inexistente o que torna todo o mecanismo mais eficiente. Isto se deve ao mecanismo de lambdas usar o novo bytecode InvokeDynamic introduzido no Java 7.

Default Methods ( M√©todos Padr√£o) s√£o um novo mecanismo para interfaces que permite definir¬†m√©todos na interface e ao mesmo tempo definir uma implementa√ß√£o padr√£o para eles. Isto efetivamente transforma¬†as interfaces ¬†java em traits que s√£o usados em outras linguagens, como Scala. Um trait √© como uma interface e √© usado para estabelecer um contrato , mas al√©m disso, permite que j√° sejam definidos outros m√©todos n√£o abstratos com um certa implementa√ß√£o padr√£o. √Č uma fus√£o entre o conceito de interface e de classe abstrata no que diz respeito a abstra√ß√Ķes. Normalmente a implementa√ß√£o destes¬†m√©todos padr√£o √©¬†baseada em outros m√©todos da interface que s√£o completamente abstratos. Por exemplo, se¬†houvesse uma interface Countable como a seguir :


public interface Countable {

public int size();

public boolean isEmpty();

}

Em que a regra é que isEmpty retorna true se size retornar zero. Podemos , em java 8, escrever isto facilmente, e poupamos o implementador da interface de se preocupar com esse método


public interface Countable {

public int size();

public default boolean isEmpty() { // note a palavra default

return size() == 0;

}

}

Isto ainda permite ao implementador da classe sobreescrever o m√©todo como achar melhor, mas torna a sua implementa√ß√£o opcional na maioria dos casos. A API para o Java 8 recebeu bastantes novos m√©todos desenhados com esta funcionalidade e inclusive alguns m√©todos antigos como Iterator.remove que lan√ßa NotSupportedOperation por padr√£o…Uma nota sobre¬†novas coisas para interfaces¬†√© que agora tamb√©m √© possivel escrever m√©todos est√°ticos em interfaces. Isto √© muito √ļtil para desenhar melhores APIs.

Esta nova tecnologia de default methods foi usada para incluir o m√©todo¬†stream() em Collection e permitir que todas as cole√ß√Ķes sejam fontes para streams.

Como disse antes, o objetivo principal do Stream n√£o √© melhorar o Iterator, mas sim permitir opera√ß√Ķes paralelas visando aumentar a performance. Isto n√£o √© conseguido apenas com o uso do conceito de Stream.¬†Por ajuda nisto, o Java define um novo tipo de iterador : o Spliterator, que √© um Iterator que permite fazer split, ou seja produzir duas partes para iterar a partir de uma √ļnica fonte. Ele tamb√©m pode ser usado como um iterador normal caso tudo aconte√ßa em sequencia. Todas as Cole√ß√Ķes ganharam um m√©todo (um outro default method) chamado spliterator(). Em conjunto o spliterator e o stream permitem realizar tarefas antes chatas e repetitivas com pouco c√≥digo.

Se já programou em java antes do java 8 então provavelmente já teve que produzir código que filtra a transforma objetos de uma coleção em outros.  Eis um exemplo que pega um conjunto de clientes e produz objetos ClientView para enviar para a tela, mas apenas se o cliente está ativo.


List<Client> clients = ... // obtido de alguma forma

List<ClientView> views = new ArrayList<>(clients.size());

for (Client c : clients){

if (c.isActive()){

Clientview v = new  ClientView();

v.setName(c.getName());

v.setId(c.getId());

views.add(v);

}

}

Aqui eu coloquei tudo em um √ļnico for, para ser mais curto e eficiente, mas √© muito comum ver c√≥digo em que cada parte tem seu pr√≥prio for. Um para filtrar e um outro para converter. Em aplica√ß√Ķes reais as regras s√£o mais complexas e muitas mais transforma√ß√Ķes s√£o necess√°rias.

Com a nova API de stream o mesmo resultado pode ser alcançado com :


List<Client> clients = ... // obtido de alguma forma

List<ClientView> views = clients.stream().filter ( c -> c.isActive())
.map( c -> new  ClientView(c) ).collect(Collectors.toList());

Bem mais curto e fácil de entender. Agora é claro que existe um filtro pelo estado de ativo porque o método filter está sendo usado, e é claro que existe uma conversão porque o método map está sendo usado.  Obter uma versão de execução fork-join para o primeiro código seria complexo, mas para a nova versão com stream, basta apenas chamar um método diferente:


List&lt;Client&gt; clients = ... // obtido de alguma forma

List&lt;ClientView&gt; views = clients.parallelStream().filter ( c -> c.isActive())
.map( c -> new  ClientView(c) ).collect(Collectors.toList());

A API de paralelismo tenta ser o mais inteligente poss√≠vel considerando , quando poss√≠vel, o n√ļmero de elementos na fonte, e pode, inclusive decidir executar tudo em sequencia se isso for mais vantajoso.

Embora a API de Stream seja realmente artilhada para fornecer paralelismo de forma simples, isso implica que as opera√ß√Ķes da Stream em si, s√£o limitadas. Se compararmos a interface Stream do java com suas contrapartes em outras linguagens veremos que faltam algumas coisas. Por exemplo, √© poss√≠vel obter o primeiro item do stream (findFirst()) , mas n√£o o √ļltimo. Para reverter a stream ou para fazer uma opera√ß√£o de groupBy temos que usar um Collector, que quebra um pouco a fluidez da api. Em .NET , por exemplo, estas opera√ß√Ķes s√£o nativas da interface IEnumerable que tem o mesmo papel que Stream. Podemos facilmente invocar GroupBy ou Last. Esperemos que isto evolua em pr√≥ximas vers√Ķes da API, pois como est√° hoje realmente √© muito orientado a paralelismo, mas sem considerar opera√ß√Ķes mais comuns do dia a dia.

Claro que podemos argumentar que opera√ß√Ķes especiais como last e reverse t√™m que ser obtidas pelo correto uso da API de cole√ß√Ķes original (iterar em reverso,¬†por exemplo, pode ser feito usando um ListIterator, mas apenas para lista onde essa opera√ß√£o √© eficiente).

Embora os m√©todos presentes em Stream sejam limitados a introdu√ß√£o do conceito representa um salto gigante para a linguagem em termos de escrita e leitura de opera√ß√Ķes sobre conjuntos. Al√©m disso, tr√°s a linguagem para o mundo moderno a par com Scala e C#.

Criar uma API de Stream n√£o √© assim t√£o complexo – ¬†e j√° me referi a isto outras vezes (sim , eu acho que voc√™ deveria ter sua pr√≥pria API de Stream) – ¬†o complexo √© torn√°-la paraleliz√°vel e perform√°tica. Queremos que as opera√ß√Ķes sejam realizadas com uma s√≥ itera√ß√£o no elementos da fonte tanto quanto poss√≠vel e isto requer, algumas vezes, um pouco de imagina√ß√£o.

A introdu√ß√£o de Stream na API Java √© realmente um grande avan√ßo e dominar esta API √© t√£o importante hoje, quanto ontem era dominar a pr√≥pria API de cole√ß√Ķes. Espero que ela evolua em funcionalidade ao n√≠vel da API¬†presente em outras linguagens mas sempre ser√° uma boa alternativa para processamento paralelo de forma simples ( que j√° n√£o √© t√£o comum em outras linguagens). √Č realmente uma API que uma vez que voc√™ se habitua voc√™ n√£o quer fazer de jeito antigo.

 

3 comentários para “Streams no Java 8 e em outras Linguagens”

  1. √ďtimo post. Voc√™ j√° utilizou a API de Collection Goldman Sachs?

    https://github.com/goldmansachs/gs-collections

  2. N√£o conhecia. Mas √© bem curioso que voc√™ tenha referido isso, porque o assunto de fazer uma melhor api de collections √© o meu pr√≥ximo post ūüôā

    Vou analisar a lib e comento no próximo post que vem bem a propósito.

  3. […] Stream que permite realizar v√°rias opera√ß√Ķes enquanto a itera√ß√£o acontece. Falei disto no post passado. Uma nova API de cole√ß√Ķes tem que levar este conceito a s√©rio. Em Scala, por exemplo, n√£o […]

Comente

Enquete

Sobre quais assuntos gosta de ler neste blog ?

View Results

Loading ... Loading ...

Artigos