[Pesquisar este blog]

sexta-feira, 11 de novembro de 2016

Coleções e Streams outra vez::Parte II

A API Streams foi uma das melhores adições do Java 8. Por meio dela podemos manipular as coleções de maneira muito flexível, simples e elegante, realizando operações em massa sobre seus elementos, mas com uso de construções funcionais. Neste segundo artigo da série sobre Coleções e Streams, concentramos nossa atenção nas operações intermediárias.

Revisão Rápida

Um stream representa uma sequência de elementos sobre os quais uma ou mais operações podem ser executadas. Para utilizar um stream é necessário organizar um stream pipeline, como o ilustrado na figura anterior, que se inicia com uma fonte (ou origem) dos dados; uma sequência de zero ou mais operações intermediárias; e uma operação terminal (ou final).

Além disso, os streams se diferenciam das coleções pelo fato de serem consumíveis, ou seja, só podem ser utilizados uma única vez por uma operação intermediária ou terminal. Sua reutilização provoca a exceção IllegalStateException.

Nos fragmentos de código que seguem, o comentário inicial indica o nome arquivo-fonte Java onde estão contidos, permitindo que sejam testados com maior facilidade. Todos exemplos estão disponíveis no GitHub (projeto pjandl/stream_again).

No fragmento de código que segue foi construído um stream pipeline com três estágios que, resumidamente, filtra os nomes da coleção iniciados com "K", contando a quantidade de ocorrências, mas mantendo a coleção nomes inalterada:
// StreamFragm101.java
long cont = nomes.stream() // obtém stream fonte
 .filter(n -> n.startsWith("K"))// filtragem
 .count(); // contagem


No primeiro, nomes é uma coleção de objetos String (por exemplo, List<String>), cujo método stream() é a  operação fonte que cria um stream e inicia o pipeline. O segundo estágio se constitui pelo método filter(Predicate), uma operação intermediária (que toma um stream como entrada, resultando em outro), cuja expressão lambda fornecida determina o predicado da filtragem, isto é, o critério de aceitação dos elementos do stream de entrada que serão enviados para o stream de saída. No último estágio temos a operação terminal count() (que toma um stream como entrada e produz um tipo não encadeável) cujo resultado long não permite a continuação do encadeamento do stream pipeline.

Neste artigo serão detalhadas um pouco mais as operações de tipo intermediária, lembrando que este material foi dividido em quatro artigos:


Operações Intermediárias

As operações intermediárias transformam um stream em outro, isto é, resultam um outro stream modificado, o que possibilita o encadeamento de operações, ou seja, tomando um stream como partida, uma operação intermediária produz um outro stream, ao qual pode ser aplicada outra operação intermediária e assim por diante, possibilitando o encadeamento desejado pelo stream pipeline.

As operações intermediárias (que retornam um outro stream) possíveis são:
  • filtragem (filtering),
  • ordenação (sorting),
  • mutação (mutation) e
  • mapeamento (maping).


Filtragem

A filtragem é uma operação intermediária que seleciona elementos do stream baseado num critério (i.e., em um predicado). A filtragem não modifica o tipo de elemento contido no stream.

Filtragem
Stream<T>filter (Predicate<T> p)
Retorna um stream consistindo dos elementos deste stream que atendem o predicado p dado.
Stream<T>distinct ( )
Retorna um stream consistindo dos elementos distintos deste stream (diferentes segundo Object.equals(Object)).
Stream<T>limit (long n)
Retorna um stream consistindo dos n primeiros elementos deste stream.
Stream<T>skip (long n)
Retorna um stream consistindo elementos restantes deste stream após o descarte dos n primeiros elementos.

Com uma coleção de Double definida na variável colecao é possível filtrar elementos que atendam um critério específico, separando uma amostra de tamanho determinado:
// StreamFrag201.java
Stream<Double> stream =
    colecao.stream()              // obtém stream fonte
.filter(e -> e >= 5.0) // filtra valores >= 5
.limit(3);             // ret. stream c/ (até) 3 elem.

