Java Interfaces
Um pássaro voa. Um avião voa. Um drone voa. Os três não têm nada mais em comum: um é animal, outro é veículo, outro é eletrônico. Não faz sentido criar uma superclasse para os três. Mas seria ótimo poder dizer "me dê qualquer coisa que voe". Para isso existem as interfaces: um contrato que diz o que uma classe sabe fazer, sem dizer o que ela é. E, diferente da herança, uma classe pode assinar quantos contratos quiser.
- Como declarar uma
interfacee implementá-la comimplements. - Os erros de quem esquece um método ou o
public. - Como uma classe implementa várias interfaces (e herda de uma classe ao mesmo tempo).
- Métodos
defaultestaticem interfaces, e as constantes. - A diferença entre interface e classe abstrata, numa tabela.
- Como as interfaces da biblioteca, como
Comparable, funcionam do mesmo jeito.
Esta aula continua Classes Abstratas e usa Polimorfismo.
A ideia: uma tomada
A tomada da parede da Ana não sabe o que vai ser ligado nela: um abajur, um carregador, uma geladeira. Ela só exige um padrão: dois ou três pinos, naquele formato. Qualquer aparelho que siga o padrão funciona. O padrão da tomada é uma interface: um acordo entre quem usa (a parede) e quem fornece (o aparelho).
Em Java, uma interface é uma lista de métodos que uma classe promete ter. A palavra vem do inglês interface, "superfície de contato": o ponto onde duas partes se encontram. Quem implementa a interface diz "eu cumpro esse contrato"; quem usa só precisa conhecer o contrato.
interface e implements
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Voador> ceu = List.of(new Passaro(), new Aviao(), new Passaro());
for (Voador v : ceu) {
v.voar();
}
}
}
interface Voador {
void voar();
}
class Passaro implements Voador {
@Override
public void voar() {
System.out.println("O pássaro bate as asas.");
}
}
class Aviao implements Voador {
@Override
public void voar() {
System.out.println("O avião liga as turbinas.");
}
}
- interface Voador
- Declara uma interface. O nome costuma ser um adjetivo ou uma habilidade:
Voador,Comparable("comparável"),Runnable("executável"). - void voar();
- Um método do contrato: só a assinatura, com
;. Numa interface, ele é automaticamentepubliceabstract, sem você escrever. - class Passaro implements Voador
implements= "implementa": a classe assina o contrato e é obrigada a escrever todos os métodos dele.- public void voar()
- A implementação. O
publicé obrigatório (já já você vê por quê). O@Overridetambém vale aqui (veja Sobrescrita). - List<Voador>
- A interface também é um tipo: variáveis, listas e parâmetros podem ser do tipo
Voadore guardar qualquer objeto que a implemente.
$ java Main.java O pássaro bate as asas. O avião liga as turbinas. O pássaro bate as asas.
O laço não sabe se cada item é um pássaro ou um avião; só sabe que todos voam. É o mesmo despacho dinâmico da herança, mas agora entre classes que não são parentes.
Missão: crie a classe Drone, que implementa Voador e mostra O drone gira as hélices. no voar(). Acrescente um new Drone() à lista. O laço não muda. Teste também: apague o public do voar() do Drone e leia o erro.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Voador> ceu = List.of(new Passaro(), new Aviao());
for (Voador v : ceu) {
v.voar();
}
}
}
interface Voador {
void voar();
}
class Passaro implements Voador {
@Override
public void voar() {
System.out.println("O pássaro bate as asas.");
}
}
class Aviao implements Voador {
@Override
public void voar() {
System.out.println("O avião liga as turbinas.");
}
}
// crie aqui a classe Drone
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Voador> ceu = List.of(new Passaro(), new Aviao(), new Drone());
for (Voador v : ceu) {
v.voar();
}
}
}
interface Voador {
void voar();
}
class Passaro implements Voador {
@Override
public void voar() {
System.out.println("O pássaro bate as asas.");
}
}
class Aviao implements Voador {
@Override
public void voar() {
System.out.println("O avião liga as turbinas.");
}
}
class Drone implements Voador {
@Override
public void voar() {
System.out.println("O drone gira as hélices.");
}
}
As três regras que o compilador cobra
1. Implementar tudo. Assinou o contrato, cumpra. Um Aviao sem voar():
$ javac Main.java
Main.java:23: error: Aviao is not abstract and does not override abstract method voar() in Voador
class Aviao implements Voador {
^
1 error
É a mesma mensagem das classes abstratas: os métodos da interface são abstratos. (Uma classe abstract pode implementar a interface só em parte, deixando o resto para as filhas.)
2. Implementar como public. Os métodos da interface são public; a implementação não pode ser menos visível:
$ javac Main.java
Main.java:25: error: voar() in Aviao cannot implement voar() in Voador
void voar() {
^
attempting to assign weaker access privileges; was public
1 error
"Não pode implementar: tentativa de atribuir privilégios de acesso mais fracos; era public." Esse é o erro mais comum com interfaces, porque na interface você não escreveu public e é natural esquecer na classe.
3. Não instanciar a interface. Um contrato não é um aparelho:
$ javac Main.java
Main.java:5: error: Voador is abstract; cannot be instantiated
List<Voador> ceu = List.of(new Voador());
^
1 error
Várias interfaces ao mesmo tempo
Aqui está a grande diferença em relação à herança. Uma classe tem uma superclasse só (veja Herança), mas pode implementar quantas interfaces quiser, separadas por vírgula. Um pato é um animal, voa e nada:
public class Main {
public static void main(String[] args) {
Pato donald = new Pato("Donald");
donald.comer();
donald.voar();
donald.nadar();
donald.pousar();
System.out.println("Altitude máxima: " + Voador.ALTITUDE_MAXIMA + " m");
System.out.println(Voador.emKm(2500));
Voador v = donald;
Nadador n = donald;
v.voar();
n.nadar();
}
}
interface Voador {
int ALTITUDE_MAXIMA = 10000;
void voar();
default void pousar() {
System.out.println("Pousando com cuidado...");
}
static double emKm(double metros) {
return metros / 1000;
}
}
interface Nadador {
void nadar();
}
class Animal {
String nome;
Animal(String nome) {
this.nome = nome;
}
void comer() {
System.out.println(nome + " está comendo.");
}
}
class Pato extends Animal implements Voador, Nadador {
Pato(String nome) {
super(nome);
}
@Override
public void voar() {
System.out.println(nome + " voa baixinho.");
}
@Override
public void nadar() {
System.out.println(nome + " nada na lagoa.");
}
}
- extends Animal
- A superclasse (uma só). O
extendsvem sempre antes. - implements Voador, Nadador
- As interfaces, separadas por vírgula. O pato precisa implementar
voar()enadar(). - Voador v = donald;
- O mesmo objeto pode ser visto como
Pato,Animal,Voador,NadadorouObject. Cada tipo de variável libera os métodos daquele "papel".
$ java Main.java Donald está comendo. « herdado de Animal Donald voa baixinho. « contrato Voador Donald nada na lagoa. « contrato Nadador Pousando com cuidado... « método default, veio pronto da interface Altitude máxima: 10000 m « constante da interface 2.5 « método static da interface Donald voa baixinho. « pela variável Voador Donald nada na lagoa. « pela variável Nadador
As linhas 4 a 6 da saída usam três recursos novos do Voador: default, constante e static. Um de cada vez.
Métodos default: código pronto na interface
Java 8+ Um método default ("padrão") tem corpo, dentro da interface. Toda classe que implementa a interface ganha esse método pronto, e pode sobrescrevê-lo se quiser. No exemplo, o Pato não escreveu pousar() e mesmo assim tem.
interface Voador {
void voar();
default void pousar() {
System.out.println("Pousando com cuidado...");
}
}
Por que isso foi criado? Imagine uma interface usada por milhares de classes no mundo inteiro, como a List da biblioteca. Se os criadores do Java acrescentassem um método abstrato novo, todas essas classes parariam de compilar. Com default, o método novo vem com uma versão pronta, e nada quebra. Foi assim que List ganhou sort() e forEach() no Java 8.
Quando dois default se chocam
E se duas interfaces trouxerem um default com o mesmo nome? O "problema do diamante", que a herança simples evitava (veja Herança), volta. O Java não escolhe sozinho:
public class Main {
public static void main(String[] args) {
new Pato().descrever();
}
}
interface Voador {
default void descrever() {
System.out.println("Eu voo.");
}
}
interface Nadador {
default void descrever() {
System.out.println("Eu nado.");
}
}
class Pato implements Voador, Nadador {
}
$ javac Main.java
Main.java:19: error: types Voador and Nadador are incompatible;
class Pato implements Voador, Nadador {
^
class Pato inherits unrelated defaults for descrever() from types Voador and Nadador
1 error
"Os tipos Voador e Nadador são incompatíveis: a classe Pato herda defaults sem relação para descrever()." A solução é o Pato sobrescrever o método e decidir. Para chamar a versão de uma interface específica, a sintaxe é NomeDaInterface.super.metodo():
class Pato implements Voador, Nadador {
@Override
public void descrever() {
Voador.super.descrever();
Nadador.super.descrever();
System.out.println("Sou um pato!");
}
}
$ java Main.java Eu voo. Eu nado. Sou um pato!
Métodos static e constantes
Métodos static
Java 8+ Uma interface pode ter métodos static, chamados pelo nome dela: Voador.emKm(2500) devolveu 2.5. Eles servem para funções úteis ligadas ao assunto da interface. Exemplos da biblioteca: List.of(…), Comparator.comparing(…), Path.of(…).
Diferente dos métodos static de classe, eles não são herdados: só funcionam pelo nome da interface. donald.emKm(10) não compila (cannot find symbol … method emKm(int) … location: variable donald of type Pato).
Constantes
Todo atributo declarado numa interface é, automaticamente, public static final: uma constante. O int ALTITUDE_MAXIMA = 10000; não pode ser mudado:
$ javac Main.java
Main.java:10: error: cannot assign a value to static final variable ALTITUDE_MAXIMA
Voador.ALTITUDE_MAXIMA = 20000;
^
Main.java:11: error: cannot find symbol
donald.emKm(10);
^
symbol: method emKm(int)
location: variable donald of type Pato
2 errors
Interfaces não têm atributos de objeto: não existe "o nome de um Voador". Estado (dados que mudam) fica nas classes. Essa é uma das diferenças centrais em relação às classes abstratas.
Métodos private (Java 9+), para dividir o código dos default em partes; e uma interface pode herdar de outras com extends: interface Anfibio extends Voador, Nadador { } (entre interfaces, o extends aceita várias).
Interface × classe abstrata
| Interface | Classe abstrata | |
|---|---|---|
| Palavra para usar | implements | extends |
| Quantas uma classe pode ter | Várias | Uma |
| Atributos de objeto (estado) | Não (só constantes public static final) | Sim |
| Construtor | Não | Sim |
| Métodos sem corpo | Sim (automaticamente public abstract) | Sim (com abstract) |
| Métodos com corpo | default, static e private | Qualquer um |
| Visibilidade dos métodos | public (ou private, para ajudantes) | Qualquer uma |
| Pode ser instanciada? | Não | Não |
| Expressa | Uma habilidade, um papel ("sabe voar") | Uma família ("é uma forma") |
| Exemplos da biblioteca | List, Comparable, Runnable | AbstractList, Number, InputStream |
A recomendação geral é: comece por interfaces. Elas deixam as classes livres para herdar de quem quiserem e combinar papéis. Use uma classe abstrata quando houver de fato estado e código para compartilhar numa família.
As interfaces da biblioteca
A biblioteca do Java (veja A Biblioteca Padrão) é cheia de interfaces, e você já usa várias: List é uma interface, e ArrayList é uma classe que a implementa. Por isso se escreve List<String> nomes = new ArrayList<>();: a variável usa o contrato, o objeto é uma implementação.
Um exemplo clássico é a Comparable, a habilidade de "se comparar com outro". Quem a implementa ganha, de graça, a ordenação da biblioteca. A Ana quer ordenar os alunos pela nota:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Aluno> turma = new ArrayList<>();
turma.add(new Aluno("Carla", 7.5));
turma.add(new Aluno("Ana", 9.5));
turma.add(new Aluno("Bruno", 6.0));
Collections.sort(turma);
System.out.println(turma);
}
}
class Aluno implements Comparable<Aluno> {
String nome;
double nota;
Aluno(String nome, double nota) {
this.nome = nome;
this.nota = nota;
}
@Override
public int compareTo(Aluno outro) {
return Double.compare(this.nota, outro.nota);
}
@Override
public String toString() {
return nome + " " + nota;
}
}
$ java Main.java [Bruno 6.0, Carla 7.5, Ana 9.5] « da menor para a maior nota
O Collections.sort não conhece a classe Aluno; ele só sabe que ela cumpre o contrato Comparable e chama compareTo para comparar dois alunos. O <Aluno> diz com quem ele se compara (são os generics). Os detalhes do compareTo e do seu primo Comparator estão em Comparable e Comparator.
Outras interfaces que você vai encontrar: Runnable (uma tarefa para rodar, em Threads), Iterable (o que pode ir num for-each), AutoCloseable (o que pode ir num try-with-resources) e as interfaces funcionais, base das lambdas.
Quando dá errado
O que aconteceu: o método que implementa a interface ficou sem public.
Como resolver: acrescente public. Todo método que implementa uma interface é public.
O que aconteceu: faltou implementar um método do contrato (ou o nome/parâmetros estão diferentes).
Como resolver: escreva o método com a mesma assinatura da interface, com @Override e public. Os editores têm a opção "implementar métodos", que cria todos de uma vez.
O que aconteceu: duas interfaces trouxeram um default com a mesma assinatura.
Como resolver: sobrescreva o método na classe; dentro dele, escolha com Interface.super.metodo().
O que aconteceu: tentativa de mudar um atributo de interface. Todo atributo de interface é constante.
Como resolver: dados que mudam vão numa classe. Se você precisa de estado compartilhado por uma família, talvez a peça certa seja uma classe abstrata.
Referência
| Escrita | Significado |
|---|---|
interface Voador { void voar(); } | Declara o contrato; o método é public abstract automaticamente |
class Passaro implements Voador | A classe cumpre o contrato |
class Pato extends Animal implements Voador, Nadador | Uma superclasse e várias interfaces |
default void pousar() { … } Java 8+ | Método com corpo, herdado por quem implementa |
static double emKm(double m) { … } Java 8+ | Chamado só como Voador.emKm(…) |
int MAX = 10; (na interface) | Constante: public static final |
Voador.super.descrever() | Chama o default de uma interface específica |
interface A extends B, C | Interface que herda de outras |
Voador v = new Passaro(); | Variável do tipo da interface |
Exercícios
1. Assinar o contrato
Complete para a classe Gerente cumprir o contrato Pagavel:
2. O modificador obrigatório
A interface tem double valor();. Complete a implementação na classe:
3. Código pronto na interface
Complete para a interface ter um método com corpo, herdado por todas as classes que a implementam:
4. Quantas interfaces uma classe pode implementar?
5. Uma interface tem int LIMITE = 5;. O que esse atributo é?
6. Impressora, Celular e Fax não têm parentesco, mas todos sabem "imprimir". O que usar?
Missão final: a Ana quer somar todas as contas do mês, de tipos diferentes. O boleto já cumpre o contrato Pagavel; a conta de luz ainda não, e o código não compila. Faça ContaDeLuz implementar Pagavel, com valor() igual a kwh * 0.75. A saída deve terminar com Total: R$ 400.0 (e cada conta aparece com o resumo(), um método default).
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Pagavel> contas = List.of(new Boleto(250), new ContaDeLuz(200));
double total = 0;
for (Pagavel p : contas) {
System.out.println(p.resumo());
total = total + p.valor();
}
System.out.println("Total: R$ " + total);
}
}
interface Pagavel {
double valor();
default String resumo() {
return "Conta de R$ " + valor();
}
}
class Boleto implements Pagavel {
double total;
Boleto(double total) {
this.total = total;
}
@Override
public double valor() {
return total;
}
}
class ContaDeLuz {
double kwh;
ContaDeLuz(double kwh) {
this.kwh = kwh;
}
}
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Pagavel> contas = List.of(new Boleto(250), new ContaDeLuz(200));
double total = 0;
for (Pagavel p : contas) {
System.out.println(p.resumo());
total = total + p.valor();
}
System.out.println("Total: R$ " + total);
}
}
interface Pagavel {
double valor();
default String resumo() {
return "Conta de R$ " + valor();
}
}
class Boleto implements Pagavel {
double total;
Boleto(double total) {
this.total = total;
}
@Override
public double valor() {
return total;
}
}
class ContaDeLuz implements Pagavel {
double kwh;
ContaDeLuz(double kwh) {
this.kwh = kwh;
}
@Override
public double valor() {
return kwh * 0.75;
}
}
Resumo
- Uma
interfaceé um contrato: uma lista de métodos que uma classe promete ter. A classe assina comimplements. - Os métodos da interface são
public abstract; a classe precisa implementá-los todos, comopublic. - Uma classe tem uma superclasse, mas pode implementar várias interfaces:
class Pato extends Animal implements Voador, Nadador. default= método com corpo na interface;static= chamado pelo nome da interface; atributos = constantes.- A interface também é um tipo:
List<Voador>aceita qualquer coisa que voe. - Interface = habilidade (várias por classe, sem estado). Classe abstrata = família (uma por classe, com estado).
- A biblioteca usa interfaces o tempo todo:
List,Comparable,Runnable.