A partir do Java 8 podemos definir métodos dentro de interfaces fornecendo uma implementação default. Para entender melhor, vamos analisar o código da Listagem 1.

Listagem 1. Método default


  package exemplo1;
   
  public interface TV {    
      default void ligar() {
          throw new UnsupportedOperationException("Error");
      }    
  }

Veja que o método ligar possui uma implementação default que será herdada por todas as classes que implementarem a interface TV. Se nas versões anteriores as interfaces se limitavam apenas a especificar contratos (que seriam implementadas pelas classes que as herdam), agora elas podem também fornecer comportamento.

A maior motivação para a criação de métodos default (conhecidos também como métodos defender ou virtual extension methods) foi a necessidade de adicionar novas funcionalidades às interfaces existentes sem quebrar o código que faz uso delas.

Nas versões anteriores do Java, ao adicionar um novo método numa interface, somos obrigados a alterar todas as classes que herdam dessa interface, como mostra a Listagem 2.

Listagem 2. Implementando interface


  package exemplo2;
   
  interface TV {    
      void ligar();
  }
   
  class LG implements TV {
      @Override
      public void ligar() { }
  }
   
  class Sony implements TV {
      @Override
      public void ligar() { }
  }
   
  public class Teste {
      public static void main(String[] args) {
          new LG();
          new Sony();
      }
  }

Repare que temos a interface TV e duas classes concretas que a implementam (LG e Sony). Se adicionarmos um método desligar na interface TV, teremos que fornecer implementações desse método para as classes LG e Sony, ou o compilador reclamará.

Para adicionar esse novo comportamento para a interface TV sem afetar as classes que a implementam, basta fornecer um método default, conforme mostra a Listagem 3.

Listagem 3. Adicionando método sem afetar o legado


  interface TV {    
      void ligar();
      default void desligar() { System.out.println(“desligar”); }
  }

Agora as classes LG e Sony herdam automaticamente o comportamento default de desligar da interface TV e podem ou não sobrescrever esse comportamento.

O recurso de métodos default é uma ótima ferramenta para os designers de API, pois permite que interfaces possam evoluir sem risco de comprometer o legado. Na API do Java 8 temos várias interfaces que fazem uso desse recurso:

  • List<E>: possui os novos métodos replaceAll, sort e spliterator;
  • Collection<E>: parallelStream, removei e stream;
  • Iterable<E>: forEach e spliterator.

Os métodos default permitem que os projetistas da linguagem forneçam uma melhor integração do recurso de Lambdas com as API's bases de Collections. Por exemplo, o método forEach da interface Iterable é um método default que recebe a interface funcional Consumer, permitindo que utilizemos lambdas, conforme mostra a Listagem 4.

Listagem 4. ForEach


  package exemplo4;
   
  import java.util.ArrayList;
  import java.util.List;
   
  public class Exemplo4 {
      
      public static void main(String[] args) {
          
          List<String> strings = new ArrayList<>();
          for(int i = 0; i < 10; i++) strings.add(String.valueOf(i));        
          
          // Sem lambda
          for (String string : strings) {
              System.out.println("Valor: " + string);
          }
   
          // Com lambda     
          strings.forEach((string) -> {
              System.out.println("Valor: " + string);
          });        
      }    
  }

Conflito de métodos

A motivação maior dos métodos default realmente foi facilitar a vida dos designers de APIs, notadamente os engenheiros da API do Java, porém esse recurso trouxe um efeito colateral: agora as interfaces podem prover comportamento. Mas qual a implicação disso no design de aplicações?

O Java não possui suporte para herança múltipla, pois a complexidade em se lidar com esse modelo de composição, na avaliação dos projetistas da linguagem, era um preço alto a se pagar, principalmente por causa de colisões entre métodos, atributos, duplicações e o problema do “diamante”, "onde há uma ambiguidade que surge quando, por exemplo, duas classes B e C herdam da classe A, e a classe D herda B e C simultaneamente (ver seção Links).

Mas se por um lado uma classe só pode estender uma única classe base no Java, ela pode implementar uma ou mais interfaces, obtendo em parte os benefícios da herança múltipla sem os problemas que ela acarreta, tornando o design das aplicações mais simples.

Essa simplicidade em si é sustentada devido ao fato das interfaces serem stateless (sem atributo/sem estado) e não definirem comportamento, provendo apenas declarações de métodos e constantes. Porém, essa premissa foi parcialmente quebrada no Java 8, pois agora as interfaces podem prover comportamento concreto e até métodos estáticos.

O que acontece se duas interfaces proverem métodos default com a mesma assinatura? Vejamos o exemplo da Listagem 5.

