[Pesquisar este blog]

quarta-feira, 3 de janeiro de 2018

Java 9::Jigsaw, a nova modularidade da plataforma (Parte II)

Depois de mais de oito anos de trabalho, o projeto Jigsaw e o novo sistema de modularidade para a plataforma Java estreiam na versão 9, trazendo um grande conjunto de novas possibilidades para o desenvolvimento de software.
Desenvolvimento modular com Java 9
Desenvolvimento modular com Java 9
A parte I deste post abordou o conceito de modularidade e a especificação do novo artefato module trazida pelo Jigsaw.

Agora, na parte II, será discutida a implementação do Jigsaw na versão 9 do Java. Para finalizar este post, na parte III, temos um exemplo de construção de uma aplicação modular.

O JDK modularizado

Uma das queixas frequentes sobre a plataforma Java era o fato de sua API nativa ser constituída de um monólito de código gigantesco. Estamos falando do famigerado arquivo rt.jar (Java Runtime archive). Qualquer aplicação Java executada na versão 8 (ou anterior), requer que o rt.jar esteja disponível, pois nele residem todas as classes e interfaces padronizadas da máquina virtual. Como tal arquivo é grande, o trabalho da JVM para localizar e extrair qualquer tipo necessário para a aplicação em execução é, obviamente, mais demorado do que a execução destas mesmas tarefas em arquivos menores.

A versão do rt.jar localizada no subdiretório lib do JRE possuia 51,9MB no JRE 1.8.0_121. Já a versão armazenada no subdiretório jre\lib, do JRE incluso no JDK da mesma versão 1.8.1_121, tem 60.4MB. Ambos possuem mais de 19000 arquivos distintos.

Além da mencionada dificuldade no carregamento de tipos, o tamanho do rt.jar é proibitivo em aplicações destinadas a equipamentos que não sejam computadores, por exemplo, para dispositivos "vestíveis" (wearable devices) ou embutidos (embeded devices).

Outro problema, menos discutido, mas igualmente presente, é a inconveniente dependência cíclica, pouco intuitiva, existente entre alguns de seus elementos.

Como já visto, o ponto de partida do projeto Jigsaw foi a criação de um novo artefato module que, conforme a especificação da versão 9, é um arquivo JAR modular. Um arquivo JAR modular é um arquivo JAR que possui um descritor de módulo, module-info.class, no diretório raiz do arquivo JAR. Tal descritor é a forma binária da declaração de um módulo.

Uma das maiores tarefas do projeto Jigsaw foi dividir o arquivo rt.jar em um conjunto adequado de módulos, menores, interdependentes, mas sem dependências circulares. O resultado pode ser visto na figura que segue (que precisa ser ampliada - basta clicar - para que seus detalhes sejam observados).

Hierarquia dos módulos no Java 9

Na parte inferior da figura podemos ver o módulo java.base, o único que não depende de outros - que possui arestas de chegada. Todo e qualquer módulo criado lê (ou depende de) java.base, implícita ou explicitamente, de maneira similar a importação do pacote java.lang.

O módulo java.base exporta pacotes como java.lang, java.util, java.math etc., que são praticamente onipresentes em qualquer aplicação Java.

Os novos módulos são como novos componentes, carregados por meio de um modulepath (que substitui o antigo classpath), mais simples e mais eficiente. Um classpath típico lista os diretórios onde residem os pacotes, sem deixar claro quais são os pacotes necessários dentro de cada um dos diretórios indicados. Já um modulepath lista apenas os módulo necessários, reduzindo a incerteza em relação aos elementos necessários para uma aplicação.

Na versão 9 o JDK é tipicamente instalado num diretório jdk-9, que possui um subdiretório jmods que contém todos os módulos da API Java, resultado do trabalho de modularização do Jigsaw. O modulo java.base está contido no arquivo java.base.jmod que possui apenas 15.8MB. Os arquivos de extensão jmod tem formato compatível ao dos arquivos jar, ou seja, são uma espécie de arquivo compactado no formato ZIP. O tamanho total do subdiretório jmods é 116MB (maior que o antigo rt.jar). Além disso, existe um arquivo modules (sem extensão), no subdiretório lib, com 167MB, que condensa todos os módulos, e cuja existência é garantir a compatibilidade com as aplicações produzidas por versões anteriores ao Java 9.

Já a versão 9 do JRE, instalada num diretório jre-9, existe apenas o subdiretório lib, no qual existe o arquivo modules com 107MB, requerido para executar qualquer aplicação Java. 

