Herencia en Java
Reutiliza y extiende el comportamiento de clases en Java con la palabra clave extends y las reglas de herencia simple.
La herencia permite que una clase se construya sobre otra en lugar de empezar desde cero. La nueva clase — la subclase — obtiene todos los campos y métodos del padre, y agrega (o reemplaza) solo lo que es diferente. Es la forma en que Java expresa relaciones "es-un": un Cat es un Animal, un AdminUser es un User.
La palabra clave es extends. La mecánica es sencilla. La parte difícil — y la parte de la que realmente trata este capítulo — es reconocer cuándo la herencia es la herramienta adecuada y cuándo no lo es.
Un primer ejemplo
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 solo purr(). Aun así tiene name y breathe(), porque los heredó:
Cat c = new Cat();
c.name = "Mittens";
c.breathe(); // Mittens breathes — inherited from Animal
c.purr(); // Mittens purrs — declared on CatAnimal es la superclase (o padre), Cat es la subclase (o hijo). Otros lenguajes los llaman clase base/derivada.
Qué se hereda
Una subclase hereda:
- Todos los campos y métodos
publicyprotectedde cada ancestro. - Campos y métodos de paquete (package-private), si la subclase está en el mismo paquete.
- Todos los miembros heredados conservan sus modificadores originales.
Una subclase no hereda:
- Constructores. No son miembros en el mismo sentido — la subclase necesita los propios.
- Campos y métodos
private. Existen en el padre (el diseño de memoria de unCattodavía incluye los camposprivatedeAnimal), pero la subclase no puede verlos por nombre. La única forma de leerlos es a través de métodos de accesopublic/protectedheredados.
Herencia simple — un único padre
Cada clase extiende exactamente otra clase. No existe herencia múltiple para clases en Java:
public class Hybrid extends Animal, Vehicle { } // ERROR — no multiple inheritancePuedes implementar muchas interfaces (cubierto en interfaces) — eso te da la flexibilidad de múltiples fuentes que la mayoría de los lenguajes usan la herencia múltiple para obtener, sin los dolores de cabeza de ambigüedad.
Si no escribes extends en absoluto, la clase extiende implícitamente Object:
public class Foo { }
// is equivalent to
public class Foo extends Object { }Por eso todo objeto Java tiene toString(), equals(), hashCode() y getClass() — todos están en Object. El capítulo sobre la clase Object los recorre en detalle.
Clases que no pueden extenderse
Una clase marcada como final no puede ser un padre en absoluto — intentar extenderla es un error de compilación:
public final class Money { }
public class Coupon extends Money { } // ERROR — cannot inherit from final MoneyEsto es deliberado, no un descuido: muchos tipos fundamentales son final precisamente para que ninguna subclase pueda cambiar su comportamiento. String, Integer y las demás clases envolventes son todas final, lo que en parte las hace seguras para compartir y almacenar en caché. Cuando quieres un tipo que no pueda ser subclasificado — generalmente por garantías de seguridad o inmutabilidad — márcalo como final.
Constructores y super
Cada constructor de subclase debe, como primera acción, llamar a un constructor del padre. Si no escribes la llamada, Java inserta super() por ti:
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)
}
}Si Animal no tuviera un constructor sin argumentos y Cat no escribiera super(...), el compilador se quejaría — no hay ningún Animal() que insertar implícitamente. El capítulo sobre la palabra clave super cubre el encadenamiento de constructores con super(...) y las llamadas super.method() en su totalidad.
Sobreescritura
Una subclase puede reemplazar un método heredado declarando uno con la misma firma:
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 es una anotación que le indica al compilador "pretendo que este método sobreescriba uno heredado — por favor, genera un error si no lo hace." Úsala siempre. Detecta errores tipográficos y discrepancias de firma que de otro modo crearían silenciosamente un nuevo método en lugar de sobreescribir el antiguo. El capítulo sobre sobreescritura de métodos cubre las reglas completas.
Upcasting y polimorfismo
Una instancia de subclase puede asignarse a una variable del tipo padre:
Animal a = new Cat(); // upcast — implicit
a.speak(); // calls Cat's speak() — picked at runtimeEsta es la base del polimorfismo, el siguiente capítulo. La variable a es de tipo Animal, pero el objeto real es un Cat, por lo que se ejecuta la versión Cat de speak.
Cuándo la herencia es la herramienta equivocada
La herencia es el mecanismo más sobreutilizado en la POO. Algunas señales de advertencia de que deberías usar composición (un campo de otro tipo) en su lugar:
- La subclase no supera realmente una prueba "es-un". Un
Stackno es realmente unVector— sin embargo,java.util.StackextiendeVectory es ampliamente considerado un error de diseño. - Los internos mutables del padre se filtran a la subclase. Los cambios en la implementación del padre rompen la subclase.
- Estás heredando para reutilizar algunos métodos, no porque los tipos sean genuinamente intercambiables.
La regla general según Effective Java de Joshua Bloch: prefiere la composición sobre la herencia. Si B necesita el comportamiento de A pero no es realmente un A, dale a B un campo privado de tipo A y reenvía lo que necesita.
// 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); }
}La versión con composición es inmune a sorpresas cuando ArrayList agrega nuevos métodos o cambia cómo funcionan sus campos privados.
Herencia y acceso
Los miembros heredados conservan el modificador que tenían en el padre. Una subclase no puede reducir la visibilidad de un método sobreescrito — hacer que un método public sea private en la subclase violaría el principio de sustitución de Liskov, y el compilador lo rechaza:
public class A {
public void hello() { }
}
public class B extends A {
private void hello() { } // ERROR — cannot reduce visibility
}Puedes ampliar la visibilidad (sobreescribir un método protected como public), pero raramente deberías hacerlo.
Un ejemplo práctico
Qué sigue
super apareció varias veces aquí — en el encadenamiento de constructores y como forma de acceder a un método padre sobreescrito. El siguiente capítulo sobre la palabra clave super recorre cada situación en la que aparece. Cuando un padre debe definir qué hacen sus hijos pero no cómo, se recurre a las clases abstractas, que se construyen directamente sobre las reglas de herencia cubiertas aquí.