1. O que é uma hierarquia de classes?
Na programação (e não apenas nela), hierarquia é uma estrutura em árvore, em que temos entidades “gerais” no topo da árvore (classes base) e, abaixo, as mais específicas (subclasses). Em Java, a hierarquia de classes é construída com herança: cada subclasse pode, por sua vez, ser pai de outras subclasses e assim por diante.
Analogia:
- Animal (Animal) — é uma classe geral.
- Mamífero (Mammal) — é um caso particular de animal.
- Cachorro (Dog) — é um caso particular de mamífero.
- Já um “Sharik” específico — é um objeto da classe Dog.
No código, isso fica assim:
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
A hierarquia de classes permite descrever propriedades e comportamentos comuns “no topo”, e os detalhes “na base”. Isso torna o código mais lógico e evita duplicação.
Esquema da hierarquia
Animal
├── Mammal
│ ├── Dog
│ └── Cat
└── Bird
└── Sparrow
2. Como construir a hierarquia: lógica e prática
Definindo o que é geral e o que é específico
A regra principal: a classe base deve conter o que é característico de todos os seus descendentes. Tudo o que for único — mova para as subclasses.
Exemplo:
- Todos os animais podem respirar e comer — logo, os métodos breathe() e eat() devem estar na classe Animal.
- Apenas aves sabem voar — então o método fly() deve estar na classe Bird, e não em Animal.
- Apenas cães sabem latir — o método bark() deve estar na classe Dog.
Exemplo: Animais
// Classe base
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(name + " come.");
}
void makeSound() {
System.out.println(name + " emite um som.");
}
}
// Subclasse: Mamífero
class Mammal extends Animal {
Mammal(String name) {
super(name);
}
void feedMilk() {
System.out.println(name + " amamenta os filhotes com leite.");
}
}
// Subclasse: Cachorro
class Dog extends Mammal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " late: Au-au!");
}
void wagTail() {
System.out.println(name + " abana o rabo.");
}
}
// Subclasse: Gato
class Cat extends Mammal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " mia: Miau!");
}
void purr() {
System.out.println(name + " ronrona.");
}
}
// Subclasse: Ave
class Bird extends Animal {
Bird(String name) {
super(name);
}
void fly() {
System.out.println(name + " voa.");
}
@Override
void makeSound() {
System.out.println(name + " pia: Piu-piu!");
}
}
Chamada no main:
public class ZooDemo {
public static void main(String[] args) {
Dog sharik = new Dog("Sharik");
Cat murka = new Cat("Murka");
Bird sparrow = new Bird("Vorobey");
sharik.eat(); // Sharik come.
sharik.makeSound(); // Sharik late: Au-au!
sharik.feedMilk(); // Sharik amamenta os filhotes com leite.
sharik.wagTail(); // Sharik abana o rabo.
murka.eat(); // Murka come.
murka.makeSound(); // Murka mia: Miau!
murka.feedMilk(); // Murka amamenta os filhotes com leite.
murka.purr(); // Murka ronrona.
sparrow.eat(); // Vorobey come.
sparrow.makeSound(); // Vorobey pia: Piu-piu!
sparrow.fly(); // Vorobey voa.
}
}
Visualização: árvore de classes
| Classe | Superclasse | Características |
|---|---|---|
|
|
name, eat(), makeSound() |
|
|
feedMilk() |
|
|
makeSound(), wagTail() |
|
|
makeSound(), purr() |
|
|
fly(), makeSound() |
3. Mais exemplos do mundo real
Figuras geométricas
Hierarquias de classes são excelentes para modelar geometria.
// Classe base
class Shape {
void draw() {
System.out.println("Desenhando uma figura.");
}
}
// Círculo
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
void draw() {
System.out.println("Desenhando um círculo de raio " + radius);
}
}
// Retângulo
class Rectangle extends Shape {
double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
void draw() {
System.out.println("Desenhando um retângulo " + width + "x" + height);
}
}
Uso:
public class ShapeDemo {
public static void main(String[] args) {
Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);
s1.draw(); // Desenhando um círculo de raio 5.0
s2.draw(); // Desenhando um retângulo 3.0x4.0
}
}
Transportes
class Vehicle {
void move() {
System.out.println("O veículo se move.");
}
}
class Car extends Vehicle {
@Override
void move() {
System.out.println("O carro anda na estrada.");
}
}
class Bicycle extends Vehicle {
@Override
void move() {
System.out.println("O ciclista pedala.");
}
}
main:
Vehicle v1 = new Car();
Vehicle v2 = new Bicycle();
v1.move(); // O carro anda na estrada.
v2.move(); // O ciclista pedala.
Usuários
class User {
String username;
User(String username) { this.username = username; }
void login() { System.out.println(username + " fez login no sistema."); }
}
class Admin extends User {
Admin(String username) { super(username); }
void deleteUser(String user) {
System.out.println(username + " excluiu o usuário " + user);
}
}
class Customer extends User {
Customer(String username) { super(username); }
void buy() { System.out.println(username + " realizou uma compra."); }
}
4. Nuances úteis
Não abuse da herança. Herança é uma ferramenta para relações “is-a”. Se você quer dizer “Gato é um Animal”, use herança. Se “Gato contém Cauda”, use composição (has-a).
Ruim:
class Engine { /* ... */ }
class Car extends Engine { /* Carro é um motor? Não, isso é composição! */ }
Bom:
class Car {
Engine engine; // O carro contém um motor
}
Não crie hierarquias muito profundas. Quanto mais níveis — mais difícil é manter e entender o código. Normalmente, 2–3 níveis são o máximo para a maioria das tarefas.
Não crie hierarquias muito “planas”. Se você tem 20 classes, todas herdando diretamente de uma única base, talvez seja hora de rever a arquitetura.
Herança “por conveniência”. Às vezes dá vontade de “pegar emprestados” métodos ou campos sem pensar na lógica. Isso leva ao caos! Se class A e class B não estão logicamente relacionados, não faça class B extends A apenas por causa de alguns métodos.
Violação do princípio da substituição de Liskov. Se a subclasse não puder ser usada no lugar do pai sem surpresas — a hierarquia foi construída de forma incorreta.
Duplicação de código. Se você se pega pensando “opa, este método está sendo copiado de classe em classe”, talvez seja hora de movê-lo para a classe base.
5. Erros comuns ao construir hierarquias de classes
Erro nº 1: Herança sem a lógica “is-a”.
Se você usa herança apenas para acessar métodos ou campos, e não porque a subclasse realmente “é” o pai, seu código vai virar uma bagunça rapidamente. Por exemplo, “Carro herda Motor” — isso não é is-a, e sim has-a.
Erro nº 2: Ignorar características únicas da subclasse.
Se todas as suas subclasses parecem iguais e não adicionam nada novo, talvez a hierarquia não seja necessária — use uma única classe.
Erro nº 3: Duplicar código nas subclasses.
Se você copia a mesma implementação para várias subclasses, vale a pena movê-la para a classe pai.
Erro nº 4: Hierarquia muito complexa ou profunda.
Hierarquias com muitos níveis são difíceis de manter e testar. Quanto mais simples — melhor!
Erro nº 5: Sobrescrever métodos sem a anotação @Override.
Sem a anotação, é fácil errar na assinatura do método e, então, o método não será sobrescrito, mas considerado um novo. Sempre use @Override!
GO TO FULL VERSION