Herencia en Java
En esta lección
- El problema: código repetido
- extends: superclase y subclase
- Qué se hereda y qué no
- Constructores y super()
- Sobrescribir métodos con @Override
- super.metodo(): reutilizar la versión del padre
- Jerarquías de clases
- Object: la raíz de todas las clases
- final: impedir la herencia
- Herencia simple
- Herencia frente a composición
- Errores frecuentes
- Resumen
La herencia permite crear una clase nueva a partir de otra que ya existe, aprovechando todo lo que tiene y añadiendo o cambiando solo lo necesario. Es uno de los pilares de la programación orientada a objetos y la base de lo que verás después: clases abstractas, interfaces y polimorfismo.
En esta lección construiremos, paso a paso, el sistema de nóminas de una empresa con empleados, programadores y gerentes.
El problema: código repetido
Imagina que la empresa tiene programadores y gerentes. Sin herencia, escribirías dos clases casi iguales:
public class Programador {
private String nombre;
private double salarioBase;
private String lenguaje;
// getNombre(), calcularSalario()...
}
public class Gerente {
private String nombre;
private double salarioBase;
private double bonus;
// getNombre(), calcularSalario()... ¡otra vez!
}
El nombre, el salario base y sus métodos están duplicados. Si mañana hay que añadir el DNI a todos los empleados, tendrás que cambiarlo en cada clase, y es fácil olvidarse de alguna.
La solución es sacar lo común a una clase Empleado y hacer que las demás hereden de ella.
extends: superclase y subclase
public class Empleado {
private String nombre;
protected double salarioBase;
public Empleado(String nombre, double salarioBase) {
this.nombre = nombre;
this.salarioBase = salarioBase;
}
public String getNombre() { return nombre; }
public double calcularSalario() {
return salarioBase;
}
public String describir() {
return nombre + " cobra " + calcularSalario() + " €";
}
}
public class Programador extends Empleado {
private String lenguaje;
public Programador(String nombre, double salarioBase, String lenguaje) {
super(nombre, salarioBase);
this.lenguaje = lenguaje;
}
public String getLenguaje() { return lenguaje; }
}
Empleadoes la superclase (o clase padre, o clase base).Programadores la subclase (o clase hija, o clase derivada).extendssignifica “hereda de” o “es una ampliación de”.
Un Programador tiene todo lo de un Empleado más lo suyo:
Programador ana = new Programador("Ana", 2200, "Java");
System.out.println(ana.getNombre()); // Ana (heredado)
System.out.println(ana.getLenguaje()); // Java (propio)
System.out.println(ana.calcularSalario()); // 2200.0 (heredado)
System.out.println(ana.describir()); // Ana cobra 2200.0 €
Qué se hereda y qué no
| Elemento de la superclase | ¿Lo hereda la subclase? |
|---|---|
Atributos y métodos public | Sí, y puede usarlos directamente |
Atributos y métodos protected | Sí, y puede usarlos directamente |
| Atributos y métodos sin modificador | Solo si la subclase está en el mismo paquete |
Atributos y métodos private | Existen dentro del objeto, pero la subclase no puede tocarlos |
| Constructores | No se heredan nunca |
Lo de private sorprende al principio. Un Programador tiene un nombre (ocupa sitio en el objeto), pero su código no puede escribir nombre directamente:
public String saludo() {
return "Hola " + nombre; // error: nombre has private access in Empleado
}
Tiene que usar el getter público: "Hola " + getNombre().
protected
Si quieres que las subclases puedan usar un atributo directamente, pero que el resto del mundo no, decláralo protected. Es lo que hemos hecho con salarioBase. La tabla completa de modificadores está en paquetes y modificadores de acceso.
Consejo: aunque
protectedes cómodo, muchos programadores prefieren dejar los atributosprivatey dar a las subclases métodosprotectedpara leerlos o cambiarlos. Así la superclase sigue controlando sus datos.
Constructores y super()
Los constructores no se heredan, así que cada subclase escribe el suyo. Pero la parte “de empleado” del objeto la tiene que inicializar el constructor de Empleado. Para llamarlo se usa super(...):
public Programador(String nombre, double salarioBase, String lenguaje) {
super(nombre, salarioBase); // primero, la parte de Empleado
this.lenguaje = lenguaje; // después, lo propio
}
Reglas:
super(...)debe ser la primera instrucción del constructor (en Java 21; Java 25 permite algunas instrucciones antes, siempre que no usenthis).- Si no lo escribes, Java añade por su cuenta
super()sin argumentos. Si la superclase no tiene un constructor sin parámetros, el programa no compila:
public class Becario extends Empleado {
public Becario(String nombre) { }
// error: constructor Empleado in class Empleado cannot be applied to given types;
}
Orden de construcción
Al crear un objeto de una subclase, siempre se construye primero la parte del padre. Si añadimos un mensaje a cada constructor:
// En Empleado: System.out.println("Creando empleado " + nombre);
// En Programador: System.out.println("Creando programador de " + lenguaje);
Programador ana = new Programador("Ana", 2200, "Java");
// Creando empleado Ana
// Creando programador de Java
Tiene sentido: la casa se construye desde los cimientos.
Sobrescribir métodos con @Override
Los gerentes cobran su salario base más un bonus. El método heredado calcularSalario() no nos vale tal cual, así que lo sobrescribimos: escribimos en la subclase un método con la misma firma (mismo nombre, mismos parámetros y el mismo tipo de retorno, o uno más concreto).
public class Gerente extends Empleado {
private double bonus;
public Gerente(String nombre, double salarioBase, double bonus) {
super(nombre, salarioBase);
this.bonus = bonus;
}
@Override
public double calcularSalario() {
return salarioBase + bonus;
}
}
Gerente luis = new Gerente("Luis", 3000, 500);
System.out.println(luis.calcularSalario()); // 3500.0
System.out.println(luis.describir()); // Luis cobra 3500.0 €
Fíjate en la última línea. describir() está escrito en Empleado y llama a calcularSalario(); como el objeto es un Gerente, se ejecuta la versión del gerente. Esta idea es la base del polimorfismo.
Para qué sirve @Override
@Override es una anotación: le dice al compilador “este método debería sobrescribir uno del padre”. No es obligatoria, pero te protege de erratas:
@Override
public double calcularSalaro() { ... }
// error: method does not override or implement a method from a supertype
Sin la anotación, Java pensaría que es un método nuevo, compilaría sin quejarse y el gerente seguiría cobrando sin bonus. Ponla siempre.
Cuidado: al sobrescribir no puedes hacer el método más restrictivo. Si en el padre es
public, en el hijo debe seguir siendopublic.
super.metodo(): reutilizar la versión del padre
A veces no quieres sustituir el método del padre, sino ampliarlo. Con super.metodo() llamas a la versión de la superclase:
@Override
public double calcularSalario() {
return super.calcularSalario() + bonus; // lo del padre + el bonus
}
@Override
public String describir() {
return super.describir() + " (gerente)";
}
System.out.println(luis.describir()); // Luis cobra 3500.0 € (gerente)
Así, si algún día cambia la forma de calcular el salario base en Empleado, el gerente lo aprovecha sin tocar su código.
Jerarquías de clases
Una subclase puede tener a su vez subclases. Un director es un gerente con un 10 % extra:
public class Director extends Gerente {
public Director(String nombre, double salarioBase, double bonus) {
super(nombre, salarioBase, bonus);
}
@Override
public double calcularSalario() {
return super.calcularSalario() * 1.10;
}
}
Director eva = new Director("Eva", 4000, 1000);
System.out.println(eva.calcularSalario()); // 5500.0
La jerarquía queda así:
Object
└── Empleado
├── Programador
└── Gerente
└── Director
Un Director es un Gerente, que es un Empleado. Evita jerarquías muy profundas: con dos o tres niveles suele bastar. Cuantos más niveles, más difícil es saber de dónde viene cada comportamiento.
Object: la raíz de todas las clases
Si una clase no pone extends, Java hace que herede de Object. Por eso todas las clases heredan, directa o indirectamente, de Object, y todos los objetos tienen algunos métodos aunque tú no los escribas. Los más importantes son:
| Método | Qué hace por defecto |
|---|---|
toString() | Devuelve algo como Empleado@45ee12a7 |
equals(Object o) | Compara si son el mismo objeto en memoria (como ==) |
hashCode() | Devuelve un número asociado al objeto; debe ser coherente con equals |
Sobrescribir toString()
System.out.println(objeto) llama a toString(). La versión de Object no es muy útil, así que se suele sobrescribir:
@Override
public String toString() {
return "Empleado[" + getNombre() + "]";
}
System.out.println(ana); // Empleado[Ana]
Sobrescribir equals()
Dos empleados con el mismo nombre (en un caso real, el mismo DNI) deberían considerarse iguales, pero == y el equals heredado dicen que no porque son objetos distintos:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Empleado otro)) return false;
return nombre.equals(otro.nombre);
}
@Override
public int hashCode() {
return Objects.hash(nombre); // import java.util.Objects;
}
Empleado e1 = new Empleado("Marta", 1800);
Empleado e2 = new Empleado("Marta", 1800);
System.out.println(e1 == e2); // false: dos objetos distintos
System.out.println(e1.equals(e2)); // true: mismos datos
Si sobrescribes equals, sobrescribe también hashCode; si no, los objetos se comportarán mal en un HashSet o un HashMap (lo verás en mapas y conjuntos). El instanceof Empleado otro es pattern matching, que se explica en polimorfismo.
final: impedir la herencia
Como viste en encapsulación, final bloquea la herencia:
- Una clase
finalno puede tener subclases.Stringesfinalpara que nadie pueda cambiar su comportamiento. - Un método
finalno se puede sobrescribir, aunque la clase sí se pueda heredar.
public class Empleado {
public final String getNombre() { return nombre; } // ninguna subclase podrá cambiarlo
}
Úsalo cuando cambiar un comportamiento en una subclase pueda romper algo importante.
Herencia simple
En Java una clase solo puede tener una superclase. Esto no compila:
public class Becario extends Empleado, Estudiante { } // error de sintaxis
Otros lenguajes, como C++, permiten herencia múltiple, pero trae problemas: si las dos superclases tienen un método con el mismo nombre, ¿cuál se hereda? Java lo evita. Para que una clase “sea” varias cosas a la vez se usan interfaces, que verás en clases abstractas e interfaces.
Herencia frente a composición
La herencia es potente, pero no siempre es la herramienta adecuada. Hazte esta pregunta:
- “Es un” → herencia. Un programador es un empleado.
- “Tiene un” → composición: la clase guarda un objeto de la otra como atributo. Un departamento tiene empleados, pero no es un empleado.
public class Departamento {
private String nombre;
private List<Empleado> empleados = new ArrayList<>(); // tiene empleados
public Departamento(String nombre) { this.nombre = nombre; }
public void contratar(Empleado e) { empleados.add(e); }
public int getTamanio() { return empleados.size(); }
}
Departamento it = new Departamento("Informática");
it.contratar(new Empleado("Ana", 2200));
it.contratar(new Empleado("Luis", 3000));
System.out.println(it.getTamanio()); // 2
Hacer Departamento extends ArrayList<Empleado> “funcionaría”, pero expondría decenas de métodos de lista que no tienen sentido para un departamento. Por eso se suele decir: prefiere la composición a la herencia, y usa herencia solo cuando la relación “es un” sea clara.
Errores frecuentes
- No llamar a
super(...)cuando el padre no tiene constructor sin parámetros. El compilador se queja de que el constructor del padre no se puede aplicar. - Poner
super(...)después de otra instrucción. Debe ir la primera. - Acceder a atributos
privatedel padre. Usa los getters o hazlosprotected. - Olvidar
@Overridey escribir mal el nombre. Creas un método nuevo sin darte cuenta. - Sobrescribir
equalssinhashCode. Los conjuntos y mapas dejan de funcionar bien. - Heredar solo para reutilizar código cuando la relación es “tiene un”. Usa composición.
Resumen
| Concepto | Idea clave |
|---|---|
extends | La subclase hereda atributos y métodos de la superclase |
super(...) | Llama al constructor del padre; primera línea del constructor |
super.metodo() | Ejecuta la versión del padre de un método |
@Override | Marca un método sobrescrito y detecta erratas |
protected | Visible para subclases y para el paquete |
Object | Raíz de todas las clases: toString, equals, hashCode |
final | Clase que no se hereda o método que no se sobrescribe |
| Herencia simple | Solo una superclase; para más, interfaces |
| Composición | “Tiene un”: guardar un objeto como atributo |
En la siguiente lección verás cómo definir clases que solo sirven de plantilla, las clases abstractas e interfaces.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.