A presença do arquivo modules, embora pareça recriar a situação anterior do rt.jar, é uma consequência da retrocompatibilidade garantida pelo Java 9 em relação às versões anteriores. Esta é a configuração default do JDK e do JRE nesta versão. Mas a grande diferença proporcionada pelo Jigsaw está naquilo que pode ser feito além da configuração padrão.

Com a modularização do JDK torna-se possível especificar quais módulos do Java Runtime serão usados, reduzindo o trabalho da JVM na localização de tipos (com menos módulos, menos tipos, menos trabalho no carregamento de classes). Assim, se a aplicação não usa componentes Swing, o módulo java.desktop não precisa ser incluído; se o suporte para Corba não é necessário, nada  de especificar java.corba; e assim por diante. Apenas o módulo java.base é essencial (por isso é carregado implicitamente).

Na prática, isto significa que o Java 9 torna possível customizar a JVM para conter apenas os módulos necessários a um conjunto específico de aplicações; até mesmo otimizando-o para uma única aplicação especial. Quando tal customização não é feita, a configuração default, de compatibilidade mais ampla, é garantida.

jlink

A nova ferramenta de linha de comando jlink permite que sejam escolhidos os módulos e dependências a serem inclusas em uma distribuição, com granularidade muito fina, unindo-os. Isto muito contribui para reduzir e controlar o tamanho das distribuições. 

Abaixo temos o formato típico da linha de comando do jlink, envolvendo o modulepath, os módulos inclusos e a indicação da saída.

> jlink --module-path <modulepath>
       --add-modules <module>[,<module>...]
       --output <path>

Com isto o jlink permite criar um arquivo modules completamente customizado para uma aplicação específica, o que permite otimizar a JVM para ambientes particulares. Dentre as várias opções do jlink é possível indicar o nível de compressão dos módulos, os serviços interligados, plugins, a lista de módulos observados, etc.

Um exemplo de uso do jlink é:

> jlink --module-path %JAVA_HOME%/jmods;projectMod
      --add-modules br.gov.sp.fatec.saudacoes
      --output saudacoesapp

Este comando cria uma imagem runtime que contém o módulo br.gov.sp.fatec.saudacoes, incluindo suas dependências transitivas (isto é, decorrentes de sua execução). O valor de --module-path é o caminho dos diretórios contendo os módulos previamente empacotados. Use ';' como separadores de diretório no Microsoft Windows e ':' no Unix/Linux. Assim, %JAVA_HOME%/jmods é o diretório que contém o módulo java.base.jmod (e os demais módulos padrão do JDK). O diretório projecMod, incluído no modulepath, é aquele que contém os demais artefatos, no caso, o módulo denominado br.gov.sp.fatec.saudacoes. A saída produzida, a imagem da aplicação, será armazenada no diretório saudacoesapp.

Além disso, o jlink permite também controle avançado sobre a ligação entre os módulos, com a possibilidade de escolhe entre ligação estática (static linking), que cria módulos maiores, mas mais rápidos; e ligação dinâmica (dynamic linking), que cria módulos menores, mas com mais tempo de carregamento maior.


Conclusões

O Jigsaw tem um papel central no Java, pois com ele se espera melhorar a modularidade, a eficiência e também a segurança da plataforma. Com ele torna-se possível criar aplicações mais escaláveis, leves, robustas e seguras.

A ambição maior do Jigsaw é incentivar o desenvolvimento de um ecossistema completo baseado na linguagem e na JVM, por meio de um software develpment kit (SDK) modular, para criar aplicações modulares com uso de ferramentas igualmente modulares.

Concretamente o Jigsaw permite solucionar o JAR hell (vide primeira parte do post); obter melhor encapsulamento e segurança entre pacotes e clientes destes pacotes; performance melhorada e tamanho reduzido do ambiente mínimo requerido para aplicações modulares.

Na próxima e última parte deste post será construída uma aplicação modular.

Para Saber Mais



domingo, 17 de dezembro de 2017

Java 9::Jigsaw, a nova modularidade da plataforma (Parte I)

A característica mais esperada da versão 9 do Java é, certamente, o resultado do projeto Jigsaw, que tomou anos de discussões e desenvolvimento, e cuja essência é a melhoria substancial da modularidade da plataforma Java.


Para falar do Jigsaw é necessário, primeiro, discutir a ideia de  modularidade, para, depois, discutir os dois aspectos principais do próprio Jigsaw, que são sua especificação e sua implementação na versão 9 da plataforma Java.

