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
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"
.