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 chamar 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 chamar 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
A() B() C()
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
Shadowed Sam
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".