Listagem 5. Colisão de métodos


  package exemplo5;
   
  interface Carro {
      default void acelerar() {
          System.out.println("Carro acelerando");
      }
  }
   
  interface Barco {
      default void acelerar() {
          System.out.println("Barco acelerando");
      }
  }
   
  class BarcoMovel implements Carro, Barco {
      
  }
   
  public class Exemplo5 {
      public static void main(String[] args) {
          new BarcoMovel().acelerar();
      }  
  }   

As interfaces Carro e Barco possuem a mesma assinatura de método e ambas são mixadas na classe BarcoMovel. O código apresentado não compilará, pois o compilador acusará o seguinte erro:

“class BarcoMovel inherits unrelated defaults for acelerar() from types Carro and Barco“

Por não saber qual método aplicar, o compilador exige que o programador forneça a implementação do método na classe BarcoMovel, conforme o exemplo da Listagem 6.

Listagem 6. Definindo o comportamento


  class BarcoMovel implements Carro, Barco {
      @Override
      public void acelerar() {
          System.out.println("BarcoMovel acelerando");
      }
  }

Podemos também escolher uma das implementações default herdadas das interfaces Carro e Barco através de super, como podemos ver na Listagem 7.

Listagem 7. Definindo o comportamento


  class BarcoMovel implements Carro, Barco {
      @Override
      public void acelerar() {
          Barco.super.acelerar();
          //Carro.super.acelerar();
      }
  }

Veja que definimos que o comportamento escolhido será o da interface Barco, através da linha Barco.super.acelerar(). Poderíamos ter optado pelo método de Carro ou, dependendo do caso, chamar o método acelerar das duas interfaces.

No caso de métodos com a mesma assinatura mas retornos diferentes, o compilador não permitirá a combinação de interfaces, como mostra a Listagem 8.

Listagem 8. Retornos diferentes


  package exemplo6;
   
  interface Carro {
      default void acelerar() {
          System.out.println("Carro acelerando");
      }
  }
   
  interface Barco {
      default String acelerar() {
          System.out.println("Barco acelerando");        
          return "OK";
      }
  }
   
  // Isso não compila
  class BarcoMovel implements Carro, Barco {
      @Override
      public void acelerar() {
          System.out.println("BarcoMovel acelerando");
      }
      
      @Override
      public String acelerar() {
          System.out.println("BarcoMovel acelerando");
          return "";
      }    
  }
   
  public class Exemplo6 {
      public static void main(String[] args) {
          new BarcoMovel().acelerar();
      }  
  } 

O código da Listagem 8 não compilará, pois a classe BarcoMovel não pode fornecer duas versões sobrecarregadas de um mesmo método com assinatura igual e tipo de retorno diferente, pois o compilador só leva em consideração os argumentos do método e o nome para operações de sobrecarga. Isso não deve ser novidade, pois mesmo sem métodos default essa limitação já existia.

Outra situação de colisão é quando uma classe possui uma superclasse e implementa uma interface, e ambas fornecem métodos concretos com a mesma assinatura, como podemos ver na Listagem 9.

Listagem 9. Superclasse vs Interface


  package exemplo7;
   
  class Humano {
      public void voar() {
          throw new UnsupportedOperationException("Impossivel");
      }
  }
   
  interface Morcego {
      default void voar() {
          System.out.println("Voando...");        
      }
  }
   
  class Batman extends Humano implements Morcego {}
   
  public class Exemplo7 {
      public static void main(String[] args) {
          new Batman().voar();
      }  
  } 

Se executarmos o código apresentado, a saída será:


  Exception in thread "main" java.lang.UnsupportedOperationException: Impossivel
           at exemplo7.Humano.voar(Exemplo7.java:5)
           at exemplo7.Exemplo7.main(Exemplo7.java:19)
  

A implementação da superclasse sempre terá preferência sobre a implementação default da interface. Mas se a intenção for realmente usar o comportamento default da interface, basta usar super novamente, como mostra a Listagem 10.

Listagem 10. Usando super


  class Batman extends Humano implements Morcego {
      @Override
      public void voar() {
          Morcego.super.voar();
      }
  }

Ortogonalidade, Mixin e Cake Pattern

Observem o seguinte trecho de código da Listagem 11.

Listagem 11. Cronometrando trecho de código


  package exemplo9;
   
  import java.util.Random;
   
  public class Exemplo9 {
      
      public void fazerAlgo() {
          long start = System.currentTimeMillis();
          // INICIO Tarefa
          for (int i = 0; i < 100000 + new Random().nextInt(10000); i++) {}
          // FIM Tarefa
          long end = System.currentTimeMillis();
          System.out.println("Milisegundos = " + (end - start));
      }    
      
      public static void main(String[] args) {
          new Exemplo9().fazerAlgo();
      }    
  }

