Métodos públicos e abstratos em Interfaces
Imagine que temos a interface Aquatico
, que define um método move
:
interface Aquatico { public abstract void move(); }
Se tivermos uma classe Navio
que implementa essa interface mas não fornece uma implementação para o método move
, teremos um erro de compilação:
class Navio implements Aquatico { ^ error: Navio is not abstract and does not override abstract method move() in Aquatico }
Uma interface nos obriga a respeitar um contrato, ou seja, a assinatura (nome, parâmetros e retorno) de um ou mais métodos.
No Java, desde os primórdios, os métodos das interfaces são implicitamente public
e abstract
. Essa última palavra-chave indica que são métodos abstratos, ou seja, sem corpo definido e sem código, meros moldes a serem preenchidos.
Para o código da classe concreta Navio
ser compilado com sucesso, temos que fornecer uma implementação para o método move
. Por exemplo:
class Navio implements Aquatico { public void move() { System.out.println("Navega na água!"); } }
O código acima compila normalmente e podemos invocá-lo de maneira polimórfica, como a seguir:
public class Programa { public static void main(String[] args){ Aquatico titanic = new Navio(); titanic.move(); //imprime "Navega na água!" } }
Métodos com mesma assinatura em Interfaces diferentes
Considere que temos uma interface Terrestre
, conforme abaixo:
interface Terrestre { public abstract void move(); }
Poderíamos criar um classe Caminhao
que implementa a interface Terrestre
, de maneira semelhante ao que fizemos acima:
class Caminhao implements Terrestre { public void move() { System.out.println("Move na longa estrada da vida!"); } }
Para utilizar a classe Caminhao
, faríamos de maneira parecida com a anterior:
public class Programa { public static void main(String[] args){ Caminhao scania = new Caminhao(); scania.move(); //imprime "Move na longa estrada da vida!" } }
Mas o que acontece se tivermos uma classe que representa um Hovercraft, um veículo anfíbio, que pode se deslocar tanto na água como na terra?
Será que podemos implementar tanto a interface Aquatico
como a interface Terrestre
simultaneamente?
Claro que sim, já que a interface define apenas a assinatura (ou seja, nome, parâmetros e retorno) do método move
e não uma implementação.
class Hovercraft implements Aquatico, Terrestre { public void move(){ System.out.println("Move na água e na terra!"); } }
Não há problema algum, porque a implementação do método move
na classe Hovercraft
é a mesma tanto para Aquatico
como para Terrestre
.
Podemos usar a classe Hovercraft
conforme a seguir:
public class Programa { public static void main(String[] args){ Hovercraft srn4 = new Hovercraft(); srn4.move(); //imprime "Move na terra e na água!" } }
Métodos default em Interfaces no Java 8
Conforme vimos em um post anterior, no Java 8 agora é possível definir, além de contratos, implementações de métodos em interfaces.
Esse novo mecanismo é chamado de métodos default e, com ele, é possível evoluir interfaces antigas como a Iterable
, que agora possui um método forEach
, por exemplo.
No exemplo acima, poderíamos definir o comportamento de navegar na água diretamente na interface Aquatico
, utilizando a palavra reservada default
:
interface Aquatico { public default void move() { System.out.println("Navega na água!"); } }
Métodos default só podem ser definidos em interfaces e são implicitamente públicos e, é claro, não-abstratos porque possuem um corpo definido, com código.
A classe Navio
já não precisará fornercer uma implementação para o método move
:
class Navio implements Aquatico { }
A interface Terrestre
também pode ter uma implementação do método move
com um método default:
interface Terrestre { public default void move() { System.out.println("Move na longa estrada da vida!"); } }
A classe Caminhao
também seria simplificada:
class Caminhao implements Terrestre { }
O programa que utiliza as classes Navio
e Caminhao
, bem como suas respectivas interfaces, não precisa ser alterado:
public class Programa { public static void main(String[] args){ Aquatico titanic = new Navio(); titanic.move(); //imprime "Navega na água!" Caminhao scania = new Caminhao(); scania.move(); //imprime "Move na longa estrada da vida!" } }
Colisão de métodos default
Mas o que será que acontece com a nossa classe Hovercraft
, que implementa tanto Aquatico
como Terrestre
?
class Hovercraft implements Aquatico, Terrestre { ^ error: class Hovercraft inherits unrelated defaults for move() from types Aquatico and Terrestre
Observe que, para a classe Hovercraft
, o código não pode ser compilado. Como com um método default não apenas define uma assinatura mas também uma implementação, há ambiguidade na definição do método move
. O compilador não pode definir qual das duas implementações deve ser escolhida e, por isso, nos avisa dessa colisão com um erro de compilação.
Para fazermos a classe Hovercraft
compilar com sucesso, temos que definir uma implementação para o método move
, evitando qualquer ambiguidade:
class Hovercraft implements Aquatico, Terrestre { public void move() { System.out.println("Move na terra e na água!"); } }
Uma alternativa é invocar um método default de uma interface a partir de uma classe que a implementa, utilizando o nome da interface seguido de super
e do nome do método default. Por exemplo:
class Hovercraft implements Aquatico, Terrestre { public void move() { Aquatico.super.move(); Terrestre.super.move(); System.out.println("Move na terra e na água!"); } }
Se utilizarmos a classe Hovercraft
acima em um programa, teremos algo como:
public class Programa { public static void main(String[] args){ Hovercraft srn4 = new Hovercraft(); srn4.move(); // imprime // "Navega na água!" // "Move na longa estrada da vida!" // "Move na terra e na água!" } }
Precedência de métodos da superclasse
Suponha que tenhamos uma interface Aereo
, com um método default chamado move
:
interface Aereo { public default void move() { System.out.println("Move no ar!"); } }
Também temos uma classe Aviao
que implementa a interface Aereo
, porém sobreescrevendo o método move
:
class Aviao implements Aereo { public void move(){ System.out.println("Voa..."); } }
Se criarmos um objeto da classe Aviao
, qual método será chamado: o método sobreescrito na classe ou o método default da interface?
Será escolhido o método sobreescrito na classe Aviao
, funcionando da mesma maneira que sobreescrita de métodos para classes:
public class Programa { public static void main(String[] args){ Aviao boeing747 = new Aviao(); boeing747.move(); //imprime "Voa..." } }
Agora, se quisermos representar um Hidroavião, um tipo de avião que pode decolar e pousar na água, teremos uma classe semelhante a seguinte:
class HidroAviao extends Aviao implements Aquatico { }
A classe HidroAviao
é um Aviao
que implementa a interface Aquatico
.
Mas o que será que acontece ao invocarmos o método move
de uma instância de HidroAviao
? Será que é invocado a implementação da superclasse Aviao
ou da interface Aquatico
?
A implementação escolhida será a da superclasse:
public class Programa { public static void main(String[] args){ HidroAviao curtissNC = new HidroAviao(); curtissNC.move(); //imprime "Voa..." } }
Uma implementação definida em uma superclasse sempre tem precedência sobre uma implementação definida em um método default de uma interface.
É importante lembrar que, em Java, uma classe só pode ter uma superclasse.
Resumindo
As regras do uso de métodos default são bem claras:
- Colisão de interfaces – se uma classe implementa duas interfaces que possuem métodos default com a mesma assinatura, o método deve ser sobreescrito
- Precedência de superclasses – se uma superclasse provê uma implementação de um método que tem a mesma assinatura de um método default de uma interface, a implementação da superclasse tem precedência
Post muito didático! Fizeram um mecanismo bem interessante para esses métodos default. Agora o céu é o limite pra evolução da API do Java 😀