1. Introdução
Em Java, não é possível herdar várias classes ao mesmo tempo. Isso é feito de propósito para evitar o “diamante da morte” — situação em que duas classes-pai definem o mesmo método e não fica claro qual implementação usar. Já interfaces você pode implementar quantas quiser! Por quê? Porque a interface é apenas um contrato; ela não contém implementação (até o Java 8 — sem implementação; a partir do Java 8 — são possíveis métodos default e static). Portanto, não há confusão com herança de código.
É parecido com a vida real: você pode ser simultaneamente “Motorista”, “Usuário de computador” e “Nadador”. Cada uma dessas “interfaces” descreve determinadas habilidades, mas não obriga você a ser uma cópia de outra pessoa.
Sintaxe da implementação múltipla de interfaces
Em Java, uma classe pode implementar várias interfaces, listando-as separadas por vírgula após a palavra-chave implements. Veja um exemplo básico:
public interface Movable {
void move(int x, int y);
}
public interface Chargeable {
void charge();
}
public class Robot implements Movable, Chargeable {
@Override
public void move(int x, int y) {
System.out.println("O robô se move para o ponto (" + x + ", " + y + ")");
}
@Override
public void charge() {
System.out.println("O robô está carregando.");
}
}
Neste exemplo, Robot é bastante versátil: ele se move e também carrega. Como na vida: quanto mais você sabe, mais frequentemente é chamado para entrevistas!
2. Para que serve? Exemplos práticos
Exemplo 1. Diferentes “papéis” de um objeto
Imagine que você está projetando um personagem de jogo:
- Ele pode se mover (Movable)
- Pode atacar (Attackable)
- Pode ser salvo em arquivo (Serializable — existe esse interface na biblioteca padrão do Java)
public interface Attackable {
void attack();
}
public class Hero implements Movable, Attackable, java.io.Serializable {
@Override
public void move(int x, int y) {
System.out.println("O herói se move para uma nova posição.");
}
@Override
public void attack() {
System.out.println("O herói ataca!");
}
}
Agora sua classe pode ser usada em diversos contextos: você pode passá-la para métodos que exijam qualquer uma dessas interfaces.
Exemplo 2. Combinando interfaces padrão
Com muita frequência, na biblioteca padrão do Java aparecem as interfaces Comparable (para comparar objetos) e Serializable (para salvar objetos em arquivo ou transmiti-los pela rede). Às vezes, é necessário que o objeto seja as duas coisas:
public class Person implements Comparable<Person>, java.io.Serializable {
private String name;
private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
Agora objetos Person podem ser ordenados (por exemplo, em uma lista) e gravados em arquivo.
3. Características e limitações
Uma implementação por método
Se duas interfaces definirem um método com a mesma assinatura, você precisa implementá-lo apenas uma vez. Exemplo:
public interface A {
void doSomething();
}
public interface B {
void doSomething();
}
public class MyClass implements A, B {
@Override
public void doSomething() {
System.out.println("Implementação de doSomething para ambas as interfaces.");
}
}
O Java não reclamará — o importante é que as assinaturas coincidam. Se os métodos diferirem pela assinatura, eles são considerados métodos distintos — é preciso implementar cada um.
Sem “diamante da morte”
Diferentemente da herança múltipla de classes, ao implementar várias interfaces não surge a situação de herdar duas implementações diferentes do mesmo método. Até o Java 8, não havia implementação em interfaces; e, com o surgimento dos métodos default, se houver conflito, você é obrigado a resolvê-lo explicitamente (mais detalhes na próxima aula).
Sem estado
Interfaces não podem conter campos comuns (apenas constantes — public static final). Portanto, não há confusão com “dois campos pai com o mesmo nome”.
4. Exemplo: implementando várias interfaces em uma única classe
Vamos adicionar novas funcionalidades ao nosso aplicativo de estudo (por exemplo, um zoológico). Suponha que temos animais que podem se mover e emitir som:
public interface Movable {
void move(int x, int y);
}
public interface Soundable {
void makeSound();
}
public class Dog implements Movable, Soundable {
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public void move(int x, int y) {
System.out.println(name + " corre para (" + x + ", " + y + ")");
}
@Override
public void makeSound() {
System.out.println(name + " diz: Au-au!");
}
}
public class Cat implements Movable, Soundable {
private String name;
public Cat(String name) {
this.name = name;
}
@Override
public void move(int x, int y) {
System.out.println(name + " se esgueira para (" + x + ", " + y + ")");
}
@Override
public void makeSound() {
System.out.println(name + " diz: Miau!");
}
}
Agora podemos escrever um método genérico para trabalhar com qualquer objeto “que se move” ou “que emite som”:
public static void testMovable(Movable m) {
m.move(10, 20);
}
public static void testSoundable(Soundable s) {
s.makeSound();
}
public static void main(String[] args) {
Dog rex = new Dog("Reks");
Cat murka = new Cat("Murka");
testMovable(rex); // Reks corre para (10, 20)
testSoundable(murka); // Murka diz: Miau!
}
E, claro, se um objeto implementa ambas as interfaces, ele pode ser passado para um e para outro!
6. Nuances úteis
E se as interfaces entrarem em conflito?
Às vezes acontece de duas interfaces definirem métodos com a mesma assinatura, mas com significados diferentes. Por exemplo, uma interface espera que o método reset() zere as coordenadas, enquanto outra espera que esse mesmo método desligue o dispositivo. Nesse caso, é preciso ter atenção: ainda assim você deve implementar o método apenas uma vez, e ele deve “saber” lidar com ambos os comportamentos (ou pelo menos escolher o que fazer). Na prática, tais situações são raras, mas, se ocorrerem, vale repensar o desenho do sistema.
Exemplo com uma coleção de objetos de diferentes interfaces
Suponha que temos uma lista de objetos que implementam interfaces diferentes. Podemos percorrê-los e chamar os métodos necessários:
Movable[] movables = {
new Dog("Sharik"),
new Cat("Barsik"),
new Robot()
};
for (Movable m : movables) {
m.move(0, 0);
}
De forma análoga, é possível fazer isso para qualquer interface.
7. Erros comuns ao implementar várias interfaces
Erro nº 1: nem todos os métodos das interfaces foram implementados.
Se a classe declarou que implementa uma interface, mas não implementou pelo menos um de seus métodos — o compilador emitirá um erro imediatamente. Não se esqueça de todos os métodos, mesmo que pareçam “supérfluos”.
Erro nº 2: métodos conflitantes com a mesma assinatura.
Se duas interfaces definem métodos iguais, você deve implementá-los apenas uma vez. Mas se o significado desses métodos for diferente, isso pode levar a confusão e bugs. Nesse caso, é melhor repensar a arquitetura.
Erro nº 3: tentativa de herdar interface com extends em uma classe.
Em uma classe, para implementar uma interface sempre se usa implements, e não extends. Por exemplo:
public class MyClass implements A, B { ... } // correto
public class MyClass extends A, B { ... } // erro!
Erro nº 4: tentativa de criar um objeto de uma interface.
Interface é um contrato; não é possível instanciá-la diretamente:
Movable m = new Movable(); // erro de compilação
Você só pode criar objetos de classes que implementam a interface.
GO TO FULL VERSION