🧬 ¿Qué es la herencia en Java?
La herencia es uno de los cuatro pilares fundamentales de la Programación Orientada a Objetos, junto con la abstracción, el encapsulamiento y el polimorfismo. En Java, la herencia es el mecanismo que permite crear una nueva clase —denominada clase hija o subclase— a partir de una clase existente —denominada clase padre o superclase—, de modo que la clase hija recibe automáticamente todos los atributos y métodos no privados de la clase padre.
Gracias a la herencia podemos establecer jerarquías de clases que modelan relaciones del tipo «es un» (is-a). Por ejemplo, un Perro «es un» Animal, un Ingeniero «es un» Empleado y un Rectangulo «es una» FiguraGeometrica. Esta capacidad de generalizar y especializar permite reutilizar código existente y extenderlo sin modificar las clases ya probadas, cumpliendo el principio abierto/cerrado (Open/Closed Principle) de la ingeniería del software.
Java implementa la herencia mediante la palabra clave extends. Todo objeto en Java hereda, directa o indirectamente, de la clase java.lang.Object, que constituye la raíz de la jerarquía de clases del lenguaje. Esto significa que métodos como toString(), equals() y hashCode() están disponibles en cualquier objeto Java.
📜 Origen histórico de la herencia en POO
El concepto de herencia en la programación orientada a objetos tiene sus raíces en el lenguaje Simula, desarrollado a mediados de la década de 1960 por los científicos noruegos Ole-Johan Dahl (1931–2002) y Kristen Nygaard (1926–2002) en el Centro Noruego de Computación de Oslo. Mientras trabajaban en simulaciones de sistemas complejos —entre ellos simulaciones de naves—, descubrieron que agrupar datos y comportamiento en clases de objetos y permitir que unas clases heredaran de otras simplificaba enormemente la complejidad del código.
Simula I (1962–1965) introdujo versiones primitivas de estos conceptos, pero fue Simula 67 (1967) el que formalizó la herencia de clases tal como la conocemos hoy: una clase podía declarar una superclase de la que heredaba atributos y comportamiento, y las subclases podían añadir o redefinir métodos. Este diseño influyó directamente en Smalltalk (Alan Kay, Xerox PARC, década de 1970), C++ (Bjarne Stroustrup, 1983) y, finalmente, en Java (James Gosling, Sun Microsystems, 1995).
James Gosling, al diseñar Java, tomó la decisión deliberada de soportar únicamente herencia simple de clases (una clase solo puede extender a otra) para evitar la complejidad y ambigüedad que genera la herencia múltiple de C++. Como alternativa, Java introdujo las interfaces, que permiten a una clase comprometerse a implementar múltiples contratos sin los problemas del conocido «problema del diamante».
⌨️ Sintaxis: la palabra clave extends
Para declarar que una clase hereda de otra en Java, se utiliza la palabra reservada extends en la declaración de la clase hija. La sintaxis general es:
// Clase padre (superclase)
public class Animal {
protected String nombre;
protected int edad;
public Animal(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
public void comer() {
System.out.println(nombre + " está comiendo.");
}
public void dormir() {
System.out.println(nombre + " está durmiendo.");
}
@Override
public String toString() {
return nombre + " (" + edad + " años)";
}
}
// Clase hija (subclase) — hereda de Animal
public class Perro extends Animal {
private String raza;
public Perro(String nombre, int edad, String raza) {
super(nombre, edad); // Invoca al constructor de Animal
this.raza = raza;
}
// Método propio de Perro (no existe en Animal)
public void ladrar() {
System.out.println(nombre + " dice: ¡Guau, guau!");
}
@Override
public String toString() {
return nombre + " (" + raza + ", " + edad + " años)";
}
}
En este ejemplo, Perro hereda de Animal los atributos nombre y edad (accesibles porque son protected) y los métodos comer() y dormir(). Además, Perro añade un atributo propio (raza) y un método propio (ladrar()), y redefine toString() para incluir la raza.
▶️ Uso desde el método main
public class Main {
public static void main(String[] args) {
Perro miPerro = new Perro("Rex", 5, "Pastor Alemán");
miPerro.comer(); // Heredado de Animal
miPerro.dormir(); // Heredado de Animal
miPerro.ladrar(); // Propio de Perro
System.out.println(miPerro); // Usa el toString() redefinido
}
}
// Salida:
// Rex está comiendo.
// Rex está durmiendo.
// Rex dice: ¡Guau, guau!
// Rex (Pastor Alemán, 5 años)
protected (en lugar de public) cuando necesites que las clases hijas accedan directamente a ellos, pero que el resto de clases externas no puedan. Si prefieres máximo encapsulamiento, usa private y proporciona getters/setters.🔀 Tipos de herencia en Java
En la teoría de la POO existen varios tipos de herencia. Sin embargo, Java solo soporta directamente algunos de ellos. Es fundamental entender esta distinción:
| Tipo de herencia | Descripción | ¿Soportado en Java? |
|---|---|---|
| Simple | Una clase hija hereda de una única clase padre. | ✅ Sí |
| Multinivel | Una clase hereda de otra que a su vez hereda de otra (cadena A → B → C). | ✅ Sí |
| Jerárquica | Varias clases hijas heredan de una misma clase padre. | ✅ Sí |
| Múltiple | Una clase hereda de dos o más clases padre simultáneamente. | ❌ No (se usa interfaces) |
| Híbrida | Combinación de herencia múltiple con otros tipos. | ❌ No directamente |
🔹 Ejemplo de herencia multinivel
// Nivel 1: clase raíz
public class SerVivo {
protected boolean estaVivo = true;
public void respirar() {
System.out.println("Respirando...");
}
}
// Nivel 2: hereda de SerVivo
public class Animal extends SerVivo {
protected String especie;
public void moverse() {
System.out.println("El animal se mueve.");
}
}
// Nivel 3: hereda de Animal (que hereda de SerVivo)
public class Gato extends Animal {
private String color;
public Gato(String especie, String color) {
this.especie = especie; // Heredado de Animal
this.color = color;
}
public void maullar() {
System.out.println("¡Miau!");
}
}
// Uso:
// Gato g = new Gato("Felino", "Naranja");
// g.respirar(); // De SerVivo
// g.moverse(); // De Animal
// g.maullar(); // De Gato
🔹 Ejemplo de herencia jerárquica
// Una clase padre, varias hijas
public class FiguraGeometrica {
protected String color;
public FiguraGeometrica(String color) {
this.color = color;
}
public double calcularArea() {
return 0; // Será redefinido por cada hija
}
}
public class Circulo extends FiguraGeometrica {
private double radio;
public Circulo(String color, double radio) {
super(color);
this.radio = radio;
}
@Override
public double calcularArea() {
return Math.PI * radio * radio;
}
}
public class Rectangulo extends FiguraGeometrica {
private double ancho, alto;
public Rectangulo(String color, double ancho, double alto) {
super(color);
this.ancho = ancho;
this.alto = alto;
}
@Override
public double calcularArea() {
return ancho * alto;
}
}
class C extends A, B. Si necesitas que una clase cumpla múltiples contratos, debes usar interfaces: class C extends A implements InterfazB, InterfazC. Las interfaces con métodos default (Java 8+) permiten reutilizar comportamiento sin herencia múltiple de clases.🔑 La palabra clave super
La palabra reservada super en Java tiene dos usos principales dentro de una clase hija:
▶️ 1. Invocar al constructor de la clase padre
Cuando una clase hija necesita inicializar los atributos heredados, debe llamar al constructor de la clase padre mediante super(argumentos). Esta llamada debe ser la primera instrucción del constructor de la clase hija.
public class Vehiculo {
protected String marca;
protected int anio;
public Vehiculo(String marca, int anio) {
this.marca = marca;
this.anio = anio;
}
}
public class Coche extends Vehiculo {
private int numPuertas;
public Coche(String marca, int anio, int numPuertas) {
super(marca, anio); // OBLIGATORIO: llama al constructor de Vehiculo
this.numPuertas = numPuertas;
}
}
super(...) con los argumentos adecuados. Si no lo hace, el compilador genera un error.▶️ 2. Acceder a miembros de la clase padre
Si una clase hija redefine un método o tiene un atributo con el mismo nombre que uno de la clase padre, puede usar super.metodo() o super.atributo para referirse explícitamente a la versión de la clase padre:
public class Empleado {
protected double salarioBase;
public Empleado(double salarioBase) {
this.salarioBase = salarioBase;
}
public double calcularSalario() {
return salarioBase;
}
public String describir() {
return "Empleado con salario base: " + salarioBase + " €";
}
}
public class Gerente extends Empleado {
private double bonificacion;
public Gerente(double salarioBase, double bonificacion) {
super(salarioBase);
this.bonificacion = bonificacion;
}
@Override
public double calcularSalario() {
return super.calcularSalario() + bonificacion; // Reutiliza el cálculo padre
}
@Override
public String describir() {
return super.describir() + " + bonificación: " + bonificacion + " €";
}
}
En el ejemplo anterior, Gerente.calcularSalario() no repite la lógica de la clase padre sino que la invoca con super.calcularSalario() y le añade la bonificación. Este patrón es muy común y fomenta el principio DRY (Don't Repeat Yourself).
🏗️ Clases abstractas y métodos abstractos
Al construir jerarquías de herencia, es frecuente que la clase padre represente un concepto tan general que no tenga sentido crear objetos directamente de ella. Por ejemplo, no tiene sentido crear un objeto FiguraGeometrica genérico sin especificar si es un círculo, un triángulo o un rectángulo. En estos casos, Java permite declarar la clase como abstracta.
// Clase abstracta: no se puede instanciar
public abstract class FiguraGeometrica {
protected String color;
public FiguraGeometrica(String color) {
this.color = color;
}
// Método abstracto: sin cuerpo, OBLIGA a las hijas a implementarlo
public abstract double calcularArea();
// Método abstracto
public abstract double calcularPerimetro();
// Método concreto: las hijas lo heredan tal cual (pueden redefinirlo)
public void mostrarInfo() {
System.out.println("Figura de color " + color);
System.out.println("Área: " + calcularArea());
System.out.println("Perímetro: " + calcularPerimetro());
}
}
// Clase concreta: DEBE implementar todos los métodos abstractos
public class Triangulo extends FiguraGeometrica {
private double ladoA, ladoB, ladoC;
public Triangulo(String color, double a, double b, double c) {
super(color);
this.ladoA = a;
this.ladoB = b;
this.ladoC = c;
}
@Override
public double calcularArea() {
// Fórmula de Herón
double s = (ladoA + ladoB + ladoC) / 2;
return Math.sqrt(s * (s - ladoA) * (s - ladoB) * (s - ladoC));
}
@Override
public double calcularPerimetro() {
return ladoA + ladoB + ladoC;
}
}
Las reglas fundamentales de las clases abstractas en Java son:
| Regla | Descripción |
|---|---|
| No instanciable | No se puede hacer new FiguraGeometrica(...). |
| Puede tener constructores | Las clases hijas los invocan con super(). |
| Puede mezclar métodos | Puede tener métodos abstractos (sin cuerpo) y concretos (con cuerpo). |
| Herencia obligatoria | Si una clase hija no implementa todos los métodos abstractos, también debe declararse abstract. |
Palabra clave abstract |
Se coloca antes de class y antes de los métodos sin cuerpo. |
🔄 Redefinición de métodos (Override)
La redefinición de métodos (en inglés method overriding) es la capacidad de una clase hija de proporcionar su propia implementación de un método heredado de la clase padre. Para que la redefinición sea válida, deben cumplirse estas condiciones:
| Condición | Detalle |
|---|---|
| Mismo nombre | El método en la hija debe tener exactamente el mismo nombre. |
| Mismos parámetros | La lista de parámetros (tipo y orden) debe ser idéntica. |
| Mismo tipo de retorno (o covariante) | Puede devolver el mismo tipo o un subtipo del tipo de retorno del padre. |
| Modificador de acceso | No puede ser más restrictivo que el del padre (si el padre es protected, la hija puede ser protected o public, pero no private). |
Anotación @Override |
Opcional pero muy recomendada. El compilador verifica que la firma coincida. |
public class Ave extends Animal {
public Ave(String nombre, int edad) {
super(nombre, edad);
}
// Redefinición: cambia el comportamiento heredado
@Override
public void comer() {
System.out.println(nombre + " picotea su comida con el pico.");
}
// Método propio
public void volar() {
System.out.println(nombre + " extiende sus alas y vuela.");
}
}
// Demostración de polimorfismo con herencia:
public class Main {
public static void main(String[] args) {
Animal a1 = new Perro("Rex", 5, "Pastor Alemán");
Animal a2 = new Ave("Piolín", 2);
a1.comer(); // Rex está comiendo. (versión de Animal)
a2.comer(); // Piolín picotea su comida. (versión redefinida de Ave)
}
}
Observa cómo ambas variables (a1 y a2) son de tipo Animal, pero al llamar a comer() se ejecuta la versión correspondiente a la clase real del objeto. Esto es el polimorfismo en acción, y solo es posible gracias a la herencia y la redefinición de métodos.
Ave escribes public void comer(String tipo), estás creando un método nuevo, no redefiniendo el comer() sin parámetros de Animal. La anotación @Override te protege de este error.🏢 Ejemplo integrador: sistema de gestión de empleados
Veamos un ejemplo completo que combina todos los conceptos anteriores en un escenario empresarial realista: un sistema de nóminas con distintos tipos de empleados.
// ─── Clase abstracta padre ─────────────────────────────────
public abstract class Empleado {
protected String nombre;
protected String departamento;
protected double salarioBase;
public Empleado(String nombre, String departamento, double salarioBase) {
this.nombre = nombre;
this.departamento = departamento;
this.salarioBase = salarioBase;
}
// Método abstracto: cada tipo de empleado calcula su salario
public abstract double calcularSalarioMensual();
// Método concreto compartido
public void mostrarNomina() {
System.out.printf("%-20s | %-15s | Salario: %,.2f €%n",
nombre, departamento, calcularSalarioMensual());
}
@Override
public String toString() {
return nombre + " [" + departamento + "]";
}
}
// ─── Empleado a tiempo completo ────────────────────────────
public class EmpleadoCompleto extends Empleado {
private double bonificacion;
public EmpleadoCompleto(String nombre, String depto,
double salarioBase, double bonificacion) {
super(nombre, depto, salarioBase);
this.bonificacion = bonificacion;
}
@Override
public double calcularSalarioMensual() {
return salarioBase + bonificacion;
}
}
// ─── Empleado a tiempo parcial ─────────────────────────────
public class EmpleadoParcial extends Empleado {
private int horasTrabajadas;
private double precioPorHora;
public EmpleadoParcial(String nombre, String depto,
int horas, double precioHora) {
super(nombre, depto, 0); // Sin salario base fijo
this.horasTrabajadas = horas;
this.precioPorHora = precioHora;
}
@Override
public double calcularSalarioMensual() {
return horasTrabajadas * precioPorHora;
}
}
// ─── Gerente (herencia multinivel: Empleado → EmpleadoCompleto → Gerente)
public class Gerente extends EmpleadoCompleto {
private int empleadosACargo;
private static final double PLUS_POR_EMPLEADO = 50.0;
public Gerente(String nombre, String depto,
double salarioBase, double bonificacion,
int empleadosACargo) {
super(nombre, depto, salarioBase, bonificacion);
this.empleadosACargo = empleadosACargo;
}
@Override
public double calcularSalarioMensual() {
return super.calcularSalarioMensual()
+ (empleadosACargo * PLUS_POR_EMPLEADO);
}
}
// ─── Programa principal ────────────────────────────────────
public class SistemaNominas {
public static void main(String[] args) {
// Array polimórfico: todos son "Empleado"
Empleado[] plantilla = {
new EmpleadoCompleto("Ana García", "Desarrollo", 2800, 400),
new EmpleadoParcial("Carlos López", "Soporte", 80, 15),
new Gerente("María Torres", "Dirección", 3500, 600, 12),
new EmpleadoCompleto("Pedro Ruiz", "Marketing", 2500, 300),
};
System.out.println("═══════════ NÓMINA MENSUAL ═══════════");
double totalNomina = 0;
for (Empleado emp : plantilla) {
emp.mostrarNomina();
totalNomina += emp.calcularSalarioMensual();
}
System.out.println("══════════════════════════════════════");
System.out.printf("TOTAL NÓMINA EMPRESA: %,.2f €%n", totalNomina);
}
}
📋 Salida esperada
═══════════ NÓMINA MENSUAL ═══════════ Ana García | Desarrollo | Salario: 3.200,00 € Carlos López | Soporte | Salario: 1.200,00 € María Torres | Dirección | Salario: 4.700,00 € Pedro Ruiz | Marketing | Salario: 2.800,00 € ══════════════════════════════════════ TOTAL NÓMINA EMPRESA: 11.900,00 €
Este ejemplo demuestra el poder de la herencia combinada con polimorfismo. El bucle for recorre un array de Empleado, pero cada elemento ejecuta su propia versión de calcularSalarioMensual(). Si mañana se añade un nuevo tipo de empleado (por ejemplo, EmpleadoBecario), basta con crear la nueva clase que extienda Empleado; el bucle de nóminas no necesita modificarse.
🐛 Errores frecuentes con herencia
❌ Error 1: olvidar llamar a super()
public class Coche extends Vehiculo {
private int numPuertas;
public Coche(String marca, int anio, int numPuertas) {
// ❌ ERROR: Vehiculo no tiene constructor sin argumentos
// El compilador busca super() implícitamente y no lo encuentra
this.numPuertas = numPuertas;
}
}
// Error de compilación:
// "There is no default constructor available in 'Vehiculo'"
public class Coche extends Vehiculo {
private int numPuertas;
public Coche(String marca, int anio, int numPuertas) {
super(marca, anio); // ✅ Llamada explícita al constructor padre
this.numPuertas = numPuertas;
}
}
❌ Error 2: confundir sobrecarga con redefinición
public class Ave extends Animal {
// ❌ Esto NO redefine comer(), sino que SOBRECARGA (parámetros distintos)
public void comer(String alimento) {
System.out.println(nombre + " come " + alimento);
}
}
// Al llamar: new Ave("Loro", 3).comer();
// Se ejecuta la versión de Animal, NO la de Ave
@Override encima de los métodos que pretendes redefinir. Si la firma no coincide exactamente con la del padre, el compilador te avisará con un error en tiempo de compilación.❌ Error 3: acceder a miembros private del padre
public class Persona {
private String dni; // Privado: NO visible para hijas
}
public class Estudiante extends Persona {
public void mostrarDni() {
System.out.println(dni); // ❌ ERROR: 'dni' has private access
}
}
public class Persona {
private String dni;
// Acceso controlado mediante getter
public String getDni() {
return dni;
}
}
public class Estudiante extends Persona {
public void mostrarDni() {
System.out.println(getDni()); // ✅ Accede a través del getter
}
}
❌ Error 4: intentar heredar de una clase final
public final class Constantes {
public static final double PI = 3.14159;
}
// ❌ ERROR: Cannot inherit from final class 'Constantes'
public class MisConstantes extends Constantes { }
La palabra clave final en una clase impide que sea extendida. Clases como String, Integer y Math son final en Java por razones de seguridad e inmutabilidad. De forma análoga, un método declarado final no puede ser redefinido en clases hijas.
✏️ Ejercicios prácticos
Ejercicio 1: ¿Qué imprime este código? (Comprensión)
Analiza el siguiente código y determina la salida sin ejecutarlo:
class A {
public A() { System.out.print("A "); }
public void saludar() { System.out.print("Hola-A "); }
}
class B extends A {
public B() { System.out.print("B "); }
@Override
public void saludar() { System.out.print("Hola-B "); }
}
class C extends B {
public C() { System.out.print("C "); }
}
public class Test {
public static void main(String[] args) {
C obj = new C();
obj.saludar();
}
}
Ejercicio 2: Sistema de cuentas bancarias (Aplicación)
Crea una jerarquía de clases para un banco:
- CuentaBancaria (abstracta): atributos
titular(String) ysaldo(double). Métodos:depositar(double),retirar(double)abstracto, ymostrarSaldo(). - CuentaAhorro: extiende CuentaBancaria.
retirar()cobra un 1% de comisión sobre el importe retirado. - CuentaCorriente: extiende CuentaBancaria.
retirar()permite descubierto hasta -500 €.
Escribe un main que cree una cuenta de cada tipo, realice operaciones y muestre los saldos finales.
Ejercicio 3: Diseña tu propia jerarquía (Diseño)
Diseña e implementa una jerarquía de clases para una tienda de instrumentos musicales:
- Clase abstracta
Instrumentocon atributosnombre,precioy método abstractotocar(). - Al menos 3 clases hijas:
Guitarra,Piano,Bateria, cada una con atributos propios y redefinición detocar(). - Un
mainque cree un array polimórfico deInstrumento[], recorra todos los instrumentos llamando atocar()y calcule el precio total del inventario.
❓ Preguntas frecuentes sobre Herencia en Java
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Herencia en Java? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!