Atualizado: 26/07/2025

Este conteúdo é original e não foi gerado por inteligência artificial.

Herança em Java

A herança é um dos conceitos centrais da programação orientada a objetos. Por meio dela, é possível ampliar a funcionalidade de classes existentes, acrescentando novos comportamentos ou modificando os já definidos.

Exemplo básico

Considere a classe Person, que representa uma pessoa:

class Person {

    String name;

    public String getName() {
        return name;
    }

    public Person(String name) {
        this.name = name;
    }

    public void display() {
        System.out.println("Name: " + name);
    }
}

Agora, suponha que seja necessário criar uma classe para representar um funcionário (Employee). Como todo funcionário também é uma pessoa, a classe Employee pode herdar de Person:

class Employee extends Person {

    public Employee(String name) {
        // chamada obrigatória ao construtor da superclasse
        super(name);
    }
}

O modificador extends indica que Employee é uma subclasse de Person. A subclasse herda todos os atributos e métodos públicos e protegidos da superclasse.

Quando a superclasse define construtores, a subclasse precisa invocar explicitamente um deles usando super(...). Essa chamada deve ser a primeira instrução do construtor. No exemplo acima, super(name) aciona o construtor de Person que recebe um parâmetro, delegando a inicialização do atributo name.

Mesmo que a subclasse não execute nenhuma outra operação no construtor, a chamada ao construtor da classe base continua obrigatória se não houver um construtor sem parâmetros na superclasse.

Exemplo de uso

public class Program {
    public static void main(String[] args) {
        Person tom = new Person("Tom");
        tom.display();

        Employee sam = new Employee("Sam");
        sam.display();
    }
}

Esse exemplo mostra que a herança permite reaproveitar a lógica da superclasse. Employee herda display() de Person, e como super(name) foi chamado no construtor, o método funciona sem reimplementação.


Acrescentando novos membros à subclasse

Uma subclasse pode definir seus próprios atributos e métodos, além dos herdados:

public class Program {
    public static void main(String[] args) {
        Employee sam = new Employee("Sam", "Microsoft");
        sam.display();  // Name: Sam
        sam.work();     // Sam works in Microsoft
    }
}

class Person {

    String name;

    public String getName() {
        return name;
    }

    public Person(String name) {
        this.name = name;
    }

    public void display() {
        System.out.println("Name: " + name);
    }
}

class Employee extends Person {

    String company;

    public Employee(String name, String company) {
        super(name);
        this.company = company;
    }

    public void work() {
        System.out.printf("%s works in %s%n", getName(), company);
    }
}

Nesse caso, Employee acrescenta o atributo company e o método work.


Sobrescrita de métodos

Uma subclasse pode redefinir métodos herdados para alterar seu comportamento. No exemplo abaixo, Employee sobrescreve o método display:

class Employee extends Person {

    String company;

    public Employee(String name, String company) {
        super(name);
        this.company = company;
    }

    @Override
    public void display() {
        System.out.printf("Name: %s%n", getName());
        System.out.printf("Works in %s%n", company);
    }
}

A anotação @Override não é obrigatória, mas é recomendada, pois ajuda o compilador a verificar se realmente existe um método correspondente na superclasse.

O nível de acesso do método sobrescrito não pode ser mais restritivo que o da superclasse. Por exemplo, se o método original é public, a versão sobrescrita também precisa ser public.

Quando parte da lógica da superclasse ainda é útil, é possível invocar sua implementação com super:

@Override
public void display() {
    super.display();
    System.out.printf("Works in %s%n", company);
}

Isso permite complementar, em vez de substituir totalmente, o comportamento original.


Restrições à herança

Para impedir que uma classe seja estendida, declara-se a classe como final:

public final class Person {
}

Se Person for definida assim, tentar criar Employee extends Person gerará erro de compilação.

Também é possível impedir a sobrescrita de métodos específicos:

public final void display() {
    System.out.println("Name: " + name);
}

Despacho dinâmico de métodos

Com herança e sobrescrita, é possível que variáveis do tipo da superclasse referenciem objetos de subclasses:

Person sam = new Employee("Sam", "Oracle");

Mesmo que o tipo declarado seja Person, a JVM identifica que a instância real é de Employee. Ao chamar sam.display(), será executada a versão sobrescrita de Employee:

public class Program {
    public static void main(String[] args) {
        Person tom = new Person("Tom");
        tom.display();

        Person sam = new Employee("Sam", "Oracle");
        sam.display();
    }
}

class Person {

    String name;

    public String getName() {
        return name;
    }

    public Person(String name) {
        this.name = name;
    }

    public void display() {
        System.out.printf("Person %s%n", name);
    }
}

class Employee extends Person {

    String company;

    public Employee(String name, String company) {
        super(name);
        this.company = company;
    }

    @Override
    public void display() {
        System.out.printf("Employee %s works in %s%n", getName(), company);
    }
}

Saída:

Person Tom
Employee Sam works in Oracle

Esse comportamento é chamado de dynamic method lookup ou despacho dinâmico de métodos. A decisão sobre qual versão executar ocorre em tempo de execução, com base no tipo real do objeto.


📝 Exercícios

Tarefa 1 — Ordem de chamada de construtores

Descrição: Análise da ordem das mensagens impressas com herança em cadeia. Identifique a saída do programa.

class A {
    public A() {
        System.out.println("A()");
    }
}

class B extends A {
    public B() {
        System.out.println("B()");
    }
}

class C extends B {
    public C() {
        System.out.println("C()");
    }
}

public class Program {
    public static void main(String[] args) {
        C c = new C();
    }
}
Resposta

Saída do programa:

A()
B()
C()

Explicação: A construção de C dispara a construção de suas superclasses em cadeia. A chamada a super() é implícita e ocorre como primeira instrução de cada construtor, o que leva à ordem A depois B e por fim C.


Tarefa 2

Descrição: Explique a diferença no resultado entre acessar um campo com o mesmo nome na subclasse e chamar um método que retorna o valor da superclasse.

class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

class Employee extends Person {
    // campo com o mesmo nome, mas independente do da superclasse
    public String name = "Shadowed";

    public Employee(String name) {
        super(name);
    }

    public void printFields() {
        System.out.println(name);
        System.out.println(getName());
    }
}

public class Program {
    public static void main(String[] args) {
        Employee e = new Employee("Sam");
        e.printFields();
    }
}
Resposta

Saída do programa:

Shadowed
Sam

Explicação: O campo name declarado em Employee não substitui o campo privado de Person, apenas o oculta no escopo da subclasse. A primeira linha acessa o campo de Employee. A segunda linha chama getName(), que retorna o campo privado mantido em Person, inicializado com "Sam".

Política de Privacidade

Copyright © www.programicio.com Todos os direitos reservados

É proibida a reprodução do conteúdo desta página sem autorização prévia do autor.

Contato: programicio@gmail.com