Herança em Java
Reutilize e estenda o comportamento de classes em Java com a palavra-chave extends e as regras de herança simples.
A herança permite que uma classe construa sobre outra em vez de começar do zero. A nova classe — a subclasse — herda todos os campos e métodos do pai, e acrescenta (ou substitui) apenas o que é diferente. É assim que Java expressa relacionamentos do tipo "é um": um Cat é um Animal, um AdminUser é um User.
A palavra-chave é extends. A mecânica é simples. A parte difícil — e o que este capítulo realmente aborda — é reconhecer quando a herança é a ferramenta certa e quando não é.
Um primeiro exemplo
public class Animal {
String name;
void breathe() { System.out.println(name + " breathes"); }
}
public class Cat extends Animal {
void purr() { System.out.println(name + " purrs"); }
}Cat declara apenas purr(). Ainda assim, ela possui name e breathe(), porque os herdou:
Cat c = new Cat();
c.name = "Mittens";
c.breathe(); // Mittens breathes — inherited from Animal
c.purr(); // Mittens purrs — declared on CatAnimal é a superclasse (ou pai), Cat é a subclasse (ou filha). Outras linguagens as chamam de classe base/derivada.
O que é herdado
Uma subclasse herda:
- Todos os campos e métodos
publiceprotectedde cada ancestral. - Campos e métodos com visibilidade de pacote, se a subclasse estiver no mesmo pacote.
- Todos os membros herdados mantêm seus modificadores originais.
Uma subclasse não herda:
- Construtores. Eles não são membros no mesmo sentido — a subclasse precisa dos seus próprios.
- Campos e métodos
private. Eles existem no pai (o layout de memória de umCatainda inclui quaisquer camposprivatedeAnimal), mas a subclasse não pode vê-los pelo nome. A única forma de acessá-los é por meio de métodos acessorespublic/protectedherdados.
Herança simples — apenas um pai
Toda classe estende exatamente uma outra classe. Não há herança múltipla para classes em Java:
public class Hybrid extends Animal, Vehicle { } // ERROR — no multiple inheritanceVocê pode implementar muitas interfaces (abordadas em interfaces) — isso oferece a flexibilidade de múltiplas fontes que a maioria das linguagens usa herança múltipla para obter, sem as complicações de ambiguidade.
Se você não escrever extends, a classe implicitamente estende Object:
public class Foo { }
// is equivalent to
public class Foo extends Object { }É por isso que todo objeto Java possui toString(), equals(), hashCode() e getClass() — todos estão em Object. O capítulo sobre a classe Object os aborda em detalhes.
Classes que não podem ser estendidas
Uma classe marcada com final não pode ser um pai de forma alguma — tentar estendê-la é um erro de compilação:
public final class Money { }
public class Coupon extends Money { } // ERROR — cannot inherit from final MoneyIsso é intencional, não um descuido: muitos tipos fundamentais são final justamente para que nenhuma subclasse possa alterar seu comportamento. String, Integer e as outras classes wrapper são todas final, o que faz parte do que as torna seguras para compartilhar e armazenar em cache. Quando você quiser um tipo que não possa ser criado em subclasse — geralmente por razões de segurança ou imutabilidade — marque-o com final.
Construtores e super
Todo construtor de subclasse deve, como sua primeira ação, chamar um construtor do pai. Se você não escrever a chamada, Java insere super() automaticamente:
public class Animal {
String name;
public Animal(String name) { this.name = name; }
}
public class Cat extends Animal {
public Cat(String name) {
super(name); // call Animal(String)
}
}Se Animal não tivesse um construtor sem argumentos e Cat não escrevesse super(...), o compilador reclamaria — não há Animal() para inserir implicitamente. O capítulo sobre a palavra-chave super cobre o encadeamento de construtores com super(...) e as chamadas super.method() em detalhes.
Sobrescrita
Uma subclasse pode substituir um método herdado declarando um com a mesma assinatura:
public class Animal {
String speak() { return "(some noise)"; }
}
public class Cat extends Animal {
@Override
String speak() { return "meow"; }
}
Cat c = new Cat();
System.out.println(c.speak()); // meow@Override é uma anotação que diz ao compilador "pretendo que este método sobrescreva um herdado — por favor, gere um erro se não for o caso." Sempre use-a. Ela detecta erros de digitação e incompatibilidades de assinatura que, de outra forma, criariam silenciosamente um novo método em vez de sobrescrever o antigo. O capítulo sobre sobrescrita de métodos cobre as regras completas.
Upcasting e polimorfismo
Uma instância de subclasse pode ser atribuída a uma variável do tipo pai:
Animal a = new Cat(); // upcast — implicit
a.speak(); // calls Cat's speak() — picked at runtimeEste é o fundamento do polimorfismo, o próximo capítulo. A variável a é do tipo Animal, mas o objeto real é um Cat, então a versão de Cat de speak é executada.
Quando a herança é a ferramenta errada
A herança é o mecanismo mais abusado na POO. Alguns sinais de alerta de que você deveria usar composição (um campo de outro tipo) em vez disso:
- A subclasse não passa realmente em um teste "é um". Um
Stacknão é realmente umVector— ainda assim,java.util.StackestendeVectore é amplamente considerado um erro de design. - Os internos mutáveis do pai vazam para a subclasse. Mudanças na implementação do pai quebram a subclasse.
- Você está herdando para reutilizar alguns métodos, não porque os tipos são genuinamente substituíveis.
A regra de ouro do Effective Java de Joshua Bloch: prefira composição à herança. Se B precisa do comportamento de A mas não é realmente um A, dê a B um campo privado do tipo A e redirecione o que for necessário.
// Inheritance — fragile
public class MyList extends ArrayList<String> { ... }
// Composition — robust
public class MyList {
private final List<String> inner = new ArrayList<>();
public void add(String s) { inner.add(s); }
}A versão com composição é imune a surpresas quando ArrayList adiciona novos métodos ou muda como seus campos privados funcionam.
Herança e acesso
Membros herdados mantêm o modificador que tinham no pai. Uma subclasse não pode restringir a visibilidade de um método sobrescrito — tornar um método public em private na subclasse violaria o princípio de substituição de Liskov, e o compilador recusa:
public class A {
public void hello() { }
}
public class B extends A {
private void hello() { } // ERROR — cannot reduce visibility
}Você pode ampliar a visibilidade (sobrescrever um método protected como public), mas raramente deveria.
Um exemplo completo
O que vem a seguir
super apareceu várias vezes aqui — no encadeamento de construtores e como forma de acessar um método pai sobrescrito. O próximo capítulo sobre a palavra-chave super percorre todas as situações em que ela aparece. Quando um pai deve definir o que seus filhos fazem, mas não como, você recorre a classes abstratas, que se baseiam diretamente nas regras de herança abordadas aqui.