1. ¿Qué es una jerarquía de clases?
En programación (y no solo en ella), una jerarquía es una estructura en forma de árbol, donde tenemos entidades «generales» en la parte superior del árbol (clases base) y, por debajo, entidades más concretas (subclases). En Java, la jerarquía de clases se construye mediante herencia: cada subclase puede a su vez ser el padre de otras subclases, y así sucesivamente.
Analogía:
- Un animal (Animal) es una clase general.
- Un mamífero (Mammal) es un caso particular de animal.
- Un perro (Dog) es un caso particular de mamífero.
- Y un «Rex» concreto ya es un objeto de la clase Dog.
En código se ve así:
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
La jerarquía de clases permite describir propiedades y comportamientos comunes «arriba», y los detalles «abajo». Esto hace que el código sea más lógico y evita la duplicación.
Esquema de la jerarquía
Animal
├── Mammal
│ ├── Dog
│ └── Cat
└── Bird
└── Sparrow
2. Cómo construir una jerarquía: lógica y práctica
Definir lo general y lo particular
La regla principal: la clase base debe contener lo que es característico de todos sus descendientes. Todo lo exclusivo debe ir a las subclases.
Ejemplo:
- Todos los animales pueden respirar y comer; por lo tanto, los métodos breathe() y eat() deben estar en la clase Animal.
- Solo las aves saben volar; por lo tanto, el método fly() estará en la clase Bird, no en Animal.
- Solo los perros saben ladrar; el método bark() estará en la clase Dog.
Ejemplo: Animales
// Clase 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 un sonido.");
}
}
// Subclase: Mamífero
class Mammal extends Animal {
Mammal(String name) {
super(name);
}
void feedMilk() {
System.out.println(name + " alimenta a las crías con leche.");
}
}
// Subclase: Perro
class Dog extends Mammal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " ladra: ¡Guau-guau!");
}
void wagTail() {
System.out.println(name + " mueve la cola.");
}
}
// Subclase: Gato
class Cat extends Mammal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " maúlla: ¡Miau!");
}
void purr() {
System.out.println(name + " ronronea.");
}
}
// Subclase: Ave
class Bird extends Animal {
Bird(String name) {
super(name);
}
void fly() {
System.out.println(name + " vuela.");
}
@Override
void makeSound() {
System.out.println(name + " gorjea: ¡Pío-pío!");
}
}
Llamada en main:
public class ZooDemo {
public static void main(String[] args) {
Dog sharik = new Dog("Rex");
Cat murka = new Cat("Kitty");
Bird sparrow = new Bird("Gorrión");
sharik.eat(); // Rex come.
sharik.makeSound(); // Rex ladra: ¡Guau-guau!
sharik.feedMilk(); // Rex alimenta a las crías con leche.
sharik.wagTail(); // Rex mueve la cola.
murka.eat(); // Kitty come.
murka.makeSound(); // Kitty maúlla: ¡Miau!
murka.feedMilk(); // Kitty alimenta a las crías con leche.
murka.purr(); // Kitty ronronea.
sparrow.eat(); // Gorrión come.
sparrow.makeSound(); // Gorrión gorjea: ¡Pío-pío!
sparrow.fly(); // Gorrión vuela.
}
}
Visualización: árbol de clases
| Clase | Padre | Características |
|---|---|---|
|
|
name, eat(), makeSound() |
|
|
feedMilk() |
|
|
makeSound(), wagTail() |
|
|
makeSound(), purr() |
|
|
fly(), makeSound() |
3. Más ejemplos de la vida real
Figuras geométricas
Las jerarquías de clases son muy adecuadas para modelar geometría.
// Clase base
class Shape {
void draw() {
System.out.println("Dibujamos una figura.");
}
}
// Círculo
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
void draw() {
System.out.println("Dibujamos un círculo de radio " + radius);
}
}
// Rectá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("Dibujamos un rectá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(); // Dibujamos un círculo de radio 5.0
s2.draw(); // Dibujamos un rectángulo 3.0x4.0
}
}
Transporte
class Vehicle {
void move() {
System.out.println("El vehículo se mueve.");
}
}
class Car extends Vehicle {
@Override
void move() {
System.out.println("El coche circula por la carretera.");
}
}
class Bicycle extends Vehicle {
@Override
void move() {
System.out.println("El ciclista pedalea.");
}
}
main:
Vehicle v1 = new Car();
Vehicle v2 = new Bicycle();
v1.move(); // El coche circula por la carretera.
v2.move(); // El ciclista pedalea.
Usuarios
class User {
String username;
User(String username) { this.username = username; }
void login() { System.out.println(username + " ha iniciado sesión."); }
}
class Admin extends User {
Admin(String username) { super(username); }
void deleteUser(String user) {
System.out.println(username + " ha eliminado al usuario " + user);
}
}
class Customer extends User {
Customer(String username) { super(username); }
void buy() { System.out.println(username + " ha realizado una compra."); }
}
4. Matices útiles
No abuses de la herencia. La herencia es una herramienta para relaciones «is-a». Si quieres decir «El Gato es un Animal», usa herencia. Si, en cambio, «El Gato contiene una Cola», usa composición (has-a).
Mal:
class Engine { /* ... */ }
class Car extends Engine { /* ¿Un coche es un motor? No, ¡esto es composición! */ }
Bien:
class Car {
Engine engine; // Un coche contiene un motor
}
No hagas jerarquías demasiado profundas. Cuantos más niveles, más difícil es mantener y entender el código. Normalmente, 2–3 niveles son el máximo para la mayoría de tareas.
Tampoco hagas jerarquías demasiado «planas». Si tienes 20 clases que heredan directamente de una base, quizá debas revisar la arquitectura.
Herencia «por conveniencia». A veces apetece «tomar prestados» métodos o campos sin pensar en la lógica. ¡Eso lleva al caos! Si class A y class B no están relacionados lógicamente, no hagas que class B extends A solo por un par de métodos.
Violación del principio de sustitución de Liskov. Si una subclase no puede usarse en lugar del padre sin sorpresas, la jerarquía está mal construida.
Duplicación de código. Si te descubres pensando «vaya, este método se copia de una clase a otra», quizá sea hora de moverlo a la clase base.
5. Errores típicos al construir jerarquías de clases
Error n.º 1: herencia sin lógica «is-a».
Si usas herencia solo para acceder a métodos o campos, y no porque la subclase realmente «sea» el padre, tu código se volverá un caos rápidamente. Por ejemplo, «El Coche hereda de Motor» no es is-a, sino has-a.
Error n.º 2: ignorar las características únicas de la subclase.
Si todas tus subclases se ven iguales y no añaden nada nuevo, quizá la jerarquía no sea necesaria: usa una sola clase.
Error n.º 3: duplicación de código en las subclases.
Si copias la misma implementación en varias subclases, conviene moverla a la clase padre.
Error n.º 4: jerarquía demasiado compleja o profunda.
Las jerarquías con muchos niveles son difíciles de mantener y probar. ¡Cuanto más simple, mejor!
Error n.º 5: sobrescritura de métodos sin la anotación @Override.
Sin la anotación es fácil equivocarse en la signatura del método y, entonces, no se sobrescribirá sino que se considerará uno nuevo. ¡Usa siempre @Override!
GO TO FULL VERSION