No método fazerAlgo executamos uma tarefa em particular e medimos o tempo que ela leva para terminar. Em aplicações onde o tempo de resposta é um requisito importante, geralmente escrevemos código para mensurar o tempo. O problema dessa abordagem é que o método fazerAlgo acaba tendo mais responsabilidades do que deveria, quebrando o Príncipio da Responsabilidade Única, pois a ação de mensurar é ortogonal em relação a tarefa executada pelo método.

Deveria haver um jeito de deixar a tarefa de mensuração menos invasiva, e além do mais, se tivermos que medir o tempo de vários métodos teremos lógica duplicada em vários pontos do sistema. Vamos desconsiderar o uso de Aspectos e ver como o core da linguagem pode nos fornecer uma solução. Com Java 8 e Lambdas podemos resolver esse problema de uma forma mais econômica, como mostra a Listagem 12.

Listagem 12. Método utilitário


  package exemplo10;
   
  import java.util.Random;
  import java.util.function.Supplier;
   
  final class Cronometro {
      public static <T> T cronometrar(Supplier<T> supplier) {
          long start = System.currentTimeMillis();        
          T result = supplier.get();
          long end = System.currentTimeMillis();
          System.out.println("Milisegundos = " + (end - start));        
          return result;
      }
  }
   
  public class Exemplo10 {
      
      public void fazerAlgo() {
          Cronometro.<Void>cronometrar( () -> {
              for (int i = 0; i < 100000 + new Random().nextInt(10000); i++) {}
              return null;    
          });        
      }    
      
      public static void main(String[] args) {
          new Exemplo10().fazerAlgo();
      }    
  }
  

No código criamos uma classe utilitária Cronometro com um método estático cronometrar, que recebe a interface funcional Supplier. Com isso podemos passar para esse método qualquer rotina, na forma:

() => { //tarefa; 
            return T; }

Onde T é o tipo de retorno gerado pela tarefa e no caso acima, a tarefa não retorna nada, logo o retorno é null e o tipo parametrizado T é Void.

No caso de retornar algo teremos o mesmo apresentado na Listagem 13.

Listagem 13. Método utilitário com retorno de dado


  package exemplo11;
   
  import java.util.Random;
  import java.util.function.Supplier;
   
  final class Cronometro {
      public static <T> T cronometrar(Supplier<T> supplier) {
          long start = System.currentTimeMillis();        
          T result = supplier.get();
          long end = System.currentTimeMillis();
          System.out.println("Milisegundos = " + (end - start));        
          return result;
      }
  }
   
  public class Exemplo11 {
      
      public long fazerAlgo() {
          return Cronometro.<Long>cronometrar( () -> {
              long soma = 0;
              for (int i = 0; i < 100000 + new Random().nextInt(10000); i++) {
                  soma += i;
              }
              return soma;    
          });        
      }    
      
      public static void main(String[] args) {
          long result = new Exemplo11().fazerAlgo();
          System.out.println("Soma = " + result);
      }    
  }
  

O código lida com uma tarefa que tem um valor de retorno.

Com a classe utilitária conseguimos eliminar a duplicação de código em todos os métodos que precisam de cronometragem. Porém, com o Java 8 e métodos default em interfaces, podemos prover outra solução, como a apresentada na Listagem 14.

Listagem 14. Intefarce como Mixin


  package exemplo12;
   
  import java.util.Random;
  import java.util.function.Supplier;
   
  interface Cronometro {
      default <T> T cronometrar(Supplier<T> supplier) {
          long start = System.currentTimeMillis();        
          T result = supplier.get();
          long end = System.currentTimeMillis();
          System.out.println("Milisegundos = " + (end - start));        
          return result;
      }
  }
   
  public class Exemplo12 implements Cronometro {
      
      public long fazerAlgo() {
          return cronometrar( () -> {
              long soma = 0;
              for (int i = 0; i < 100000 + new Random().nextInt(10000); i++) {
                  soma += i;
              }
              return soma;    
          });        
      }    
      
      public static void main(String[] args) {
          long result = new Exemplo12().fazerAlgo();
          System.out.println("Soma = " + result);
      }    
  }

O código faz algo muito interessante: ele “mixa” o comportamento de cronometragem na classe Exemplo12 através da interface.