A parte I deste post aborda o conceito de modularidade e a especificação do Jigsaw. A parte II tratará de sua implementação, enquanto a parte III contém um exemplo de seu uso. Estes posts fazem parte de uma série que trata das novidades do Java 9:Primeiras Impressões.

Modularidade

A modularidade é um princípio muito importante no projeto de software que enfatiza a criação de conjuntos de classes ou componentes que possam ser reutilizados em diferentes contextos. Tais conjuntos são considerados os módulos de um sistema de software.

Assim o processo de modularização auxilia: 
  • na ocultação da implementação por meio de encapsulamento forte; 
  • na redução do acoplamento entre componentes; 
  • na simplificação dos contratos entre componentes (de diferentes módulos principalmente); e 
  • na redução e explicitação de dependências entre componentes. 


Modularidade no Java

Até a versão 8, a unidade básica da modularidade no Java eram os arquivos JAR (Java Archives), que nada mais são do que arquivos compactados contendo uma estrutura de subdiretórios e arquivos de classe Java correspondentes a um ou mais pacotes de classes Java, eventualmente incluindo alguns outros tipos de arquivos (até mesmo o próprio código fonte), que podem ser tratados como recursos de aplicações. 

Embora os arquivos JAR sejam capazes de agrupar classes relacionadas, sua organização possui algumas limitações:
  • contratos e dependências explicitas entre os arquivos JAR componentes de uma aplicação;
  • encapsulamento fraco dos elementos contidos nos JARs.

Além disso, o mecanismo de carregamento de classes baseado nos arquivos JAR não impede, restringe ou sequer notifica que múltiplas versões de um mesmo JAR são encontradas no classpath, levando a efeitos indesejáveis e imprevisíveis, um problema sério conhecido como Jar Hell.

Isso sem contar com aplicações que são corretamente compiladas, mas falham em tempo de execução por conta de classes não encontradas (JARs ausentes) ou exibem erros decorrentes de versões incorretas de certas classes (JARs inadequados).

Com estas limitações em mente, os projetistas do Java criaram uma nova construção na linguagem para estabelecer uma nova unidade de modularização que pudesse superar os problemas existentes com os JARs, a qual foi denominada (sem muita criatividade) como module. 

O novo artefato: module

No Java 9, como resultado do projeto Jigsaw, é possível a criação de módulos. Embora os módulos sejam também arquivos JAR, eles são dotados de três propriedades obrigatórias e fundamentais que, explicitamente indicam:
  • o nome do módulo, que é sua identificação (única);
  • suas dependências, isto é, o que este módulo requer; e
  • a definição de sua Application Programming Interface (API), ou seja, o que este módulo fornece/exporta.


O nome do módulo, embora possa ser arbitrário, deve ser único. Por isso é recomendado o uso do esquema conhecido de denominação de pacotes da convenção Java, ou seja, sua URL invertida, tais como org.apache.commons.io, com.google.guava, ou br.gov.sp.cps.fatec.ads.

As dependências de um módulo, ou seja, suas necessidades, são expressas por uma lista de pacotes que precisam ser exportados por um ou mais módulos. Observe que o nome dos módulos exportadores não é necessário, mas apenas os pacotes exigidos. As classes necessárias nestes módulos devem ser públicas.

O módulo também deve indicar os pacotes que são exporta, ou seja, sua API pública. Deve ser notado que classes existentes no módulo, não serão acessíveis externamente se seus pacotes não forem exportados, mesmo que sejam públicas.

Também existem outras duas propriedades, cujo uso esperado não é tão comum, mas que permitem especificar:
  • os serviços consumidos pelo módulo e
  • os serviços providos pelo módulo.

Estas propriedades são usadas apenas quando serviços são providos e consumidos por meio da interface java.util.ServiceLoader.

Estas informações são codificadas num descritor de módulo (module descriptor) que é criado como um arquivo module-info.java, o qual é compilado como module-info.class e empacotado junto os demais elementos do módulo, de modo que a JVM possa recuperar tais informações, permitindo o carregamento de classes e outros recursos, tratando-os como um módulo.

A informação de módulo, contida no arquivo module-info.java, tem a estrutura que segue:

module MODULE_NAME {
   requires MODULE_NAME_1;
   requires MODULE_NAME_2;

   requires MODULE_NAME_N;

   exports PACKAGE_NAME_1;
   exports PACKAGE_NAME_2;

   exports PACKAGE_NAME_N;
}


