[Pesquisar este blog]

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