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.

Você vai aprender
  • Como declarar uma interface e implementá-la com implements.
  • 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 default e static em 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 é automaticamente public e abstract, 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 @Override também vale aqui (veja Sobrescrita).
List<Voador>
A interface também é um tipo: variáveis, listas e parâmetros podem ser do tipo Voador e 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 extends vem sempre antes.
implements Voador, Nadador
As interfaces, separadas por vírgula. O pato precisa implementar voar() e nadar().
Voador v = donald;
O mesmo objeto pode ser visto como Pato, Animal, Voador, Nadador ou Object. 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:

Este código tem um erro de propósito
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.

O que mais pode haver numa interface

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

InterfaceClasse abstrata
Palavra para usarimplementsextends
Quantas uma classe pode terVáriasUma
Atributos de objeto (estado)Não (só constantes public static final)Sim
ConstrutorNãoSim
Métodos sem corpoSim (automaticamente public abstract)Sim (com abstract)
Métodos com corpodefault, static e privateQualquer um
Visibilidade dos métodospublic (ou private, para ajudantes)Qualquer uma
Pode ser instanciada?NãoNão
ExpressaUma habilidade, um papel ("sabe voar")Uma família ("é uma forma")
Exemplos da bibliotecaList, Comparable, RunnableAbstractList, 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

"… cannot implement … attempting to assign weaker access privileges; was public"

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.

"… is not abstract and does not override abstract method … in …"

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.

"… inherits unrelated defaults for … from types … and …"

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().

"cannot assign a value to static final variable …"

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

EscritaSignificado
interface Voador { void voar(); }Declara o contrato; o método é public abstract automaticamente
class Passaro implements VoadorA classe cumpre o contrato
class Pato extends Animal implements Voador, NadadorUma 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, CInterface 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:

class Gerente { … }

2. O modificador obrigatório

A interface tem double valor();. Complete a implementação na classe:

{ return 100; }

3. Código pronto na interface

Complete para a interface ter um método com corpo, herdado por todas as classes que a implementam:

void mostrar() { System.out.println(valor()); }

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 com implements.
  • Os métodos da interface são public abstract; a classe precisa implementá-los todos, como public.
  • 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.