Como cada módulo contém a informação que necessita dos demais, a questão do Jar Hell se torna coisa do passado, com mostra a figura que segue.


Os módulos (modules) são a construção de mais alto nível no novo sistema de modularização implementado na linguagem, englobando os pacotes (packages). Os desenvolvedores podem organizar seu código em módulos, declarando as dependências existentes entre eles nas definições contidas no arquivo module-info.java.

A nova acessibilidade

Com o novo sistema de modularidade do Java introduzido pelo Jigsaw, a acessibilidade dos tipos existentes sofre uma mudança considerável.

A acessibilidade do Java 8 e das versões anteriores era definida pelos níveis:
  • público, 
  • default, 
  • protegido e 
  • privado
Tais níveis são indicados por meio dos especificadores de acesso public, protected e private, sendo que a omissão da indicação explícita do nível de acesso é entendida como o nível default, que não tem especificador próprio.

Em resumo, até o Java 8, qualquer tipo público presente no classpath seria acessível por qualquer outro tipo.

No Java 9, com o Jigsaw temos:
  • público para todos que tem acesso ao módulo (exports);
  • público para módulos específicos (comentado na parte 2 do post);
  • público apenas para os demais tipos pertencentes ao módulo, mas não outros externos ao módulo;
  • default;
  • protegido; e
  • privado.
Assim, o novo esquema de modularidade inclui dois níveis extras de acesso público, cujas restrições são associadas ao novo artefato module.

Benefícios esperados

O Jigsaw desempenha um papel central no futuro da plataforma Java, pois provê a base para sua evolução, ao mesmo tempo que garante compatibilidade com o esquema de modularidade anterior, baseado exclusivamente em pacotes.

De fato, um programador pode continuar a utilizar a versão 9 do Java como se o Jigsaw e o novo sistema de modularidade nem existissem, pois sua utilização é completamente transparerente.

Isto mostra que o projeto alcançou um ótimo consenso, consolidando um passo na direção de uma plataforma aderente a boas práticas do projeto de software; compatível com arquiteturas de software mais modernas (microserviços -- microservices); de distribuição melhorada (compartimentos -- containers); e otimizada, pois o novo JRE mínimo tem pouco mais de 15MB.

Na próxima parte deste post será tratada a implementação do Jigsaw na plataforma, ou seja, como estão organizados os módulos a partir da versão 9. A parte III inclui um exemplo de como construir uma aplicação modular.

Para Saber Mais

domingo, 10 de dezembro de 2017

Java 9::Melhorias no Optional

NullPointerException: quem nunca ficou realmente chateado ao ver, inesperadamente, esta exceção que atire a primeira crítica! Como toda exceção não prevista, é provável que sua ocorrência cause o encerramento anormal e abrupto do programa, algo muito constrangedor quando é o usuário que presencia este fato.
É notório que a famigerada exceção NullPointerException é lançada quando ocorre uma tentativa de acessar um campo ou acionar um método por meio de uma variável de referência inadequadamente inicializada, isto é, sem exista uma instância de objeto na variável referência utilizada. Traduzindo em miúdos: a variável de referência contém apenas null, que sinaliza a ausência de um objeto.

Todo programador sabe que isso (a tentativa de usar um objeto inexistente) é uma operação ilegal, ou seja, a questão aqui não é o uso inadvertido de variáveis de referência não inicializadas, até mesmo porque muitos dos IDE disponíveis, como o Eclipse, sinalizam, até com certo estardalhaço, que variáveis não inicializadas estão em uso; mas quando operações realizadas sobre outros objetos retornam referências válidas.

O problema mais comum, que dá origem às ocorrências de NullPointerException, é o acionamento de um método que, eventualmente, possa retornar um resultado null. Até a versão 7 do Java, a solução para evitar esta exceção era testar as variáveis de referências duvidosas, fazendo seu uso apenas quando diferentes de null. Mas isto polui o código.

A versão 8 do Java trouxe a classe Optional para solucionar este problemas.

Optional<T>

Com nítida inspiração nas linguagens de programação Scala, Haskell e Guava, a classe genérica Optional<T> funciona como uma classe genérica (um template portanto) para encapsular referências do tipo T que podem ser nulas. Seu uso permite que o projetista de uma API possa, mais claramente, indicar que um valor nulo pode ser retornando ou passado para um método, reduzindo a necessidade de consulta à documentação desta operação.

Objetos do tipo Optional<T> são como contêineres (i.e., containers) que podem armazenar um valor de qualquer tipo T ou apenas null. A classe Optional também provê alguns métodos úteis que podem eliminar a verificação explícita da presença de null.