Podemos considerar a interface Cronometro um Mixin, que é um tipo projetado para ser combinado com outros tipos e prover novas funcionalidades, funcionando de modo parecido com as Classes Abstratas, mas sem a limitação da herança simples, uma vez que uma classe pode implementar mais de uma interface.

Há algumas críticas em relação a existência de métodos estáticos no Java, pois se perde o benefício do polimorfismo.

No Scala, por exemplo, não existe o conceito de estático e nem primitivas, tudo é objeto, sendo que os métodos estáticos são substituídos por objetos singletons através do recurso “Companion Objects” ou mesmo isoladamente, atuando como fábrica de objetos ou locais para criar métodos utilitários.

E qual a vantagem do uso de interface em relação ao método estático? Podemos, por exemplo, sobrescrever o comportamento do método fornecendo outra implementação. Ao invés de milissegundos, poderíamos usar algo mais preciso: nanosegundos, como mostra a Listagem 15.

Listagem 15. Sobrecarregando


  package exemplo13;
   
  import java.util.Random;
  import java.util.function.Supplier;
   
  interface Cronometro {
      default <T> T cronometrar(Supplier<T> supplier) {
          long start = System.currentTimeMillis();        
          T result = supplier.get();
          long end = System.currentTimeMillis();
          System.out.println("Milisegundos = " + (end - start));        
          return result;
      }
  }
   
  public class Exemplo13 implements Cronometro {
      
      @Override
      public <Long> Long cronometrar(Supplier<Long> supplier) {
          long start = System.nanoTime();
          Long result = supplier.get();
          long end = System.nanoTime();
          System.out.println("Nanosegundos = " + (end - start));        
          return result;
      }    
      
      public long fazerAlgo() {
          return cronometrar( () -> {
              long soma = 0;
              for (int i = 0; i < 100000 + new Random().nextInt(10000); i++) {
                  soma += i;
              }
              return soma;    
          });        
      }    
      
      public static void main(String[] args) {
          long result = new Exemplo13().fazerAlgo();
          System.out.println("Soma = " + result);
      }    
  }

No código sobrescrevemos o comportamento default por outro que usa nanosegundos como unidade de medida.

Analisando o exemplo acima, podemos desenvolver um pouco mais o nosso modelo, como apresentado na Listagem 16.

Listagem 16. Definindo Comportamentos


  package exemplo14;
   
  import java.util.Random;
  import java.util.function.Supplier;
   
  interface Cronometro {
      <T> T cronometrar(Supplier<T> supplier);
  }
   
  // Nao faz cronometragem
  interface CronometroNulo<T> extends Cronometro {
      @Override
      default <T> T cronometrar(Supplier<T> supplier) {
          return supplier.get();
      }
  }
   
  interface CronometroMilisegundos extends Cronometro {
      @Override
      default <T> T cronometrar(Supplier<T> supplier) {
          long start = System.currentTimeMillis();        
          T result = supplier.get();
          long end = System.currentTimeMillis();
          System.out.println("Milisegundos = " + (end - start));        
          return result;
      }
  }
   
  interface CronometroNanosegundos extends Cronometro {
      @Override
      default <T> T cronometrar(Supplier<T> supplier) {
          long start = System.nanoTime();        
          T result = supplier.get();
          long end = System.nanoTime();
          System.out.println("Nanosegundos = " + (end - start));        
          return result;
      }
  }
   
  public class Exemplo14 implements CronometroNanosegundos {
              
      public long fazerAlgo() {
          return cronometrar( () -> {
              long soma = 0;
              for (int i = 0; i < 100000 + new Random().nextInt(10000); i++) {
                  soma += i;
              }
              return soma;    
          });        
      }    
      
      public static void main(String[] args) {
          long result = new Exemplo14().fazerAlgo();
          System.out.println("Soma = " + result);
      }    
  }

Agora podemos escolher estaticamente o comportamento que desejarmos: cronometragem em nanosegundos, em milissegundos ou não cronometrar.

ssa abordagem lembra um pouco a injeção de dependência, só que via código. Vamos refatorar o exemplo anterior para incluir injeção “estática” no estilo Spring, como vemos na Listagem 17.

