Java Herança
Um cachorro e um gato são diferentes: um late, o outro mia. Mas os dois têm nome, idade e comem. Se você escrever uma classe Cachorro e uma Gato, vai repetir essa parte comum nas duas. A herança resolve isso: você escreve o que é comum uma vez, numa classe Animal, e diz que Cachorro e Gato são tipos de Animal. Eles recebem tudo de graça e só acrescentam o que têm de especial.
- O que é herança e como escrevê-la com
extends. - O teste do "é um", para saber quando usar herança (e quando não usar).
- O que a subclasse herda e o que ela não enxerga (
private). - Por que o Java só deixa herdar de uma classe, e o erro real de quem tenta duas.
- A cadeia de herança, que sempre termina na classe
Object.
Esta é a primeira aula do grupo "Herança e Polimorfismo". Ela supõe que você já sabe criar classes e objetos, com atributos e métodos.
A ideia: herdar características
Na biologia, um filho herda características dos pais. Em Java, uma classe pode herdar de outra: ela recebe automaticamente os atributos e métodos da outra, e pode acrescentar os seus.
| Nome | Também chamada de | No exemplo |
|---|---|---|
| Superclasse | classe mãe, classe base, classe pai | Animal: o que todo animal tem |
| Subclasse | classe filha, classe derivada | Cachorro, Gato: o específico de cada um |
"Super" vem do latim, "acima": a superclasse fica acima na hierarquia. "Sub" é "abaixo". Uma superclasse pode ter muitas subclasses (Animal tem Cachorro, Gato, Passaro…), como uma mãe pode ter muitos filhos.
O problema: código repetido
Sem herança, as duas classes da Ana ficariam assim:
class Cachorro {
String nome;
int idade;
void comer() {
System.out.println(nome + " está comendo.");
}
void latir() {
System.out.println(nome + ": Au au!");
}
}
class Gato {
String nome; // repetido
int idade; // repetido
void comer() { // repetido
System.out.println(nome + " está comendo.");
}
void miar() {
System.out.println(nome + ": Miau!");
}
}
Metade do código é cópia. E cópia dá trabalho: se a Ana quiser mudar a mensagem de comer(), precisa lembrar de mudar nos dois lugares (e, mais tarde, em Passaro, Peixe, Coelho…). Esquecer um é questão de tempo.
extends: herdando de Animal
Com herança, a parte comum vai para Animal, e cada subclasse diz extends Animal:
public class Main {
public static void main(String[] args) {
Cachorro rex = new Cachorro();
rex.nome = "Rex";
rex.idade = 3;
rex.comer();
rex.latir();
Gato mimi = new Gato();
mimi.nome = "Mimi";
mimi.idade = 5;
mimi.comer();
mimi.miar();
}
}
class Animal {
String nome;
int idade;
void comer() {
System.out.println(nome + " está comendo.");
}
}
class Cachorro extends Animal {
void latir() {
System.out.println(nome + ": Au au!");
}
}
class Gato extends Animal {
void miar() {
System.out.println(nome + ": Miau!");
}
}
- class Animal
- A superclasse, uma classe comum. Nada nela diz que alguém vai herdar dela.
- class Cachorro
- A subclasse.
- extends
- "Estende", "amplia":
Cachorroé uma versão ampliada deAnimal. Ele recebenome,idadeecomer()sem escrever nada. - Animal
- De quem herdar. Só pode haver uma classe aqui.
- void latir()
- O que só o cachorro tem. Repare que ele usa
nome, que foi herdado.
$ java Main.java Rex está comendo. « comer() veio de Animal Rex: Au au! « latir() é do Cachorro Mimi está comendo. « o mesmo comer(), herdado pelo Gato Mimi: Miau!
A classe Cachorro tem três linhas de código, mas um objeto Cachorro tem dois atributos e dois métodos. Agora, mudar a mensagem de comer() é mudar um lugar, e todos os animais mudam juntos.
A herança é uma via de mão única. O Cachorro sabe tudo o que o Animal sabe, mas o Animal não sabe latir:
$ javac Main.java
Main.java:5: error: cannot find symbol
bicho.latir();
^
symbol: method latir()
location: variable bicho of type Animal
1 error
Aqui, bicho era um new Animal(). O erro diz: "não achei o método latir() na variável bicho, do tipo Animal". Nem todo animal late.
Missão: crie a classe Passaro, que herda de Animal e tem um método voar() que mostra NOME está voando!. No main, crie um pássaro chamado Piu e chame comer() e voar(). A saída deve ter Piu está comendo. e Piu está voando!.
public class Main {
public static void main(String[] args) {
Cachorro rex = new Cachorro();
rex.nome = "Rex";
rex.comer();
rex.latir();
// crie aqui o pássaro Piu
}
}
class Animal {
String nome;
int idade;
void comer() {
System.out.println(nome + " está comendo.");
}
}
class Cachorro extends Animal {
void latir() {
System.out.println(nome + ": Au au!");
}
}
// crie aqui a classe Passaro
public class Main {
public static void main(String[] args) {
Cachorro rex = new Cachorro();
rex.nome = "Rex";
rex.comer();
rex.latir();
Passaro piu = new Passaro();
piu.nome = "Piu";
piu.comer();
piu.voar();
}
}
class Animal {
String nome;
int idade;
void comer() {
System.out.println(nome + " está comendo.");
}
}
class Cachorro extends Animal {
void latir() {
System.out.println(nome + ": Au au!");
}
}
class Passaro extends Animal {
void voar() {
System.out.println(nome + " está voando!");
}
}
O teste do "é um"
Herança é poderosa, e por isso é fácil exagerar. A pergunta que decide é: "um X é um Y?". Se a frase faz sentido sempre, a herança cabe.
| Frase | Faz sentido? | Então… |
|---|---|---|
Um Cachorro é um Animal | Sim | Cachorro extends Animal |
Uma ContaPoupanca é uma Conta | Sim | ContaPoupanca extends Conta |
Um Gerente é um Funcionario | Sim | Gerente extends Funcionario |
Um Carro é um Motor | Não: um carro tem um motor | Atributo: Motor motor; dentro de Carro |
Uma Turma é um Aluno | Não: uma turma tem alunos | Atributo: List<Aluno> alunos; |
Quando a relação é "tem um", o certo é um atributo; isso se chama composição. Herdar de Motor só para "aproveitar" os métodos dele faria um Carro ter acelerarPistao() como se fosse dele, e qualquer lugar que espera um motor aceitaria um carro inteiro. Quando estiver em dúvida, prefira composição: é a recomendação clássica dos livros de orientação a objetos.
O que é herdado (e o que não é)
A subclasse herda os atributos e métodos da superclasse. Mas "herdar" e "enxergar" são coisas diferentes. Os membros private existem dentro do objeto, só que a subclasse não pode tocá-los. Veja: o Animal tem um número de chip privado.
public class Main {
public static void main(String[] args) {
Cachorro rex = new Cachorro();
rex.nome = "Rex";
rex.mostrarSegredo();
}
}
class Animal {
String nome;
private String chip = "BR-0042";
String getChip() {
return chip;
}
}
class Cachorro extends Animal {
void mostrarSegredo() {
System.out.println(nome + " tem o chip " + chip);
}
}
$ javac Main.java
Main.java:20: error: chip has private access in Animal
System.out.println(nome + " tem o chip " + chip);
^
1 error
Chip has private access in Animal = "chip tem acesso privado em Animal". O private quer dizer "só a própria classe": nem as filhas entram (veja Modificadores de Acesso). O chip está lá, dentro de todo cachorro; o caminho para chegar até ele é o método público que o Animal oferece:
public class Main {
public static void main(String[] args) {
Cachorro rex = new Cachorro();
rex.nome = "Rex";
System.out.println(rex.nome + " tem o chip " + rex.getChip());
}
}
class Animal {
String nome;
private String chip = "BR-0042";
String getChip() {
return chip;
}
}
class Cachorro extends Animal {
}
$ java Main.java Rex tem o chip BR-0042 « a classe Cachorro está vazia e mesmo assim tem getChip()
Esse é o encapsulamento funcionando junto com a herança: a superclasse protege os dados dela até das subclasses e decide o que oferecer.
| Membro da superclasse | A subclasse herda? | A subclasse pode usar direto? |
|---|---|---|
public | Sim | Sim |
protected | Sim | Sim (mesmo em outro pacote) |
| Sem modificador (de pacote) | Sim | Só se estiver no mesmo pacote |
private | O dado existe no objeto | Não: só pelos métodos públicos/protegidos |
| Construtores | Não | Só chamando com super(…) (veja super) |
Membros static | Acessíveis pelo nome da subclasse | Sim, mas continuam sendo da superclasse |
protected: "da família"
protected ("protegido") fica entre public e private: a classe, as subclasses (em qualquer pacote) e as classes do mesmo pacote enxergam. É útil para atributos que as filhas precisam mexer, mas o resto do programa não. Na dúvida, comece com private + métodos, como no chip.
Os construtores merecem destaque: cada classe tem os seus. Como o Animal aqui não declara nenhum, tudo funciona sozinho. Quando a superclasse tem um construtor com parâmetros, a subclasse precisa chamá-lo com super(…): esse é o assunto da próxima aula, super.
Herança em vários níveis
Uma subclasse pode ser superclasse de outra. A Ana organiza: todo Mamifero é um Animal, e todo Cachorro é um Mamifero.
public class Main {
public static void main(String[] args) {
Cachorro rex = new Cachorro();
rex.nome = "Rex";
rex.respirar();
rex.mamar();
rex.latir();
}
}
class Animal {
String nome;
void respirar() {
System.out.println(nome + " respira (Animal)");
}
}
class Mamifero extends Animal {
void mamar() {
System.out.println(nome + " mama (Mamifero)");
}
}
class Cachorro extends Mamifero {
void latir() {
System.out.println(nome + " late (Cachorro)");
}
}
$ java Main.java Rex respira (Animal) « herdado do "avô" Rex mama (Mamifero) « herdado da mãe Rex late (Cachorro) « dele mesmo
O Cachorro herda do Mamifero, que herda do Animal; então o Cachorro tem tudo dos dois. Na prática, cadeias com mais de três ou quatro níveis ficam difíceis de entender: a cada método, você precisa subir a escada para descobrir de onde ele veio.
Herança simples: só uma superclasse
Um morcego é um mamífero e voa como uma ave. A Ana tentou herdar das duas:
public class Main {
public static void main(String[] args) {
}
}
class Mamifero {
}
class Ave {
}
class Morcego extends Mamifero, Ave {
}
$ javac Main.java
Main.java:12: error: '{' expected
class Morcego extends Mamifero, Ave {
^
1 error
A mensagem parece estranha: '{' expected = "esperava {". O compilador leu extends Mamifero e esperava a chave que abre a classe; a vírgula não faz parte da gramática do extends. Ou seja: em Java, uma classe tem exatamente uma superclasse. Isso se chama herança simples.
Por que essa limitação? Linguagens com herança múltipla (como C++) têm o "problema do diamante": se Mamifero e Ave tiverem, cada um, um método mover() diferente, qual o Morcego herda? Os criadores do Java preferiram evitar a confusão. Para "ser várias coisas ao mesmo tempo", o Java tem as interfaces: uma classe pode implementar quantas quiser (class Morcego extends Mamifero implements Voador).
No topo de tudo: Object
E o Animal, que não tem extends, herda de quem? Toda classe sem extends herda, automaticamente, de java.lang.Object. É como se o compilador escrevesse class Animal extends Object por você. Dá para ver a escada inteira pedindo a cada classe a sua superclasse:
public class Main {
public static void main(String[] args) {
Cachorro rex = new Cachorro();
System.out.println(rex.getClass());
System.out.println(rex.getClass().getSuperclass());
System.out.println(rex.getClass().getSuperclass().getSuperclass());
System.out.println(rex.getClass().getSuperclass().getSuperclass().getSuperclass());
System.out.println(rex.toString());
}
}
class Animal {
}
class Cachorro extends Animal {
}
- rex.getClass()
- Devolve a classe do objeto:
Cachorro. - .getSuperclass()
- Devolve a superclasse de uma classe. Encadeado, sobe um degrau de cada vez.
- rex.toString()
- Um método que nem
AnimalnemCachorroescreveram: veio deObject.
$ java Main.java class Cachorro class Animal class java.lang.Object « o topo da escada null « Object não tem superclasse Cachorro@6a396c1e « toString() herdado de Object
Por isso todo objeto Java, de qualquer classe, já nasce com toString(), equals(), hashCode() e getClass(). O Cachorro@6a396c1e é o toString() padrão: nome da classe, @ e um número em hexadecimal (que muda de uma execução para outra). Esses métodos têm uma aula só para eles: A Classe Object.
A família completa do exemplo da aula fica assim:
Object
└── Animal
├── Cachorro
├── Gato
└── Passaro
Quando dá errado
$ javac Main.java
Main.java:20: error: chip has private access in Animal
System.out.println(nome + " tem o chip " + chip);
^
1 error
O que aconteceu: a subclasse tentou usar um atributo (ou método) private da superclasse.
Como resolver: use um método público da superclasse (getChip()). Se você é o dono da superclasse e as filhas realmente precisam do atributo, mude para protected.
extends A, B
O que aconteceu: tentativa de herdar de duas classes. Java só permite uma.
Como resolver: escolha a superclasse que responde ao "é um" mais forte e transforme o outro papel numa interface (implements), ou num atributo (composição).
$ javac Main.java
Main.java:5: error: cannot find symbol
bicho.latir();
^
symbol: method latir()
location: variable bicho of type Animal
1 error
O que aconteceu: o método existe na subclasse, mas a variável é do tipo da superclasse. A herança desce (filha recebe da mãe), nunca sobe.
Como resolver: use uma variável do tipo Cachorro. Variáveis do tipo da superclasse guardando objetos da subclasse são o assunto de Polimorfismo.
O que aconteceu: a superclasse é final, como String e Integer.
Como resolver: use composição: guarde um objeto dessa classe como atributo. Veja final.
Referência
| Escrita / termo | Significado |
|---|---|
class B extends A | B herda de A: B é a subclasse, A a superclasse |
| "é um" | Teste para usar herança: um B é um A? |
| "tem um" | Use composição (atributo), não herança |
private na superclasse | Existe no objeto, mas a subclasse não acessa direto |
protected | Visível para subclasses e para o mesmo pacote |
extends A, B | Não existe: herança simples (erro '{' expected) |
Sem extends | Herda de java.lang.Object |
obj.getClass().getSuperclass() | A superclasse da classe do objeto |
Exercícios
1. A palavra da herança
Complete para Moto herdar de Veiculo:
2. O topo da escada
Qual é a superclasse de uma classe que não tem extends? (nome completo)
3. "Da família"
Complete com o modificador que deixa as subclasses acessarem o atributo, mas não o resto do programa (de outros pacotes):
4. Qual destas relações deve ser feita com herança?
5. Animal tem private int idade. O que acontece se um método de Cachorro (que estende Animal) usar idade direto?
6. Quantas superclasses diretas uma classe Java pode ter?
Missão final: a loja da Ana tem funcionários e gerentes. A classe Gerente foi escrita copiando tudo de Funcionario. Refaça: Gerente deve herdar de Funcionario (extends) e manter só o que é dele (o atributo bonus e o método ganhoTotal()). Apague o que ficou repetido. A saída deve continuar com Caio ganha 3500.0 e Bia ganha 9000.0.
public class Main {
public static void main(String[] args) {
Funcionario caio = new Funcionario();
caio.nome = "Caio";
caio.salario = 3500;
caio.mostrar();
Gerente bia = new Gerente();
bia.nome = "Bia";
bia.salario = 7000;
bia.bonus = 2000;
System.out.println(bia.nome + " ganha " + bia.ganhoTotal());
}
}
class Funcionario {
String nome;
double salario;
void mostrar() {
System.out.println(nome + " ganha " + salario);
}
}
class Gerente {
String nome;
double salario;
double bonus;
void mostrar() {
System.out.println(nome + " ganha " + salario);
}
double ganhoTotal() {
return salario + bonus;
}
}
public class Main {
public static void main(String[] args) {
Funcionario caio = new Funcionario();
caio.nome = "Caio";
caio.salario = 3500;
caio.mostrar();
Gerente bia = new Gerente();
bia.nome = "Bia";
bia.salario = 7000;
bia.bonus = 2000;
System.out.println(bia.nome + " ganha " + bia.ganhoTotal());
}
}
class Funcionario {
String nome;
double salario;
void mostrar() {
System.out.println(nome + " ganha " + salario);
}
}
class Gerente extends Funcionario {
double bonus;
double ganhoTotal() {
return salario + bonus;
}
}
Resumo
- Herança:
class B extends AfazBreceber os atributos e métodos deA. Escreva o comum uma vez, na superclasse. - Use herança quando "um B é um A". Quando "B tem um A", use um atributo (composição).
- A subclasse acrescenta o que é dela; a superclasse não enxerga o que as filhas acrescentaram.
- Membros
privateda superclasse existem no objeto, mas a subclasse só os alcança por métodos. Construtores não são herdados. - Java tem herança simples: uma superclasse por classe. Para mais papéis, interfaces.
- Toda classe termina em
java.lang.Object, de onde vêmtoString(),equals()e companhia.