Desafio Java

Interface vs classe abstrata e herança múltipla de tipo

Enunciado

Modele um sistema com a classe abstrata Animal (com atributo nome e método abstrato emitirSom()) e as interfaces Corredor (método correr()) e Nadador (método nadar()). Crie as classes Cachorro (herda Animal, implementa Corredor e Nadador) e Peixe (herda Animal, implementa apenas Nadador). No main, crie um array de Animal, percorra-o, chame emitirSom() e, usando instanceof com pattern matching, chame os métodos específicos quando aplicável.

Requisitos

  • Animal deve ser uma classe abstrata com atributo nome, construtor e método abstrato emitirSom().
  • Corredor e Nadador devem ser interfaces com os métodos correr() e nadar(), respectivamente.
  • Cachorro deve herdar de Animal e implementar Corredor e Nadador; Peixe deve herdar de Animal e implementar apenas Nadador.
  • O main deve percorrer um array de Animal e usar instanceof com pattern matching para chamar métodos específicos, imprimindo mensagens claras.

Código inicial

abstract class Animal {
    protected String nome;

    Animal(String nome) {
        this.nome = nome;
    }

    abstract void emitirSom();
}

interface Corredor {
    void correr();
}

interface Nadador {
    void nadar();
}

// TODO: implemente Cachorro e Peixe

public class Main {
    public static void main(String[] args) {
        // TODO: crie o array e percorra
    }
}

Saída esperada

Cachorro Rex faz: Au au!
Rex esta correndo.
Rex esta nadando.
Peixe Nemo faz: Glub glub!
Nemo esta nadando.
Ver dica

Use instanceof Corredor c para verificar e já obter a variável tipada. Lembre-se de que a ordem das verificações pode importar se uma classe implementar múltiplas interfaces.

Mostrar solução
abstract class Animal {
    protected String nome;

    Animal(String nome) {
        this.nome = nome;
    }

    abstract void emitirSom();
}

interface Corredor {
    void correr();
}

interface Nadador {
    void nadar();
}

class Cachorro extends Animal implements Corredor, Nadador {
    Cachorro(String nome) {
        super(nome);
    }

    @Override
    void emitirSom() {
        System.out.println("Cachorro " + nome + " faz: Au au!");
    }

    @Override
    public void correr() {
        System.out.println(nome + " esta correndo.");
    }

    @Override
    public void nadar() {
        System.out.println(nome + " esta nadando.");
    }
}

class Peixe extends Animal implements Nadador {
    Peixe(String nome) {
        super(nome);
    }

    @Override
    void emitirSom() {
        System.out.println("Peixe " + nome + " faz: Glub glub!");
    }

    @Override
    public void nadar() {
        System.out.println(nome + " esta nadando.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal[] animais = { new Cachorro("Rex"), new Peixe("Nemo") };

        for (Animal animal : animais) {
            animal.emitirSom(); // método abstrato implementado

            // Pattern matching para interfaces
            if (animal instanceof Corredor c) {
                c.correr();
            }
            if (animal instanceof Nadador n) {
                n.nadar();
            }
        }
    }
}

Passo a passo

  1. A classe abstrata Animal define o estado comum nome e o método abstrato emitirSom(). Construtor inicializa o nome.
  2. As interfaces Corredor e Nadador definem contratos separados para habilidades específicas.
  3. Cachorro estende Animal (herança de código e estado) e implementa Corredor e Nadador (herança múltipla de tipo). Implementa todos os métodos.
  4. Peixe estende Animal e implementa apenas Nadador, pois peixes não correm.
  5. No main, criamos um array de Animal com instâncias de Cachorro e Peixe. O polimorfismo permite tratá-los como Animal.
  6. No loop, chamamos emitirSom(); cada objeto executa sua versão.
  7. Usamos instanceof Corredor c e instanceof Nadador n para verificar se o objeto implementa a interface e, em caso positivo, chamar o método correspondente. O pattern matching já cria a variável tipada.

Por que funciona

A classe abstrata fornece a base comum (nome e som), enquanto as interfaces permitem que classes adquiram capacidades adicionais sem se prender a uma única hierarquia. Cachorro pode ser um Animal, um Corredor e um Nadador ao mesmo tempo. O instanceof com pattern matching permite verificar e usar essas capacidades de forma segura e concisa.

Erros comuns

  • Tentar herdar de múltiplas classes: class Cachorro extends Animal, Corredor não compila; Java só permite uma superclasse. Use implements para interfaces.
  • Esquecer de implementar todos os métodos abstratos: se Peixe não implementar nadar(), o compilador exige que a classe seja abstrata.
  • Usar instanceof sem pattern matching e depois fazer cast manual desnecessário: if (animal instanceof Corredor) { ((Corredor) animal).correr(); } funciona, mas é mais verboso e propenso a erros. Prefira instanceof Corredor c.
  • Ordem incorreta de verificações: se uma classe implementa várias interfaces, verifique todas as que deseja chamar, não use else if a menos que queira apenas a primeira.

Outra forma de resolver

Poderia usar uma única interface Animal com métodos default para correr e nadar, mas isso forçaria todas as classes a ter esses métodos, mesmo que não façam sentido. A combinação de classe abstrata com interfaces segregadas é mais limpa e segue o princípio da segregação de interfaces.

Saída esperada

Cachorro Rex faz: Au au!
Rex esta correndo.
Rex esta nadando.
Peixe Nemo faz: Glub glub!
Nemo esta nadando.