Considere o trecho de código que segue:

String parametro = ????; // inicialização do parâmetro, que pode ou não ser nulo
System.out.println("Parâmetro está definido? " + (parametro!=null ? true : false) );
if (parametro == null) {
   System.out.println("Parâmetro: [sem definição]" );
} else {
   System.out.println("Parâmetro: " + parametro);
}
if (parametro != null) {
   System.out.println("programa " + parametro);
} else {
   System.out.println("programa");
}

Como a variável do tipo String denominada parametro pode ou não ser nula, para que não ocorram erros decorrentes de seu uso indevido, o trecho de programa verifica explicitamente (de maneiras diferentes) quando parametro é ou não nulo, avolumando o trecho e dificultando sua legibilidade. 

O uso da classe Optional<T> e seus métodos permite simplificar código como este, com uma construção que pode ser como segue, onde o parametro é nulo:

Optional<String> parametro = Optional.ofNullable(null);
System.out.println("Parâmetro está definido? " + parametro.isPresent() );
System.out.println("Parâmetro: " + parametro.orElseGet(() ‐> "[sem definição]") );
System.out.println(parametro.map(p ‐> "programa " + p).orElse( "programa" ) );

O método isPresent() retorna true quando a instância de Optional<T> contém um valor não nulo e false quando nulo. O método orElseGet() provê um mecanismo alternativo, baseado numa expressão lambda que se comporta como um Producer, que provê um valor quando Optional<T> contém nulo. Já o método map() transforma o valor corrente de Optional<T> e retorna uma nova instância. O método orElse() é semelhante a orElseGet(), mas toma um valor default ao invés de uma função de produção.

A saída deste programa, executado sem qualquer erro, é:

Parâmetro está definido? false
Parâmetro: [sem definição]
programa

Um outro exemplo seria o mesmo trecho de programa, mas com um valor provido para o parâmetro.

Optional<String> parametro = Optional.of("--check");
System.out.println("Parâmetro está definido? " + parametro.isPresent() );
System.out.println("Parâmetro: " + parametro.orElseGet(() ‐> "[sem definição]") );
System.out.println(parametro.map(p ‐> "programa " + p).orElse( "programa" ) );

Agora a saída deste programa, que continua sendo executado sem erros, é:

Parâmetro está definido? true
Parâmetro: --check
programa --check

Este exemplo simples permite observar que o uso do parâmetro, nulo ou não, é feito corretamente e de maneira segura, mesmo sem a presença de testes explícitos realizados com diretivas if.


Melhorias em Optional<T>

A versão 9 do Java traz algumas melhorias, pequenas, mas bastante úteis.

O novo método ifPresentOrElse() verifica se um valor está presente na instância de Optional<T>, conduzindo a ação indicada com o valor, ou realizando outra para a situação de vazio (conteúdo null):

public void ifPresentOrElse(Consumer<T> action, Runnable emptyAction);

Dito de outra maneira, este método codifica um padrão comum, no qual se deseja executar uma ação quando Optional<T> contém um valor, ou uma ação diferente quanto tal valor está ausente.

Considere o trecho que segue:

String parametro = getParameterByPosition(position);
if (parametro != null) {
   executeParameterizedAction(parametro);
} else {
   executeDefaultAction();
}

Com o uso do novo método de Optional<T>, o trecho equivalente seria:

Optional<String> parametro = Optional.of(getParameterByPosition(position));
parametro.ifPresenteOrElse(
    this::executeParameterizedAction,
    ()-> executeDefaultAction() );


Ou ainda mais simples, se o método getParameterByPosition(int) retornasse diretamente um Optional<String>:

getParameterByPosition(position).
   ifPresenteOrElse(this::executeParameterizedAction,
                    ()-> executeDefaultAction() );


Outro método interessante é or(), que toma uma função que cria um Optional<T> como argumento. Sua assinatura é:

public Optional<T> or(Supplier<Optional<T>> supplier);

Se o valor está presente, retorna um Optional<T> que encapsula tal valor, senão, retorna um outro Optional produzido pela função de geração. Isto é útil para encadear duas ou mais funções que retornam Optional, de modo que o primeiro Optional contendo um valor não nulo seja retornado.

Por exemplo:

public Optional<String> getRecord(int recordNumber) {
   return findInDatabase(recordNumber).or(()->findInFileSystem(recordNumber));
}

