CodeGym /Cursos /JAVA 25 SELF /Problemas de polimorfismo e abstrações

Problemas de polimorfismo e abstrações

JAVA 25 SELF
Nível 23 , Lição 3
Disponível

1. Polimorfismo: o que é e para que serve

Se você acha que polimorfismo é algo do mundo dos mutantes da Marvel, lamento desapontar: em programação tudo é bem mais tranquilo — mas não menos mágico. Polimorfismo é a capacidade de objetos com implementações diferentes responderem de maneiras distintas às mesmas chamadas de métodos.

Exemplo do dia a dia:
Você tem a classe Book e a classe Magazine, ambas herdando da classe abstrata LibraryItem. Você quer poder chamar o método printInfo() para qualquer item da biblioteca e que ele exiba as informações certas — para livro, autor e título; para revista, número da edição e data.

Exemplo de código:

abstract class LibraryItem {
    String title;

    LibraryItem(String title) {
        this.title = title;
    }

    abstract void printInfo();
}

class Book extends LibraryItem {
    String author;

    Book(String title, String author) {
        super(title);
        this.author = author;
    }

    @Override
    void printInfo() {
        System.out.println("Livro: " + title + ", autor: " + author);
    }
}

class Magazine extends LibraryItem {
    int issueNumber;

    Magazine(String title, int issueNumber) {
        super(title);
        this.issueNumber = issueNumber;
    }

    @Override
    void printInfo() {
        System.out.println("Revista: " + title + ", edição: " + issueNumber);
    }
}

Agora é possível criar um array com itens diferentes e chamar printInfo() para cada um:

LibraryItem[] items = {
    new Book("O Senhor das Moscas", "William Golding"),
    new Magazine("Ciência e Vida", 5)
};

for (LibraryItem item : items) {
    item.printInfo();
}
// Será impresso:
// Livro: O Senhor das Moscas, autor: William Golding
// Revista: Ciência e Vida, edição: 5

É assim que o polimorfismo funciona!

2. Erros comuns com polimorfismo

Tentar chamar métodos que não existem no tipo base

Um dos erros mais comuns é tentar acessar um método que só foi declarado na classe filha por meio de uma referência do tipo base.

LibraryItem item = new Book("Harry Potter", "J. K. Rowling");
// item.getAuthor(); // Erro de compilação! Em LibraryItem não existe o método getAuthor()

O Java compila o código com base no que vê no tipo da variável (LibraryItem), não no objeto real (Book). Portanto, se você precisa chamar um método específico de livro, é necessário fazer o cast:

if (item instanceof Book) {
    Book book = (Book) item;
    // Agora é possível chamar book.getAuthor()
}

Conversão de tipo sem verificação

Se você tem certeza de que o objeto é um Book, mas na verdade não é, terá um ClassCastException em tempo de execução. Por exemplo:

LibraryItem item = new Magazine("Forbes", 12);
Book book = (Book) item; // BUM! ClassCastException

A forma correta — sempre verificar o tipo:

if (item instanceof Book) {
    Book book = (Book) item;
    // OK
} else {
    System.out.println("Isto não é um livro!");
}

Não aproveitar as vantagens do polimorfismo

Às vezes, desenvolvedores escrevem código de forma que ele fique rigidamente acoplado a tipos específicos, quando poderiam usar abstrações. Por exemplo, se você escreve:

Book[] books = ...;
for (Book book : books) {
    book.printInfo();
}

Isso só funciona para livros. E se amanhã surgirem revistas, jornais, quadrinhos? É melhor usar um array de LibraryItem[] e trabalhar com métodos da classe base ou da interface.

3. Abstrações: para que servem e como não errar

Classes abstratas e interfaces

Abstração é a arte de destacar o essencial e esconder os detalhes. Em Java, para isso existem classes abstratas e interfaces.

  • Classe abstrata — é uma classe que não pode ser instanciada diretamente, apenas estendida.
  • Interface — é um contrato: o que a classe deve fazer, não como ela faz.

Erro 1: Criar uma classe abstrata sem métodos abstratos

Se na sua classe abstrata não há nenhum método abstrato, pense bem — será que ela realmente precisa ser abstrata? Talvez seja mais simples fazer uma classe comum.

abstract class UselessAbstract {
    void sayHello() {
        System.out.println("Hello!");
    }
}
// É melhor fazer uma classe comum se não houver métodos abstratos

Erro 2: Ausência de implementação de métodos obrigatórios nas classes filhas

Se uma classe herda de uma classe abstrata ou implementa uma interface, ela é obrigada a implementar todos os métodos abstratos. Se esquecer — o compilador vai avisar, mas às vezes os métodos são implementados “para constar” e não fazem nada. Isso é ruim para a manutenção do código.

