[Pesquisar este blog]

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.iocom.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

domingo, 19 de novembro de 2017

Java 9::Métodos Privados em Interfaces

Uma das novidades da versão 9 do Java são os métodos com visibilidade privada nas interfaces. Este novo elemento, acrescido aos métodos default e estáticos incluídos na versão 8, vem para completar as alternativas construtivas nas interfaces.

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

Interfaces

Uma interface é a definição de conjunto de elementos visíveis, ou seja, públicos, dos objetos de uma classe. Quando a implementação de uma classe contém todas as operações de conjunto particular, dizemos que tal classe realiza tal interface, ou seja, esta classe atende as especificações ditadas pela interface implementada.

Uma interface Java é algo simples como:

package jandl.j9;
public interface Adder {
void add(Number value);
Number getTotal();
void setTotal(Number initValue);
void reset();
}

Implicitamente, todas as operações de uma interface são, por padrão, públicas e abstratas, ou seja, todas poderiam receber o especificador de acesso public e o modificador abstract em suas declarações.

Desta maneira, uma interface é como um contrato, que dita quais operações devem estar disponíveis para obter-se um conjunto específico de funcionalidade (e de interoperabilidade, portanto). Isto é particularmente importante, pois a questão aqui é como os objetos podem ser utilizados, e não como foram implementados.

A classe AdderImpl que segue é uma implementação possível da interface Adder.

package jandl.j9;
public class AdderImpl implements Adder {
private double total;
public AdderImpl() { reset(); }
public AdderImpl(Number value) { setTotal(value); }
@Override
public void add(Number value) { total = total + ((Number) value).doubleValue(); }
@Override
public Number getTotal() { return new Double(total); }
@Override
public void setTotal(Number initValue) { total = ((Number) initValue).doubleValue(); }
@Override
public void reset() { total = 0; }
}

Desta maneira, classes pertencentes a diferentes hierarquias de objetos, mas que implementam uma interface comum, podem, polimorficamente, serem tratadas como de um mesmo tipo que corresponde a tal interface.

A modelagem de sistemas por meio da definição de interfaces é uma prática reconhecidamente boa, também conhecida como programação por contratos.

Assim, o uso de interfaces é particularmente importante no projeto de sistemas, o que também é enfatizado pelos princípios SOLID (discutidos numa série de posts iniciados por Os Princípios SOLID – Parte I).

Evolução de interfaces

Apesar das muitas conveniências no projeto e uso de interfaces, surgem situações onde uma interface necessita ser alterada. Aí o cenário deixa de positivo. Modificar uma interface, no sentido de alterar a assinatura de um dos métodos presentes ou acrescentar novas operações cria a exigência de modificar ou completar todas as classes que realizam tal interface, numa constrangedora propagação das alterações realizadas.

A partir do Java 8 se tornou possível realizar alterações numa interface existente, mantendo a compatibilidade com suas versões anteriores, isto é, sem propagar as alterações, simplificando muito o trabalho de manutenção do código. Para isto foram introduzidos os métodos default e estáticos às interfaces.

Os métodos default são aqueles declarados com o especificador de visibilidade public e com o novo modificador default. Além disso, tais métodos devem ser implementados na própria interface, pois não são abstratos, o que evita a propagação da modificação.

A interface Adder poderia receber um novo método, como segue, sem que as classes que a realizam sejam afetadas.

package jandl.j9;
public interface Adder {
:
// método default, cuja implementação é adicionada na interface modificada
default void add(Adder adder) {  add(adder.getTotal()); }
}

De forma análoga, os métodos estáticos são aqueles declarados com o especificador de visibilidade public e com o conhecido modificador static, o que também exige sua própria interface, pois, como qualquer elemento estático, pertencem a seu tipo e não a suas instâncias. Da mesma forma sua adição não propaga qualquer efeito nas classes que já realizam a interface modificada.

package jandl.j9;
public interface Adder {
:
// método estático, cuja implementação é adicionada na interface modificada
static void add(Adder adder, List<Number> list) {
    list.stream().forEach((value)->{ adder.add(value); });
}
}

