DEV Community

Emanoel Carvalho
Emanoel Carvalho

Posted on

Herança e Classes Abstratas em Programação Orientada a Objetos

O que é Herança?

Herança é um dos pilares fundamentais da Programação Orientada a Objetos (POO) que permite que uma classe (chamada de classe filha ou subclasse) herde atributos e métodos de outra classe (chamada de classe pai ou superclasse). Esse conceito promove a reutilização de código e a criação de hierarquias de classes.

Por que Usar Herança?

A herança é utilizada para:

  • Reutilização de Código: Permite que subclasses reutilizem métodos e atributos da superclasse, reduzindo a duplicação de código.
  • Organização e Estrutura: Facilita a organização de classes em uma hierarquia lógica, representando relações "é um".
  • Extensibilidade: Facilita a adição de novas funcionalidades ao sistema sem alterar o código existente.

Como Funciona a Herança?

Em Java, a herança é implementada usando a palavra-chave extends. A subclasse herda todos os métodos e atributos da superclasse, podendo também adicionar seus próprios métodos e atributos ou sobrescrever os existentes.

Exemplo de Herança

Vamos criar um exemplo simples que demonstra o conceito de herança utilizando uma classe base chamada Animal e duas subclasses chamadas Cachorro e Gato.

// Superclasse
public class Animal {
    // Atributo comum a todos os animais
    private String nome;

    // Construtor
    public Animal(String nome) {
        this.nome = nome;
    }

    // Método comum a todos os animais
    public void fazerSom() {
        System.out.println("O animal faz um som.");
    }

    // Getter para o nome
    public String getNome() {
        return nome;
    }
}
Enter fullscreen mode Exit fullscreen mode
// Subclasse Cachorro
public class Cachorro extends Animal {
    // Construtor da subclasse
    public Cachorro(String nome) {
        super(nome); // Chama o construtor da superclasse
    }

    // Sobrescrevendo o método fazerSom
    @Override
    public void fazerSom() {
        System.out.println(getNome() + " faz au au.");
    }
}
Enter fullscreen mode Exit fullscreen mode
// Subclasse Gato
public class Gato extends Animal {
    // Construtor da subclasse
    public Gato(String nome) {
        super(nome); // Chama o construtor da superclasse
    }