class Magazine extends LibraryItem {
    Magazine(String title, int issueNumber) {
        super(title);
        // ...
    }

    @Override
    void printInfo() {
        // Vazio! Ruim!
    }
}

Erro 3: Hierarquia de abstrações muito profunda ou confusa

Quando classes herdam umas das outras em cinco a dez níveis, torna-se muito difícil entendê-las. É melhor criar hierarquias “planas”, onde tudo é claro.

Exemplo ruim:

LibraryItem
  |
  BookItem
    |
    PrintedBook
      |
      IllustratedBook
        |
        ChildrenIllustratedBook

Complicado, não é? Melhor limitar-se a dois ou três níveis.

4. Prática: aplicando polimorfismo e abstrações no aplicativo didático

Vamos evoluir seu aplicativo didático para biblioteca. Antes, você tinha apenas livros. Agora vamos adicionar revistas e implementar uma interface comum para publicações impressas.

Vamos declarar a classe abstrata:

abstract class LibraryItem {
    protected String title;

    public LibraryItem(String title) {
        this.title = title;
    }

    public abstract void printInfo();
}

Vamos adicionar classes filhas:

class Book extends LibraryItem {
    private String author;

    public Book(String title, String author) {
        super(title);
        this.author = author;
    }

    @Override
    public void printInfo() {
        System.out.println("Livro: " + title + ", autor: " + author);
    }
}

class Magazine extends LibraryItem {
    private int issueNumber;

    public Magazine(String title, int issueNumber) {
        super(title);
        this.issueNumber = issueNumber;
    }

    @Override
    public void printInfo() {
        System.out.println("Revista: " + title + ", edição: " + issueNumber);
    }
}

Usando polimorfismo:

LibraryItem[] items = {
    new Book("Código Limpo", "Robert Martin"),
    new Magazine("Java World", 3)
};

for (LibraryItem item : items) {
    item.printInfo();
}

Adicionar uma interface para publicações eletrônicas

Suponha que algumas publicações possam ser lidas online. Vamos introduzir uma interface:

interface ReadableOnline {
    void openOnline();
}

class EBook extends Book implements ReadableOnline {
    private String url;

    public EBook(String title, String author, String url) {
        super(title, author);
        this.url = url;
    }

    @Override
    public void openOnline() {
        System.out.println("Abrindo o livro eletrônico no endereço: " + url);
    }
}

Agora é possível trabalhar com livros eletrônicos por meio da interface:

ReadableOnline ebook = new EBook("Java para Leigos", "Barry Burd", "https://example.com/java");
ebook.openOnline();

5. Como evitar problemas com polimorfismo e abstrações: boas práticas

  • Use interfaces e classes abstratas para descrever comportamento, não estado.
    Por exemplo, a interface Printable descreve bem a capacidade de “imprimir”, mas manter no interface um campo String title já é uma má ideia.
  • Verifique o tipo do objeto com instanceof antes de converter.
    Especialmente se o objeto puder ser de tipos diferentes. Isso evita ClassCastException.
  • Busque hierarquias “planas” e fáceis de entender.
    Quanto mais simples a árvore de herança — mais fácil manter e evoluir o código.
  • Evite criar abstrações “sem sentido”.
    Se a classe não contém métodos abstratos e não se destina à herança, não a torne abstrata.
  • Use a anotação @Override sempre que sobrescrever um método.
    Isso ajuda o compilador a detectar erros na assinatura.

6. Erros típicos ao trabalhar com polimorfismo e abstrações

Erro nº 1: Fazer cast sem verificação

Às vezes dá vontade de “encurtar caminho” e fazer o cast sem verificar. Isso pode até funcionar, mas também pode causar uma queda inesperada do programa. Sempre use instanceof:

if (item instanceof Book) {
    Book book = (Book) item;
    // ...
}

Erro nº 2: Tentar chamar método da classe filha por meio de referência do tipo base

LibraryItem item = new Book("Java", "Autor");
item.getAuthor(); // Erro de compilação: em LibraryItem não existe esse método!

Solução — ou faça o cast, ou adicione o método necessário à classe base (se isso fizer sentido).

Erro nº 3: Implementação incompleta de interface ou classe abstrata

Se esquecer de implementar todos os métodos de uma interface — o compilador não deixará montar o projeto. Mas se implementar “stubs” que não fazem nada, isso levará a comportamentos inesperados.

Erro nº 4: Hierarquia de herança muito profunda

Se você tem mais de três níveis de herança — pense se não é possível simplificar a arquitetura.

Erro nº 5: Violação do princípio da responsabilidade única

Se a abstração descreve responsabilidades demais, ela se torna difícil de manter. É melhor dividir em várias interfaces ou classes.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION