Herencia en Java

📅 Actualizado en febrero 2026 ✍️ Ángel López ⏱️ 18 min de lectura ✓ Nivel intermedio ★ ★ ★ ★ ★ (5/5)

🧬 ¿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.

💡
Concepto clave: Cuando una clase B hereda de una clase A, B obtiene una copia de todos los miembros (atributos y métodos) no privados de A. B puede añadir nuevos miembros y también redefinir los métodos heredados para especializarlos.

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).

📸
Dato histórico: Dahl y Nygaard recibieron conjuntamente el Premio Turing de la ACM en 2001 «por sus ideas fundamentales para el nacimiento de la programación orientada a objetos, mediante el diseño y desarrollo de Simula I y Simula 67». Ambos fallecieron en 2002, con apenas semanas de diferencia. También recibieron la Medalla John von Neumann del IEEE en 2002 por la misma contribución.

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:

Java
// 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

Java
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)
Buena práctica: Declara los atributos de la clase padre como 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

Java
// 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

Java
// 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;
    }
}
⚠️
Advertencia: Java no permite escribir 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.

Java
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;
    }
}
💡
Regla importante: Si la clase padre no tiene un constructor sin argumentos (constructor por defecto), la clase hija está obligada a llamar explícitamente a 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:

Java
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.

Java
// 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.
💡
Concepto clave: Una clase hija de una clase abstracta no está obligada a implementar los métodos abstractos inmediatamente. Podría ser una clase nieta (u otra descendiente) la que complete la implementación. Lo que sí es obligatorio es que en algún punto de la cadena de herencia los métodos abstractos queden definidos, para poder crear objetos concretos.

🔄 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.
Java
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.

⚠️
Cuidado — sobrecarga ≠ redefinición: Si cambias los parámetros del método en la clase hija, no estás redefiniendo sino sobrecargando. Por ejemplo, si en 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.

Java — Sistema de gestión 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

Consola
═══════════ 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()

Java — ❌ Incorrecto
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'"
Java — ✅ Correcto
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

Java — ❌ Incorrecto
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
Solución: Usa siempre @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

Java — ❌ Incorrecto
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
    }
}
Java — ✅ Correcto
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

Java — ❌ Incorrecto
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:

Java
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) y saldo (double). Métodos: depositar(double), retirar(double) abstracto, y mostrarSaldo().
  • 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 Instrumento con atributos nombre, precio y método abstracto tocar().
  • Al menos 3 clases hijas: Guitarra, Piano, Bateria, cada una con atributos propios y redefinición de tocar().
  • Un main que cree un array polimórfico de Instrumento[], recorra todos los instrumentos llamando a tocar() y calcule el precio total del inventario.

❓ Preguntas frecuentes sobre Herencia en Java

Las dudas más comunes respondidas de forma clara y directa.

La herencia es un mecanismo de la POO que permite crear una clase nueva (clase hija) a partir de otra existente (clase padre), heredando sus atributos y métodos. Sirve para reutilizar código, establecer jerarquías de clases y aplicar polimorfismo.
Java no soporta herencia múltiple de clases para evitar el problema del diamante. Sin embargo, una clase puede implementar múltiples interfaces, lo que permite obtener un efecto similar de forma segura.
super() invoca al constructor de la clase padre, mientras que this() invoca a otro constructor de la misma clase. Ambos deben ser la primera instrucción del constructor y no pueden usarse juntos en el mismo constructor.
La sobrecarga (overloading) consiste en definir varios métodos con el mismo nombre pero distintos parámetros en la misma clase. La redefinición (overriding) consiste en que una clase hija redefine un método heredado de la clase padre con la misma firma exacta.
La anotación @Override indica al compilador que el método pretende redefinir uno de la clase padre. Si la firma no coincide exactamente, el compilador genera un error en tiempo de compilación, evitando errores difíciles de detectar en tiempo de ejecución.
Sí, una clase abstracta puede tener constructores. Aunque no se puede instanciar directamente, sus constructores se invocan mediante super() desde los constructores de las clases hijas concretas que la extienden.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Herencia en Java? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!