Este método retorna o resultado de findInDatabase(int), quando este não é nulo, ou ou resultado de findInFileSystem(int) quando não encontrado pelo primeiro método.

Conclusões

As melhorias em Optional<T> são simples, mas auxiliam na construção de programas melhores, principalmente livres da desagradável exceção NullPointerException. É claro que métodos cuja implementação ainda retorne null deverão ser modificados para retornar objetos Optional<T> ou, ao menos, ter seus resultados encapsulados em objetos Optional<T> para que tais benefícios possam ser percebidos. Por outro lado, a inclusão de Optional<T> no Java 8, e das melhorias comentadas no Java 9, não farão qualquer mágica, exigindo alguma manutenção, mas que com certeza, valerá o esforço.


Para Saber Mais


domingo, 26 de novembro de 2017

Java 9::Melhorias na Stream API

As streams para coleções e as operações em massa foram uma grande adição do Java 8, pois sua utilização permite a realização de operações complexas com código simples e bastante direto.
 
 
A característica mais marcante desta API é possibilitar que cada elemento de uma coleção seja tratado sem a necessidade de construir explicitamente laços de repetição para tal processamento, ou seja, permitindo aplicar as operações desejadas nas coleções como um todo, o que se denominou de operações em massa (bulk operations) sobre as coleções.

O Java 9 traz alguns complementos úteis para esta API, como novas fontes de dados para as streams, além de algumas novas características.

Novas fontes de dados

Todas as coleções possuem um método stream() que permitem iniciar um stream pipeline, o qual poderá ser utilizado posteriormente numa sequência arbitrária de operações intermediárias, até que seja realizada uma última operação terminal. A stream que dá origem ao stream pipeline é conhecida como stream fonte (source stream).

Além da possibilidade de criar um stream fonte a partir das coleções, existia no Java 8 um conjunto limitado de possibilidades obtenção de stream fonte a partir de elementos externos às coleções, como por exemplo, java.io.BufferedReader.lines().

O Java 9 adiciona algumas novas fontes úteis a esse conjunto, por meio destes novos métodos:
  • java.util.Scanner.tokens()
  • java.util.regex.Matcher.results()
  • java.util.Optional.stream()

Em particular o método tokens() da classe Scanner é bem versátil, como mostra o exemplo que segue:
 

Novas características das Streams

Existem quatro novos métodos adicionados na interface java.util.stream.Stream<T>:
  • Stream<T> takeWhile(Predicate)
  • Stream<T> dropWhile(Predicate)
  • Stream<T> ofNullable(T)
  • Stream<T> iterate(T, Predicate< T>, UnaryOperator<T>)

Apenas recordando, a interface Stream<T> representa uma sequência de elementos (na verdade, uma cadeia de referências de objetos) sobre os quais uma ou mais operações podem ser executadas. As subinterfaces IntStream, LongStream e DoubleStream são especializações de Stream<T> voltadas para os tipos primitivos mais comuns, contendo algumas funcionalidades adicionais.

Stream<T> takeWhile(Predicate) e Stream<T> dropWhilePredicate)

Dois dos novos métodos são takeWhile(Predicate) e dropWhile(Predicate), semelhantes aos métodos existentes limit(long) e skip(long), mas que tomam um predicado ao invés de valores fixos. Um predicado é uma condição arbitrada pelo programador, ou seja, um critério de seleção, que retorna true ou false.

O método takeWhile(Predicate) considera os elementos iniciais de um stream que atendem o predicado/critério dado.

Nos fragmentos que seguem (que podem ser facilmente testados com uso do jshell), é declarada e inicializada uma coleção com 1000 valores sequenciais (0, 0.33, 0.66, 1.0, 1.33 ...).
 
 

 
Esta coleção pode ser exibida integralmente com:
 
 
 

O método limit(long) permite obter uma nova stream com, por exemplo, os n primeiros elementos da coleção, no caso os cinco primeiros:
 
 
 
O novo método takeWhile(Predicate), ao invés de tomar um número fixo dos primeiros elementos como limit(long), toma todos os primeiros elementos que atendem o critério dado (o predicado fornecido). Assim takeWhile(Predicate) vai selecionando os elementos presentes no início da stream enquanto tal predicado é satisfeito. No fragmento que segue, este método é usado para extrair os primeiros elementos da coleção que são menores do que 2.5. 
 

O método skip(long), por sua vez, produz uma nova stream pulando os n primeiros elementos da coleção (no fragmento que segue n = 990), produzindo resultados ilustrados a seguir.
 
 
 
 
 

