Introdução aos Default Methods do Java 8

Veremos neste artigo um novo recurso do Java 8 que permite economizar tempo, simplificar a implementação e evitar a cópia direta de códigos que necessariamente se repetem no projeto.

Na Programação Orientada a Objetos em Java, quando utilizamos interfaces, é obrigatório adicionar os métodos da mesma em cada classe que a implemente. Mesmo que um deles não precise executar nada, é necessário manter ao menos a sua assinatura na classe. Respeitando o contrato, chega um momento em que os métodos implementados da interface em várias classes tornam-se idênticos e repetitivos, levando o desenvolvedor a copiar e colar código para ganhar tempo de codificação, replicando os métodos iguais de uma classe para outra.

Até o Java 7, a melhor solução para não cair nessa prática era o uso de padrões de projetos, como o Strategy, que permitem reaproveitar métodos já implementados. Pensando nesse problema, na versão 8 da linguagem foi introduzido o recurso Default Methods. Com ele, deixa de ser necessário implementar todos os métodos da interface, o que possibilita redução no tempo gasto com o desenvolvimento e simplifica o uso desse tipo de estrutura.

Opções antes dos Default Methods

Para evitar lidar com a necessidade de implementar todos os métodos de uma interface em todas as classes que a estendem, os desenvolvedores geralmente adotam uma das seguintes “técnicas”:

  1. Copiar e colar;
  2. Desistir de utilizar interfaces e utilizar classes abstratas;
  3. Usar o padrão de projeto Strategy.

Nas Listagens 1, 2 e 3 veremos como cada uma dessas opções se aplica a um sistema desenvolvido para uma autoescola, o qual consiste de um simulador que pode conter vários tipos de veículos para o aluno selecionar, por exemplo, quando desejar fazer uma aula.

Nos códigos que serão analisados, trabalharemos com três opções de veículos, representadas por três classes. Dessas, duas são idênticas, fato que permite a má prática de copiar e colar código.

Listagem 1. Exemplo de classes com código dos métodos copiado

public interface IAceleracao {
   void acelerar();
   void desacelerar();
}

public abstract class Veiculo implements IAceleracao{
   Motor motor = new Motor();
}

public class Gol extends Veiculo {
   @Override
   public void acelerar() {
      System.out.println("Aceleração Constante");         
   }

   @Override
   public void desacelerar() {
      System.out.println("Desaceleração Constante");             
   }
}

public class Fiesta extends Veiculo {
   @Override
   public void acelerar() {
      System.out.println("Aceleração Constante");         
   }

   @Override
   public void desacelerar() {
      System.out.println("Desaceleração Constante");             
   }
}

public class Maverick extends Veiculo {
   public Maverick()
   {
      motor = new Motor("V8");
   }

   @Override
   public void acelerar() {
             System.out.println("Aceleração Super Rápida");
   }

   @Override
   public void desacelerar() {
         System.out.println("Desaceleração Lenta");
   }
}
      

Apesar de muitas vezes esse se mostrar o caminho mais rápido, copiar e colar código é considerado uma das piores práticas de desenvolvimento, pois se torna um multiplicador de erros e vai de encontro a um dos objetivos básicos da Orientação a Objetos: o reuso.

Como alternativa a essa prática, a Listagem 2 apresenta uma segunda solução para a implementação do mesmo simulador: desistir do uso de interface e aplicar apenas herança. Nesse novo modelo deixamos os métodos com implementação padrão na superclasse e sobrescrevemos as variações dele somente naquelas que precisarem.

Listagem 2. Exemplo de implementação sem o uso de interfaces

public abstract class Veiculo{
   Motor motor = new Motor();

   public void acelerar(){
      System.out.println("Aceleração Constante");
   }

   public void desacelerar(){
      System.out.println("Desaceleração Constante");
   }
}

public class Gol extends Veiculo { }

public class Fiesta extends Veiculo { }

public class Maverick extends Veiculo {
   public Maverick(){
      motor = new Motor("V8");
   }

   @Override
   public void acelerar() {
      System.out.println("Aceleração Super Rápida");
   }

   @Override
   public void desacelerar() {
         System.out.println("Desaceleração Lenta");
   }
} 
      

Nesse exemplo, embora pareça que tenhamos resolvido o problema e o código tenha ficado mais limpo e de fácil manutenção, a solução perdeu em flexibilidade, em relação à opção anterior (com interfaces). Além disso, apesar de nesse caso ter funcionado, em outras situações se torna inviável resolver o problema apenas com herança. Por exemplo, se houvesse a necessidade de aumentar o número de classes que herdam de Veiculo, contendo cada uma implementações diferentes para os métodos, o uso de polimorfismo em relação aos métodos seria frequente, aumentando proporcionalmente a complexidade e a manutenção do sistema. Isso acaba prejudicando também a aplicação de boas práticas da Programação Orientação a Objetos, que presam por reusabilidade e fraco acoplamento entre objetos (o que se obtém pelo uso correto de interfaces).

A próxima alternativa, e que se mostrará a melhor opção até aqui, se baseia na utilização de padrões de projeto. Observe na Listagem 3 a solução proposta para o mesmo simulador, mas dessa vez aplicando o design pattern Strategy.

Listagem 3. Exemplo de implementação utilizando o padrão de projeto Strategy

public interface IAceleracao {
   void acelerar();
   void desacelerar();
}

public class AceleracaoMotorComum implements IAceleracao {
   @Override
   public void acelerar() {
      System.out.println("Aceleração Constante");
   }

   @Override
   public void desacelerar() {
      System.out.println("Desaceleração Constante");
   }
}

public class AceleracaoMotorV8 implements IAceleracao{
   @Override
   public void acelerar() {
      System.out.println("Aceleração Super Rápida");
   }

   @Override
   public void desacelerar() {
      System.out.println("Desaceleração Lenta");
   }
}

public abstract class Veiculo{
   Motor motor = new Motor();
   IAceleracao acelerador = new AceleracaoMotorComum();
   
   public void acelerar(){
      acelerador.acelerar();
   }

   public void desacelerar(){
      acelerador.desacelerar();
   }
}

public class Gol extends Veiculo {}

public class Fiesta extends Veiculo {}

public class Maverick extends Veiculo {
   public Maverick()
   {
      acelerador = new AceleracaoMotorV8();
      motor = new Motor("V8");
   }
}
      

Essa é considerada uma solução eficiente, flexível e que aplica corretamente as práticas da Orientação a Objetos, sendo útil principalmente em situações em que há um número considerável de classes que implementam a interface, já que permite a reusabilidade de código para executar comportamentos semelhantes. Porém, ela se torna pouco eficaz quando utilizada em um contexto onde há poucas classes, já que gera um código extenso. Nesse caso, o esforço de desenvolvimento despendido não terá valido a pena, pois o uso da estrutura gerada será mínimo.

Solução com Default Methods

Com a introdução desse recurso na linguagem Java, obteve-se uma solução simples e que reduz bastante o esforço de programação, simplificando o código ao possibilitar a criação de interfaces que já possuam uma implementação padrão para alguns de seus métodos (mesmo que sejam para lançar apenas uma exceção, por exemplo). Assim, quando uma classe implementar uma interface, mas não sobrescrever seus métodos, será utilizado exatamente o código que foi definido como implementação padrão na interface. Na Listagem 4 apresentamos o mesmo exemplo do simulador, agora com Default Methods.

Listagem 4. Exemplo de implementação utilizando Default Methods

public interface IAceleracao{
   default void acelerar(){
      System.out.println("Aceleração Constante");
   }
   
   default void desacelerar(){
      System.out.println("Desaceleração Constante");
   }
}

public abstract class Veiculo implements IAceleracao{
   Motor motor = new Motor();
}

public class Maverick extends Veiculo {
   public Maverick(){
      motor = new Motor("V8");
   }
   
   @Override
   public void acelerar() {
         System.out.println("Aceleração Super Rápida");
   }

   @Override
   public void desacelerar() {
         System.out.println("Desaceleração Lenta");
   }
}

public class Gol extends Veiculo {}

public class Fiesta extends Veiculo {} 
      

Nesse exemplo é perceptível a facilidade obtida ao acrescentar a cláusula default antes da assinatura dos métodos da interface. Isso possibilita acrescentar uma rotina padrão que normalmente só seria definida nas classes que a implementassem. Esse código padrão será executado sempre que o compilador verificar, na classe, que esses métodos não foram implementados.

Artigos relacionados