    // Sobrescrevendo o método fazerSom
    @Override
    public void fazerSom() {
        System.out.println(getNome() + " faz miau.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Usando as Classes

public class Main {
    public static void main(String[] args) {
        // Criando instâncias de Cachorro e Gato
        Cachorro cachorro = new Cachorro("Rex");
        Gato gato = new Gato("Miau");

        // Chamando o método fazerSom
        cachorro.fazerSom(); // Saída: Rex faz au au.
        gato.fazerSom();     // Saída: Miau faz miau.
    }
}
Enter fullscreen mode Exit fullscreen mode

Saída Esperada:

Rex faz au au.
Miau faz miau.
Enter fullscreen mode Exit fullscreen mode

O que são Classes Abstratas?

Classes abstratas são um tipo especial de classe que não pode ser instanciada diretamente. Elas servem como uma base para outras classes e podem conter métodos abstratos (métodos sem implementação) e métodos concretos (com implementação). O principal propósito das classes abstratas é fornecer uma estrutura comum para suas subclasses, que devem implementar os métodos abstratos.

Como Funciona uma Classe Abstrata?

Em Java, para declarar uma classe abstrata, usamos a palavra-chave abstract. Métodos declarados como abstract dentro de uma classe abstrata não têm corpo e devem ser implementados nas subclasses.

Exemplo de Classe Abstrata

Vamos modificar o exemplo anterior para incluir uma classe abstrata chamada AnimalAbstrato:

// Classe Abstrata
public abstract class AnimalAbstrato {
    private String nome;

    public AnimalAbstrato(String nome) {
        this.nome = nome;
    }

    // Método abstrato
    public abstract void fazerSom();

    // Getter para o nome
    public String getNome() {
        return nome;
    }
}
Enter fullscreen mode Exit fullscreen mode
// Subclasse Cachorro que herda de AnimalAbstrato
public class Cachorro extends AnimalAbstrato {
    public Cachorro(String nome) {
        super(nome); // Chama o construtor da superclasse
    }

    // Implementando o método fazerSom
    @Override
    public void fazerSom() {
        System.out.println(getNome() + " faz au au.");
    }
}
Enter fullscreen mode Exit fullscreen mode
// Subclasse Gato que herda de AnimalAbstrato
public class Gato extends AnimalAbstrato {
    public Gato(String nome) {
        super(nome); // Chama o construtor da superclasse
    }

    // Implementando o método fazerSom
    @Override
    public void fazerSom() {
        System.out.println(getNome() + " faz miau.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Usando a Classe Abstrata

public class Main {
    public static void main(String[] args) {
        // Criando instâncias de Cachorro e Gato
        AnimalAbstrato cachorro = new Cachorro("Rex");
        AnimalAbstrato gato = new Gato("Miau");

        // Chamando o método fazerSom
        cachorro.fazerSom(); // Saída: Rex faz au au.
        gato.fazerSom();     // Saída: Miau faz miau.
    }
}
Enter fullscreen mode Exit fullscreen mode

Saída Esperada:

Rex faz au au.
Miau faz miau.
Enter fullscreen mode Exit fullscreen mode

Problemas de Herança com Superclasses

1. Dependência Forte entre Subclasse e Superclasse

Quando uma classe herda de outra, ela cria uma dependência direta entre a subclasse e a superclasse. Isso significa que qualquer mudança na superclasse pode impactar todas as subclasses, o que pode tornar o código difícil de manter.

Exemplo:

public class Animal {
    public void mover() {
        System.out.println("O animal está se movendo.");
    }
}

public class Peixe extends Animal {
    @Override
    public void mover() {
        System.out.println("O peixe está nadando.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Se no futuro você alterar a implementação de Animal, por exemplo, mudando o método mover para incluir uma lógica diferente, essa alteração será automaticamente propagada para todas as subclasses, o que pode gerar comportamentos inesperados.

Problema: Manutenção Difícil

Se a superclasse for modificada, todas as subclasses precisam ser testadas novamente para garantir que essas mudanças não quebraram o comportamento esperado.

2. Herança Múltipla

Em Java, uma classe não pode estender mais de uma classe, ou seja, não é possível ter herança múltipla. Isso significa que uma subclasse pode herdar de apenas uma superclasse. Essa restrição ajuda a evitar a complexidade que pode surgir de heranças múltiplas, como a ambiguidade de métodos (por exemplo, se duas superclasses tiverem métodos com o mesmo nome).

Exemplo:

public class A {
    public void metodo() {
        System.out.println("Método da classe A");
    }
}

public class B {
    public void metodo() {
        System.out.println("Método da classe B");
    }
}

// Isso não é permitido em Java
public class C extends A, B { // Erro: não pode estender mais de uma classe
}
Enter fullscreen mode Exit fullscreen mode

3. Polimorfismo e Hierarquia Complexa

Às vezes, a hierarquia de classes pode se tornar muito complexa, dificultando o entendimento do relacionamento entre classes e a previsibilidade do comportamento do código. Isso pode levar a um uso ineficiente de polimorfismo, onde as subclasses não se comportam como esperado.

Considerações Finais

A herança e as classes abstratas são conceitos poderosos na POO que ajudam a criar sistemas mais organizados e reutilizáveis. No entanto, é fundamental usar a herança com cautela, reconhecendo os problemas potenciais que podem surgir, como dependências fortes, a restrição da herança única e a complexidade da hierarquia. Avaliar a necessidade de herança versus outras abordagens, como composição, pode resultar em um design de software mais limpo e sustentável.

Top comments (0)