Já o novo método dropWhile(Predicate), descarta todos os primeiros elementos que atendem o critério dado (o predicado fornecido). Assim dropWhile(Predicate) vai pulando os elementos presentes no início da stream enquanto seu predicado é satisfeito. No fragmento que segue, este método é usado para descartar os primeiros elementos da coleção que são menores do que 330.
 
 
 
 

 
Como os valores da coleção estão ordenados, o resultado de takeWhile é bastante previsível. Mas, quando o conteúdo da coleção não está ordenado, o uso de takeWhile requer cautela, pois o primeiro valor que não atende o predicado interrompe a seleção de elementos, o que pode ocorrer no primeiro elemento existente, produzindo como resultado um stream vazio.

Observe o resultado do fragmento abaixo que gera uma coleção com conteúdo aleatório.
 
 
 
 

 
 
 
 
Conforme o critério usado com takeWhile, o resultado pode ser um stream com conteúdo ou mesmo vazio, apesar de existirem valores na coleção que atendem o critério, mas não seus primeiros. O exemplo que segue mostra esta situação.
 
 
 
 
O mesmo pode acontecer no uso de dropWhile.

Considerando que a seleção e o descarte proporcionados, respectivamente, por takeWhile e dropWhile, são condicionais (i.e., baseado em um predicado), pode ser conveniente ordenar o stream de onde os elementos serão retirados. A ordenação de um stream pode ser facilmente obtida com o método sorted(), que retorna um stream classificado conforme o critério de ordenação natural de seu conteúdo.
 

Stream<T> ofNullable(T)

Outro elemento novo é o método estático ofNullable(T) da interface Stream<T>. Esta operação retorna uma stream do tipo T contendo um elemento ou nenhum, nos casos do argumento fornecido ser não nulo ou nulo. Este método é bastante útil para eliminar a verificação de elementos nulos antes da construção de uma stream. O fragmento que segue ilustra o uso deste novo método.
 
 
 
 
 
 

Stream<T> iterate(T, Predicate< T>, UnaryOperator<T>)

Também foi adicionada uma nova versão do método estático iterate, que toma três argumentos: o primeiro é um valor de inicialização (seed), o segundo é uma condição expressa por um predicado, e o último é uma função de incremento ou decremento. O objetivo deste método é simular um laço, tal como:
for (T index=seed; hasNext.test(index); index = next.apply(index)) {
...
}
 
Conforme os elementos fornecidos, pode obter tanto uma sequência vazia, como uma contendo um número finito de elemento, de maneira bem conveniente. Nos fragmentos que seguem, esta versão de iterate é usada para gerar duas sequências de inteiros, uma transformada em array com toArray() e outra coletada na forma de uma lista com collect.
 
 
 
 

Conclusões

As novas adições efetuadas na Stream API tornam esta biblioteca ainda mais conveniente e flexível de usar, lembrando que quão versátil são as streams e suas operações de filtragem, mapeamento e redução. É algo que vale a pena estudar!

Para saber mais


segunda-feira, 20 de novembro de 2017

Java 9::O Console jshell

Estreia no Java uma nova interessante ferramenta de linha de comando denominada jshell, o console Java. Por meio dela é possível avaliar expressões, efetuar declarações e executar diretivas do Java, sem a necessidade de construir um projeto, um programa ou mesmo um método para seu teste.

Este post é parte da série Java 9::Primeiras Impressões, que comenta as principais características da nova versão do Java.

REPL

A abreviatura REPL significa Read-Evaluate-Print-Loop, uma referência a consoles web e plug-ins de diversas linguagens, onde o usuário fornece uma expressão ou diretiva da linguagem ao console (leitura), a qual é processada imediatamente (avaliação), tendo seus resultados exibidos (impressão), retornando à situação inicial (loop) onde uma outra expressão ou diretiva pode ser interativamente fornecida.

Conforme as características próprias de cada um dos consoles que existem, os resultados de avaliações anteriores podem ou não estar disponíveis, possibilitando a acumulação de efeitos e o uso de tais ambientes tanto para experimentação de coisas simples, como para simulação de construções mais complexas, tudo de maneira bastante direta. Esta é a grande vantagem do uso dos consoles REPL.

Como veremos, o jshell permite tanto executar expressões simples, como definir variáveis, instanciar objetos e executar trechos relativamente sofisticados de código, tornando-se igualmente apropriado para o estudante e para o programador mais experiente.

Acionando o jshell