Aqui a stream resultante proverá acesso para até três elementos maiores do que 5.0 dentre aqueles contidos na coleção. Uma construção equivalente, sem uso de streams, seria algo como:
List<Double> subcolecao = new ArrayList<>();
int tam = 3;
for(Double e:colecao) {
if (e>=5.0) {
subcolecao.add(e);
tam--;
if (tam==0) break;
}
}

Este código não é complexo, mas é maior e é menos direto que a construção em pipeline.

Outro exemplo é a obtenção da stream de caracteres de uma String, tratados como valores inteiros, que podem ser filtrados para obtenção dos elementos distintos. O método count() é uma operação terminal, de redução (que será abordada no próximo post) que conta a quantidade de elementos de um stream retornando um valor long.
// StreamFrag202.java
// obtém stream de char (valores int) de String
IntStream charList = string.chars();
// conta caracteres distintos
long count = charList.distinct().count();
System.out.println("Caracteres distintos: " + count);

Ordenação

É outra operação intermediária que possibilita classificar os elementos do stream segundo um critério. A ordenação não altera o tipo de elemento contido no stream.

Ordenação
Stream<T>sorted ( )
Retorna um stream contendo os elementos deste stream classificados em ordem natural.
Stream<T>sorted (Comparator comparator)
Retorna um stream contendo os elementos deste stream classificados conforme o comparator fornecido.

Com a mesma coleção de Double definida anteriormente, podemos criar um stream pipeline de cinco estágios, onde a ordenação é um deles:
// StreamFrag203.java
Stream<Double> stream =
  colecao.stream()              // obtém stream fonte
.filter(e -> e < 6.0)  // filtra valores < 6.0
.sorted()              // ordena elementos
           .skip(1);              // descarta primeiro
.limit(5);             // ret. stream com (até) 5 elem.

Nesta situação a stream resultante proverá acesso aos cinco menores elementos, descartado o primeiro, da stream ordenada resultada da filtragem da stream original para valores menores que 6.0 existentes na coleção.

Mutação

Permite alterar os elementos do stream, conforme a função indicada, embora o tipo do elemento não seja alterado.
Mutação
Stream<T>peek (Consumer<> action)
Retorna um stream contendo os elementos deste stream, executando a ação provida em cada elemento.

Um exemplo de uso da operação intermediária de mutação seria a obtenção de um stream com os quadrados do elementos contidos na coleção:
Stream<Double> pipe1 = colecao.stream()
.peek(e -> Math.pow(e, 2));

Poderia ser obtida um stream com os quadrado maiores que 10:
Stream<Double> pipe2 = colecao.stream()
.peek(e -> Math.pow(e, 2))
.filter(e -> e > 10);

Várias operações intermediárias podem ser encadeadas, em qualquer ordem, assim podemos criar um stream com os quadrados dos elementos menores do que 1:
Stream<Double> pipe3 = colecao.stream()
.filter(e -> e < 1)
.peek(e -> Math.pow(e, 2));

Os streams resultantes (pipe1pipe2 e pipe3) podem ser utilizados para outras operações intermediárias ou uma operação terminal.


Mapeamento

O mapeamento é uma operação intermediária mais sofisticada, que transforma os elementos do stream (considerandos como uma entrada do tipo T), com base em uma função de mapeamento  (ou de transformação), e retorna um stream contendo elementos do tipo R, ou seja, esta é uma operação pode alterar o tipo dos elementos contidos no stream.

A função de mapeamento é responsável por tomar, individualmente, elementos do tipo T transformando-os em elementos do tipo R. É bastante comum que a função de mapeamento não altere os elementos de entrada, produzindo novos objetos com os dados ou características desejadas.

Mapeamento
Stream<R>flatMap (Function<T, Stream<R>> mapper)
Retorna um stream consistindo dos resultados da substituição de cada elemento deste stream com o conteúdo do stream de mapeamento produzido pela aplicação da função de mapeamento a cada elemento.
DoubleStreamflatMapToDouble (ToDoubleFunction<T> mapper)
Retorna um DoubleStream consistindo dos resultados da substituição de cada elemento deste stream com o conteúdo do stream de mapeamento produzido pela aplicação da função de mapeamento a cada elemento.
IntStreamflatMapToInt (ToIntFunction<T> mapper)
Retorna um IntStream consistindo dos resultados da substituição de cada elemento deste stream com o conteúdo do stream de mapeamento produzido pela aplicação da função de mapeamento a cada elemento.
LongStreamflatMapToLong (ToLongFunction<T> mapper)
Retorna um LongStream consistindo dos resultados da substituição de cada elemento deste stream com o conteúdo do stream de mapeamento produzido pela aplicação da função de mapeamento a cada elemento.
Stream<R>map (Function<T, R> mapper)
Retorna um stream consistindo dos resultados da aplicação da função a cada elemento deste stream.
DoubleStreammapToDouble (ToDoubleFunction<T> mapper)
Retorna um stream especializado no tipo primitivo double consistindo dos resultados da aplicação da função a cada elemento deste stream.
IntStreammapToInt (ToIntFunction<T> mapper)
Retorna um stream especializado no tipo primitivo int consistindo dos resultados da aplicação da função a cada elemento deste stream.
LongStreammapToLong (ToLongFunction<T> mapper)
Retorna um stream especializado no tipo primitivo long consistindo dos resultados da aplicação da função a cada elemento deste stream.


Considere o método estático que segue criado para ser um gerador de coleções de String. Embora retorne uma coleção de tipo indefinido, internamente este método instancia um ArrayList de String contendo uma centena de objetos String no formato CCDc, ou seja, dois caracteres maiúsculos, seguidos de um dígito e um caractere minúsculo.
// Generator.java
public class Generator {
    public static int SIZE = 100;
    public static Collection<String> newStringCollection() {
        List<String> stringList = new ArrayList<>();
        char[] charArray = new char[4];
        for (int i = 0; i < SIZE; i++) {
            charArray[0] = (char) (65 + (int) (Math.random() * 26));
            charArray[1] = (char) (65 + (int) (Math.random() * 26));
            charArray[2] = (char) (48 + (int) (Math.random() * 10));
            charArray[3] = (char) (97 + (int) (Math.random() * 26));
            stringList.add(new String(charArray));
        }
        return stringList;
    }
}


A partir deste stream é possível a realização de várias operações intermediárias e terminais.

Uma nova coleção pode criada com:
// StreamFrag203.java
Collection<String> colecao = Generator.newStringCollection();

A quantidade de objetos desta coleção pode ser controlada por meio da definição de um outro valor para o campo estático SIZE.
// StreamFrag203.java
Generator.SIZE = 20;
Collection<String> colecao = Generator.newStringCollection();

No trecho que segue, o stream obtido é filtrado para separar os elementos iniciados por 'J'. Uma operação de mapeamento conectada ao pipeline transforma os elementos, inserindo do sufixo "ava" após o 'J' inicial. Um último estágio para prover a ordenação do stream é incluso no pipeline.
// StreamFrag204.java
Stream<String> stream = colecao.stream()
  .filter(e -> e.charAt(0)=='J')
  .map(e -> e.substring(0, 1) + "ava" + e.substring(1))
  .sorted();


Assim o uso de operações intermediárias permite processar os streams, transformando-os conforme desejado e produzindo novos streams que podem ser utilizados por zero, uma ou mais operações intermediárias; e zero ou uma operação terminal. As operações terminais é assunto do próximo post desta série!


Este artigo faz parte de uma pequena série Coleções e Streams outra vez:

Para saber mais

quarta-feira, 9 de novembro de 2016

Java 9::mais um atraso no lançamento

O lançamento do Java 9 foi adiado uma outra vez. Com essa notícia, um tanto frustante para a enorme comunidade Java, vem um questionamento bastante natural: por quê o prolongamento da fase final de desenvolvimento da nova versão?

Segundo o arquiteto-chefe da plataforma Java na Oracle, Mark Reinhold, o time de desenvolvimento não está onde deveria em relação o cronograma, pois o Jigsaw precisa mais tempo. Além disso também observou que o número de problemas relatados no JDK 9 é maior que a quantidade existente no JDK 8 quando se encontrava em fase idêntica do desenvolvimento. A partir destas declarações, o atraso se tornou oficial:em 18/10 a data de entrega da versão 9 passou de 27/03/2017 para 27/07/2017. Quatro longos meses de atraso, que se somam aos outros seis meses da data original que era 22/09/2016.

A notícia gerou muitos comentários na comunidade e na imprensa especializada (InfoWorld, 404TS, The Register, Jaxenter, eWeek, ADTmag, Takipi). Nada muito diferente do que temos aqui, o que é pouco animador. 

Jigsaw se refere ao novo mecanismo de modularização da API, característica que é carro chefe da versão. Seu objetivo é possibilitar a divisão do runtime em componentes interoperáveis que possibilitem a criação de versões otimizadas para atender necessidade específicas. Atualmente a estrutura do Java Runtime Enviroment é praticamente monolítica, sendo que o arquivo rt.jar contém quase 20.000 classes, muitas delas usadas em poucos programas e poucas usadas por quase todos. Assim, seu tamanho torna-se um peso para a JVM e, com isso, compromete o desempenho das aplicações Java. Ao particionar a API, cada programa Java poderá carregar apenos os módulos necessários, o que trará um melhor desempenho. Além disso, aplicações poderão ser construídas de maneira idêntica, ou seja, com estrutura modular, estendendo tal flexibilidade aos programas construídos com a plataforma.

Apesar da importância do projeto Jigsaw, este atraso é bastante desanimador e, de certa maneira, lança uma incerteza quanto ao cumprimento das novas datas, pois que já errou mais de uma vez, pode continuar errando. Por outro lado, a busca de qualidade ao invés de velocidade, com mero cumprimento de prazo, é algo visto como positivo pelos desenvolvedores em geral. A esperança, então, é que esta data seja cumprida.

Mesmo com tudo isso, a plataforma Java continua liderando a preferência dos programadores. No TIOBE Index de novembro/2016 o Java figura como #1 (18.755%), C como #2 (9,203%), C++ é #3 (5.415%) e C# o #4 (3.659%). Observe que o percentual exibido pelo Java é maior que a soma de C, C++ e C#. Isso diz muita coisa!

O Novo Plano


Com o adiamento da data final de lançamento, todo o cronograma da versão 9 foi alterado, na verdade prolongado, conforme a página do projeto. No momento temos:

  • 2016/12/22 Feature Extension Complete
  • 2017/01/05 Rampdown Start
  • 2017/02/09 All Tests Run
  • 2017/02/16 Zero Bug Bounce
  • 2017/03/16 Rampdown Phase 2
  • 2017/07/06 Final Release Candidate
  • 2017/07/27 General Availability
Assim, apenas em dezembro deste ano teremos a confirmação de todas as características inclusas na versão 9, ponto em que se congela a lista de características da plataforma, sem possibilidade de novas inclusões (até esta data existe até um risco do Jigsaw ser removido da versão 9 e empurrado para a 10, o que já ocorreu no passado!). A fase de testes que segue pretende produzir um candidato a primeira versão oficial do Java 9 no início de julho/2017. Com tudo isso, contando com a inspiração do time de desenvolvimento, no final de julho/2017 deverá ocorrer o General Avaliability, ou seja, quando o JDK 9 é liberado para produção. Maktub!

Para os Curiosos, Ansiosos & Afoitos

As versões preliminares do Java 9 podem ser experimentadas. Basta efetuar o download de uma versão early access no link indicado para JDK 9 Early Access Releases na seção Para Saber Mais. É só conferir!

Para saber mais