Listagem 17. Injeção Estática


  package exemplo15;
   
  import java.util.Random;
  import java.util.function.Supplier;
   
  interface Cronometro {
      <T> T cronometrar(Supplier<T> supplier);
  }
   
  interface CronometroMilisegundos extends Cronometro {
      @Override
      default <T> T cronometrar(Supplier<T> supplier) {
          long start = System.currentTimeMillis();        
          T result = supplier.get();
          long end = System.currentTimeMillis();
          System.out.println("Milisegundos = " + (end - start));        
          return result;
      }
  }
   
  interface CronometroNanosegundos extends Cronometro {
      @Override
      default <T> T cronometrar(Supplier<T> supplier) {
          long start = System.nanoTime();        
          T result = supplier.get();
          long end = System.nanoTime();
          System.out.println("Nanosegundos = " + (end - start));        
          return result;
      }
  }
   
  interface CronometroComponente {
      Cronometro getCronometro();
  }
   
  interface FactoryCronometro extends CronometroComponente {
      @Override
      default Cronometro getCronometro() {
          return new CronometroNanosegundos() {};
      }
  }
   
  public class Exemplo15 implements CronometroComponente, FactoryCronometro {
      
      private final Cronometro crononometro = getCronometro();
              
      public long fazerAlgo() {
          return crononometro.cronometrar( () -> {
              long soma = 0;
              for (int i = 0; i < 100000 + new Random().nextInt(10000); i++) {
                  soma += i;
              }
              return soma;    
          });        
      }    
      
      public static void main(String[] args) {
          long result = new Exemplo15().fazerAlgo();
          System.out.println("Soma = " + result);
      }    
  }

No código apresentado o FactoryCronometro define a medição em nanosegundos através do método default e esse comportamento é mixado junto a classe Exemplo15. Poderíamos ter optado por qualquer uma das versões de cronometragem no método de fábrica.

No Scala podemos fazer injeção estática e tipada através do padrão Cake Pattern com Traits, que são recursos que podem ser consideradas como as interfaces do Java 8, mas além de comportamento podemos definir estado (atributos) também. Podemos dizer bem por cima que o Cake Pattern em Scala é razoavelmente parecido com o exemplo da Listagem 17. Mas as semelhanças param por aí, pois Scala fornece outros recursos para implementar o Cake Pattern de forma mais sólida (linearização, self-types, atributos, implicits, sobrecarga de atributos, etc.).

Então quer dizer que além de herança múltipla de comportamento podemos fazer injeção estática? Ou seja, podemos abandonar os contêineres de injeção de dependência e usar Mixin para emular herança múltipla?

Problemas

A resposta para as questões apresentadas é: em raras ocasiões as técnicas demonstradas podem ser usadas efetivamente em código real. Primeiro, o uso indiscriminado de métodos default pode tornar o código muito mais complexo e introduzir erros que não estamos acostumados a lidar, como colisões e ambiguidades.

A injeção de dependência estática é bem simplória e limitada e torna-se complicada de se gerenciar em projetos maiores (além do Java não ter muito dos recursos que facilitam o Cake Pattern no Scala). Mas mesmo no Scala há questionamentos em se usar o Cake Pattern em projetos grandes.

O fato de interfaces não poderem definir estado (atributos) também limita muito as situações onde podemos compor código reusável.

Mixin são mais adequados para prover comportamento adicional (como no exemplo do cronometro) e não criar relacionamentos “É UM”, que são muito comuns na Orientação a Objetos. Mixin são utilizados para prover ortogonalidade e separação de conceitos isolando comportamentos que são comuns em classes não relacionadas, mas em Java existe também os Aspectos para lidar com essa questão de forma menos intrusiva e poderosa.

O exemplo do cronômetro (ortogonal) é um dos poucos casos onde Mixin com interfaces do Java 8 pode ser uma alternativa razoável, pois implementar isso numa classe abstrata nem sempre é possível, pois a classe onde pretendemos “mixar” a cronometragem pode já possuir uma superclasse, e usar aspectos só para implementar essa funcionalidade pode não ser uma opção. Além do mais, usar interfaces ao invés de métodos estáticos favorece a aplicação de mocks para testes através de polimorfismo.

E é claro que não devemos esquecer o mantra: “favorecer composição sobre herança”. Essa regra também se aplica para as interfaces do Java 8 com métodos default.

Lembrem-se que a maior motivação para o surgimento de métodos default foi permitir a evolução da API do Java sem quebrar o código existente. Portanto, métodos default devem ser usados com extrema parcimônia no código do dia a dia. Mesmo assim, não deixam de ser um recurso interessante e intrigante.

Grato e até a próxima.

Links

Diamond-of-death
https://en.wikipedia.org/wiki/Multiple_inheritance

Pandas Tristes
http://zeroturnaround.com/rebellabs/how-your-addiction-to-java-8-default-methods-may-make-pandas-sad-and-your-teammates-angry/

Cake Pattern in Scala
http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di/

Brian Goetz
http://stackoverflow.com/questions/28681737/java-8-default-methods-as-traits-safe

Métodos Default
http://zeroturnaround.com/rebellabs/java-8-explained-default-methods/