Em um prompt de comandos, console ou terminal, dependendo do seu sistema operacional, garanta que o path está corretamente ajustado. No MS Windows usualmente basta executar:

C:\Users\Jandl>path=C:\Program Files\Java\jdk-9\bin;%path%

No meu caso, o JDK da versão 9 está instalado em C:\Program Files\Java\jdk-9\. O jshell, e as demais ferramentas de linha de comando do JDK estão no subdiretório bin. Após o ajuste do path, basta acionar o comando jshell, como mostra a figura que segue.
Prompt de comando e o acionamento do jshell

Experimente algo simples, como somar dois valores inteiros, pressionando ENTER ao final:

Observe que a expressão “1 + 2” foi avaliada, de modo que o resultado foi armazenado na variável temporária denominada $1, a qual foi exibida. Fornecendo apenas o nome de tal variável, obteríamos o seu conteúdo.

Os ponto-e-vírgula, exigidos no código Java, podem ser omitidos, visto que o jshell, frequentemente, é capaz de adicioná-los.

Testando diretivas e declarações

Diretivas simples, de repetição ou decisão, também podem ser executadas, bastando digitar o código que se deseja avaliar, pressionado ENTER para seu processamento:

Se desejado, variáveis de qualquer tipo válido podem ser declaradas:

O comando /vars permite visualizar todas as variáveis válidas previamente definidas, assim como seus tipos e conteúdos:

Eventualmente, o fragmento de código que se deseja testar fica melhor organizado se escrito em várias linhas. Para isto basta digitar uma linha e, com SHIFT+ENTER, continuar na linha seguinte, como na figura abaixo:

Histórico de comandos: com as setas UP e DOWN do teclado é possível navegar pelas lista de fragmentos anteriormente processados pelo jshell. Após escolher o fragmento desejado, basta um ENTER para executá-lo.

Caso você deseje modificar um comando anterior, ou corrigir uma linha com código errado, basta usar as setas UP e DOWN do teclado para selecionar tal linha, usando as setas LEFT e RIGHT, HOME e END para navegar na linha. Se for um trecho de várias linhas, é necessário repetir todas as linhas corretas, mais as corrigidas, na sequência desejada.

O comando /list lista todos os fragmentos de código válido que foram avaliados.

Use o comando /<id> para executar o fragmento identificado pelo id correspondente ou /! para repetir a execução do último fragmento processado. Lembre-se apenas que os vários fragmentos utilizam as variáveis definidas de maneira global, ou seja, todos os fragmentos têm acesso à todas as variáveis, propagando seus efeitos por meio delas.

Características avançadas

Também é possível instanciar objetos, declarar e inicializar arrays:

Como esperado, objetos de qualquer tipo, assim como arrays, podem ser usados em fragmentos de código, como este que segue, que utiliza o objeto StringBuilder e o array de String declarados acima:

Com o jshell é possível declarar-se métodos para uso independente ou em outros fragmentos.

Até mesmo novos tipos (classes, interfaces ou enumerações) podem ser definidos.

Desta maneira, objetos dos tipos existentes ou definidos no ambiente jshell podem ser instanciados e utilizados.




Comandos do jshell

O jshell suporta a execução de vários comandos, todos precedidos por /, que podem facilitar sua utilização. A tabela que segue mostra os principais comandos em suas formas mais simples:

Comando
Efeito
/list
Lista os fragmentos válidos fornecidos.
/edit <id>
Edita o fragmento identificado por <id>.
/drop <id>
Remove o fragmento identificado por <id>.
/save <fileName>
Salva os fragmentos de código no arquivo indicado.
/open <fileName>
Abre o arquivo contendo fragmentos de código.
/vars
Lista as variáveis declaradas e seus valores.
/methods
Lista os métodos declarados e suas assinaturas.
/types
Lista os tipos declarados.
/history
Lista o histórico do código digitado.
/<id>
Executa o fragmento identificado por <id>.
/!
Reexecuta o último fragmento válido.
/help
Exibe informação sobre o jshell e seus comandos
/exit
Finaliza o jshell.

Considerações Finais

O jshell é, realmente, uma ótima ferramenta, pois alia a simplicidade e conveniência de uso com muita flexibilidade. Como permite testar declarações, diretivas e expressões, incluindo a definição de métodos independentes e de tipos completos, é muito útil para que deseja estudar Java, pois permite praticar a construção de código limitando-se ao essencial. Mesmo programadores mais experientes podem se beneficiar do seu uso para simular e testar fragmentos de código de maneira rápida.

É isso!
/exit


Para saber mais