As duas alternativas, dos métodos default e dos métodos estáticos, possibilitam a evolução de interfaces existentes, sem propagação de alterações e com garantia da compatibilidade binária com suas versões antigas.

Métodos privados em interfaces

A adição da possibilidade de declarar métodos privados em interfaces, embora estranha à primeira vista, é um complemento para auxiliar o programador no uso dos métodos default e métodos estáticos nas interfaces.

Os novos métodos privados são declarados com o especificador de visibilidade private e, opcionalmente, com o modificar static. Como qualquer membro privado, só podem ser acessados pelos demais membros do seu tipo.

Sendo assim, o uso de métodos privados, estáticos ou não, numa interface tem como objetivo permitir que o programador defina operações auxiliares para os demais métodos default e estáticos públicos, sem expor tal operação.

Na nova implementação da interface Adder, um método privado estático add(Adder, Number[]) provê o serviço de adição para três outros métodos da interface.

package jandl.j9;
import java.util.List;

public interface Adder {
void add(Number value);
Number getTotal();
void setTotal(Number initValue);
void reset();
default void add(Adder adder) { add(adder.getTotal()); }
// métodos default e estáticos que utilizam método privado desta interface
default void add(Number[] array) { add(this, array); }
default void add(List<Number> list) { add(this, list.toArray(new Number[0])); }
static void add(Adder adder, List<Number> list) {
   add(adder, list.toArray(new Number[0]));
}
// método estático privado que prove serviço para os demais
private static void add(Adder adder, Number[] array) {
   for(int i=0; i<array.length;i++) { adder.add(array[i]);
}
}
}

Considerações Finais

As interfaces na versão 9 Java suportam, então, uma variada combinação de especificadores e modificadores, como sumarizado na tabela que segue:

Especificador
Modificador
Situação
Nenhum
Nenhum
Declaração válida de método, implicitamente, público e abstrato.
Nenhum
abstract
Declaração válida de método abstrato, implicitamente público.
Nenhum
default
Declaração válida de método default, implicitamente público.
Nenhum
static
Declaração válida de método estático, implicitamente público.
public
Nenhum
Declaração válida de método público, implicitamente abstrato.
public
abstract
Declaração válida explícita de método público e abstrato.
public
default
Declaração válida explícita de método público e default.
public
static
Declaração válida explícita de método público e estático.
private
Nenhum
Declaração válida explícita de método privado.
private
abstract
Declaração inválida. Erro de compilação.
private
default
Declaração inválida. Erro de compilação.
private
static
Declaração válida explícita de método privado e estático.

Os métodos privados não são herdados por subinterfaces ou implementações das classes.
Finalmente, apesar da utilidade exemplificada, de permitir modificações em interfaces existentes sem a propagação de efeitos colaterais indesejados, cabe destacar que, se uma interface existente deve ser modificada, isto indica duas situações: alterações nas regras de negócio da aplicação ou identificação de falhas no projeto do software. Enquanto a primeira razão pode ser imprevisível e inevitável; a segunda reforça o cuidado necessário com o projeto de qualquer software.

A implementação de métodos (default ou estáticos, públicos ou privados) é, do ponto de vista da Orientação a Objetos, uma violação dos seus propósitos. Se é necessária uma implementação parcial de uma interface, é mais adequada a construção de uma classe abstrata contendo a codificação destas operações, mantendo a interface como uma classe abstrata pura (sem implementação de qualquer operação).

Reforço que isto não é um defeito do Java, mas, acredito, alternativas possíveis na plataforma para solução de problemas envolvendo interfaces.

Assim, muito cuidado no projeto de suas interfaces, que devem conter apenas as operações minimamente necessárias (tal como defendido pelo princípio de segregação das interfaces ou ISP dos princípios SOLID). Qualquer coisa diferente disso, pode não ser adequada, mesmo com todas as possibilidades ofertadas pela plataforma Java.

Para saber mais