Nexus Software acaba de comunicar una novedad a BiblioTech: la biblioteca técnica no presta solo libros. También presta revistas especializadas —que se devuelven en una semana porque circulan mucho— y DVD de formación, que se prestan tres días y cuya tarifa de retraso es el doble, porque hay pocas copias. Con lo que sabes hasta ahora, la única salida sería duplicar la clase Libro dos veces, cambiando dos números y el nombre: tres clases casi idénticas, con la misma lógica de disponibilidad copiada tres veces y tres sitios donde corregir cada error. La herencia es el mecanismo con el que Java evita exactamente eso: permite definir una clase como una especialización de otra, heredando todo lo que ya tiene y declarando únicamente aquello en lo que difiere. Es el pilar más potente de la POO y también el más fácil de usar mal, así que esta lección te enseña tanto a aplicarlo como a saber cuándo no aplicarlo.

Contenido

  1. La relación "es un"
  2. extends: sintaxis y primer ejemplo
  3. Qué se hereda y qué no
  4. super: el constructor de la superclase
  5. super: acceder a miembros de la superclase
  6. Orden de construcción en una jerarquía
  7. Sobrescritura de métodos y @Override
  8. Sobrescribir frente a sobrecargar
  9. final en clases y métodos
  10. Toda clase hereda de Object
  11. Herencia frente a composición
  12. Jerarquías profundas y la fragilidad de la clase base
  13. BiblioTech: la jerarquía de materiales prestables
  14. Errores Comunes y Consejos
  15. Ejercicios

  1. La relación "es un"

La herencia modela una relación muy concreta: "X es un Y". Si la frase suena natural y es verdadera siempre, la herencia es candidata:

  • Un libro es un material prestable. ✔
  • Una revista es un material prestable. ✔
  • Un DVD es un material prestable. ✔
  • Un préstamo es un libro. ✘ (un préstamo tiene un libro)
  • Un empleado es un libro. ✘

Vocabulario:

Término Sinónimos Significado
Superclase clase base, clase padre La clase general de la que se hereda
Subclase clase derivada, clase hija La clase especializada que hereda
Herencia extensión El mecanismo que las une

Y una regla que Java impone y conviene conocer desde el principio: una clase solo puede extender una superclase. No hay herencia múltiple de clases (sí la hay de interfaces, lección 04-01). Esta restricción evita ambigüedades famosas de otros lenguajes.

  1. extends: sintaxis y primer ejemplo

public class Material {
    public String titulo;
    public boolean disponible;

    public void prestar() {
        disponible = false;
    }
}
public class Revista extends Material {
    public int numero;          // campo propio, ademas de los heredados
}

Con solo esa línea, Revista dispone de titulo, disponible y prestar() sin haberlos escrito:

Revista r = new Revista();
r.titulo     = "Java Magazine";   // heredado
r.numero     = 42;                // propio
r.prestar();                      // heredado
System.out.println(r.disponible); // false

La subclase es igual o más grande que la superclase: nunca menos. Puede añadir campos, añadir métodos y cambiar el comportamiento de los heredados, pero no puede eliminar nada.

  1. Qué se hereda y qué no

Una tabla que resuelve la mayoría de las dudas:

Elemento de la superclase ¿Se hereda? Matiz
Campos public y protected Accesibles directamente desde la subclase
Campos private Existen, pero no son accesibles El objeto los contiene; la subclase debe usar métodos para llegar a ellos
Campos y métodos de paquete (default) Solo si la subclase está en el mismo paquete Ver 03-07
Métodos public y protected Se pueden sobrescribir
Métodos static Se heredan, pero no se sobrescriben Se "ocultan" (lección 03-06)
Constructores No Hay que declararlos en cada subclase, aunque se pueden invocar con super(...)
Bloques de inicialización Se ejecutan como parte de la construcción No se "heredan" como miembros

El punto que más confunde es el de los campos private. Un objeto Revista sí contiene los campos privados de Material —ocupan memoria y se inicializan—, pero el código de Revista no puede nombrarlos. Debe usar los métodos que la superclase exponga. No es una limitación caprichosa: es encapsulamiento (03-07), y permite cambiar la representación interna de Material sin romper a sus hijas.

El otro punto es el de los constructores: no se heredan. Si Material tiene un constructor con tres parámetros, new Revista(a, b, c) no funciona por arte de magia; Revista debe declarar su propio constructor.

  1. super: el constructor de la superclase

super(...) invoca un constructor de la superclase. Y es obligatorio en el siguiente sentido:

Todo constructor de una subclase empieza llamando a un constructor de su superclase. Si no lo escribes tú, el compilador inserta una llamada implícita a super() sin argumentos.

public class Material {
    public final String titulo;
    public final String referencia;
    public boolean disponible;

    public Material(String titulo, String referencia) {
        this.titulo     = titulo;
        this.referencia = referencia;
        this.disponible = true;
    }
}
public class Revista extends Material {
    public final int numero;

    public Revista(String titulo, String referencia, int numero) {
        super(titulo, referencia);   // PRIMERA sentencia, obligatoria aqui
        this.numero = numero;
    }
}

Reglas, gemelas de las de this(...) que viste en 03-04:

  1. super(...) debe ser la primera sentencia del constructor.
  2. No pueden coexistir super(...) y this(...) en el mismo constructor: si usas this(...), la cadena acabará en algún constructor que llame a super(...).
  3. Si la superclase no tiene constructor sin argumentos y la subclase no llama explícitamente a super(...), hay error de compilación:
public class Revista extends Material {
    public Revista() { }    // ERROR
}
error: constructor Material in class Material cannot be applied to given types;
  required: String,String
  found:    no arguments

Este error, muy frecuente, tiene una lectura clara: "has creado una subclase de algo que exige título y referencia, pero no se los estás dando".

  1. super: acceder a miembros de la superclase

super también sirve, fuera de los constructores, para llegar a un miembro de la superclase que la subclase ha sobrescrito:

public class Material {
    public String describir() {
        return titulo + " (ref. " + referencia + ")";
    }
}

public class Revista extends Material {
    public int numero;

    @Override
    public String describir() {
        return super.describir() + " - numero " + numero;   // reutiliza y amplia
    }
}

Este patrón —llamar a super.metodo() y añadir algo— es uno de los usos más valiosos de la herencia: no duplicas la lógica de la superclase, la extiendes.

Revista r = new Revista("Java Magazine", "REV-2024-42", 42);
System.out.println(r.describir());
// Java Magazine (ref. REV-2024-42) - numero 42

  1. Orden de construcción en una jerarquía

Cuando creas un objeto de una subclase, la construcción va de arriba abajo: primero se construye la parte heredada, después la propia. Compruébalo con trazas:

public class Material {
    public Material() {
        System.out.println("2. Constructor de Material");
    }
    { System.out.println("1. Bloque de instancia de Material"); }
}

public class Libro extends Material {
    public Libro() {
        super();                                   // implicito si no se escribe
        System.out.println("4. Constructor de Libro");
    }
    { System.out.println("3. Bloque de instancia de Libro"); }
}
new Libro();

Salida:

1. Bloque de instancia de Material
2. Constructor de Material
3. Bloque de instancia de Libro
4. Constructor de Libro
flowchart TD
    A["new Libro()"] --> B["Constructor de Libro
    llama a super() como primera sentencia"]
    B --> C["Bloques e inicializadores de Material"]
    C --> D["Cuerpo del constructor de Material"]
    D --> E["Bloques e inicializadores de Libro"]
    E --> F["Cuerpo del constructor de Libro"]
    F --> G["Objeto completo"]

Este orden explica el consejo que quedó pendiente en 03-04: no llames a métodos sobrescribibles desde un constructor. Si Material llamara en su constructor a un método que Libro sobrescribe, se ejecutaría el código de Libro antes de que los campos de Libro estuvieran inicializados, y este vería null y ceros. Demostración:

public class Material {
    public Material() {
        System.out.println("Material construye: " + describir());   // PELIGRO
    }
    public String describir() { return "material generico"; }
}

public class Libro extends Material {
    private final String autor;

    public Libro(String autor) {
        super();
        this.autor = autor;
    }

    @Override
    public String describir() { return "libro de " + autor; }
}
new Libro("Joshua Bloch");
// Material construye: libro de null      <-- autor todavia no asignado

El campo autor es final y acabará valiendo "Joshua Bloch", pero en el instante de la llamada aún vale null. Regla práctica: desde un constructor, llama solo a métodos private, static o final.

  1. Sobrescritura de métodos y @Override

Sobrescribir (override) es redefinir en la subclase un método heredado, con la misma firma, para cambiar su comportamiento.

public class Material {
    public int getDiasPrestamo() {
        return 15;
    }
}

public class Dvd extends Material {
    @Override
    public int getDiasPrestamo() {
        return 3;                 // los DVD circulan mas rapido
    }
}

Reglas de una sobrescritura válida:

Elemento Regla
Nombre y parámetros Idénticos (misma firma)
Tipo de retorno Igual, o un subtipo del original (retorno covariante)
Visibilidad Igual o más permisiva (protectedpublic sí; publicprivate no)
Método final No se puede sobrescribir
Método static No se sobrescribe, se oculta (03-06)
Método private No se hereda, así que no se sobrescribe

La anotación @Override no es obligatoria, pero debes ponerla siempre. Su función es pedirle al compilador que verifique que realmente estás sobrescribiendo algo. Sin ella, un error tipográfico se convierte en un método nuevo que nadie llama:

public class Dvd extends Material {
    public int getDiasPrestamos() {    // ¡ojo! "Prestamos" con S
        return 3;
    }
}

Ese código compila perfectamente y no sobrescribe nada: los DVD seguirán teniendo 15 días de plazo, y descubrir por qué te costará una tarde. Con @Override delante, el compilador falla de inmediato:

error: method does not override or implement a method from a supertype

Una anotación que se escribe en un segundo y evita una clase entera de errores silenciosos. Las anotaciones en general se estudian en la lección 10-02.

  1. Sobrescribir frente a sobrecargar

Esta es una de las confusiones más persistentes de quien aprende Java, agravada por lo parecidos que suenan los términos en español. La tabla lo deja claro:

Aspecto Sobrecarga (overload) Sobrescritura (override)
Dónde ocurre En la misma clase (o heredando) Entre superclase y subclase
Firma Distinta lista de parámetros Idéntica
Nombre Igual Igual
Tipo de retorno Puede cambiar libremente Igual o subtipo
Cuándo se decide En compilación, según el tipo declarado En ejecución, según el objeto real
Anotación Ninguna @Override
Para qué sirve Ofrecer variantes de la misma operación Especializar el comportamiento heredado
Ejemplo calcularMulta() y calcularMulta(int) Dvd.getDiasPrestamo() sobre Material.getDiasPrestamo()

Ejemplo con las dos cosas a la vez:

public class Material {
    public double calcularMulta(int diasRetraso) { return diasRetraso * 0.25; }
}

public class Dvd extends Material {

    @Override
    public double calcularMulta(int diasRetraso) {     // SOBRESCRITURA
        return diasRetraso * 0.50;
    }

    public double calcularMulta(int diasRetraso, double descuento) {   // SOBRECARGA
        return calcularMulta(diasRetraso) * (1 - descuento);
    }
}

La fila más importante de la tabla es la de "cuándo se decide": la sobrecarga la resuelve el compilador mirando los tipos escritos en el código; la sobrescritura la resuelve la JVM en ejecución mirando el objeto real. Esa diferencia es el corazón del polimorfismo, y es el tema de la lección siguiente.

  1. final en clases y métodos

final tiene tres usos en Java, y ya conoces dos:

Aplicado a Significa
Variable o campo No se puede reasignar (módulos 1 y 03-04)
Método No se puede sobrescribir en ninguna subclase
Clase No se puede extender: no admite subclases
public class Material {
    /** El calculo del tope legal no puede alterarse en ninguna subclase. */
    public final double aplicarTope(double importe) {
        return Math.min(importe, MULTA_MAXIMA);
    }
}
public final class Prestamo {   // nadie puede crear "SubPrestamo"
    // ...
}

¿Para qué sirve? Para proteger invariantes. Si una regla debe cumplirse siempre, sin excepción, un método final impide que una subclase la salte. Las clases final más famosas de Java son String e Integer: si pudieras extender String y sobrescribir sus métodos, la seguridad de todo el lenguaje se vendría abajo.

El consejo profesional, formulado por Joshua Bloch en Java Efectivo (sí, el libro del catálogo de BiblioTech): diseña para la herencia y documéntala, o prohíbela. Una clase que no ha sido pensada explícitamente para ser extendida es mejor declararla final.

  1. Toda clase hereda de Object

Una regla del lenguaje que probablemente ya has notado: si una clase no declara extends, hereda implícitamente de java.lang.Object.

public class Material { }
// equivale exactamente a
public class Material extends Object { }

Por eso puedes escribir esto sin haber definido nada:

Libro l = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
System.out.println(l.toString());   // com.nexussoftware...Libro@1b6d3586
System.out.println(l.equals(l));    // true
System.out.println(l.hashCode());   // 460141958

La jerarquía real de BiblioTech, por tanto, es esta:

classDiagram
    Object <|-- Material
    Material <|-- Libro
    Material <|-- Revista
    Material <|-- Dvd
    Object <|-- Empleado
    Object <|-- Prestamo
    class Object {
        +toString() String
        +equals(Object) boolean
        +hashCode() int
        +getClass() Class
    }

Los métodos que Object regala funcionan, pero su comportamiento por defecto rara vez es el que quieres: toString() imprime un críptico Libro@1b6d3586 y equals compara direcciones de memoria, de modo que dos libros con el mismo ISBN son distintos. Sobrescribirlos correctamente es el tema de la lección 03-09, que cierra este módulo.

  1. Herencia frente a composición

La herencia es tan cómoda que se abusa de ella. La alternativa es la composición: en lugar de "ser un", el objeto tiene un colaborador al que delega el trabajo.

Criterio Herencia Composición
Relación "es un" "tiene un"
Sintaxis class B extends A class B { private A a; }
Acoplamiento Fuerte: B depende de los detalles internos de A Débil: B solo usa la API pública de A
Se decide en Compilación (no cambia nunca) Ejecución (se puede sustituir el colaborador)
Reutiliza Toda la implementación de A, quiera o no Solo lo que decida usar
Riesgo Cambios en A rompen B Bajo

Un caso donde la herencia sería un error. Imagina que quieres que Prestamo reutilice el código de Libro para no repetir el título y la referencia, y escribes:

public class Prestamo extends Libro {   // ERROR DE DISEÑO
    public Empleado empleado;
    public int diasTranscurridos;
}

Compila. Y es un desastre, por tres razones:

  1. La frase es falsa. Un préstamo no es un libro; un préstamo tiene un libro. Si la frase "es un" suena rara al decirla en voz alta, no uses herencia.
  2. Hereda operaciones absurdas. prestamo.prestar() y prestamo.devolver() pasan a existir, y no significan nada. Estás ampliando la superficie pública con métodos sin sentido.
  3. Rompe el polimorfismo. Cualquier código que reciba un Libro aceptaría un Prestamo, y un catálogo de libros podría acabar conteniendo préstamos.

La forma correcta es la que ya tienes:

public class Prestamo {          // sin extends
    public final Material material;    // TIENE un material
    public final Empleado empleado;    // TIENE un empleado
}

Regla práctica que se cita constantemente en la industria: prefiere composición a herencia. Usa herencia solo cuando la subclase sea genuinamente sustituible por la superclase en cualquier contexto —lo que se conoce como principio de sustitución de Liskov— y cuando quieras aprovechar el polimorfismo.

  1. Jerarquías profundas y la fragilidad de la clase base

Dos problemas que aparecen en proyectos reales y que conviene anticipar:

Jerarquías profundas. Cadenas como Object → Material → MaterialFisico → MaterialImpreso → Publicacion → PublicacionPeriodica → Revista hacen que entender Revista exija leer seis ficheros, que un cambio en cualquier nivel se propague hacia abajo y que sea imposible saber de dónde sale un método concreto. Criterio sano: dos o tres niveles como máximo; si necesitas más, probablemente lo que buscas es composición o interfaces (04-01).

Fragilidad de la clase base (fragile base class problem). Una subclase depende no solo de qué hace la superclase, sino de cómo lo hace. Un cambio interno inofensivo puede romperla:

public class Material {
    public int prestamosRegistrados;

    public void registrar()          { prestamosRegistrados++; }
    public void registrarVarios(int n) {
        for (int i = 0; i < n; i++) {
            registrar();                     // detalle de implementacion
        }
    }
}

public class Dvd extends Material {
    public int vecesRegistradoDesdeDvd;

    @Override
    public void registrar() {
        vecesRegistradoDesdeDvd++;
        super.registrar();
    }

    @Override
    public void registrarVarios(int n) {
        vecesRegistradoDesdeDvd += n;        // suma n...
        super.registrarVarios(n);            // ...y super llama n veces a registrar()
    }
}

dvd.registrarVarios(3) incrementa vecesRegistradoDesdeDvd en 6, no en 3: la superclase llama internamente a un método que la subclase ha sobrescrito. Y si mañana alguien optimiza registrarVarios para hacer prestamosRegistrados += n sin llamar a registrar(), el contador de la subclase pasará a valer 3. El mismo código, resultado distinto, sin haber tocado la subclase. Esa es la fragilidad: la subclase depende de detalles internos que nadie prometió mantener.

La defensa: documentar exhaustivamente qué métodos llama internamente la clase base, declarar final lo que no deba tocarse, y preferir composición cuando no necesites polimorfismo.

  1. BiblioTech: la jerarquía de materiales prestables

Aplicamos todo al proyecto. El modelo objetivo:

classDiagram
    class Material {
        +String titulo
        +String referencia
        +boolean disponible
        +MULTA_MAXIMA double
        +UMBRAL_LEVE int
        +getDiasPrestamo() int
        +getTarifaDiaria() double
        +calcularDiasRetraso(int) int
        +calcularMulta(int) double
        +clasificarGravedad(int) String
        +prestar() void
        +devolver() void
        +describir() String
        +getTipo() String
    }
    class Libro {
        +String autor
        +int anioPublicacion
        +getIsbn() String
    }
    class Revista {
        +int numero
        +String periodicidad
    }
    class Dvd {
        +int duracionMinutos
    }
    Material <|-- Libro
    Material <|-- Revista
    Material <|-- Dvd

La clase base Material

package com.nexussoftware.bibliotech.dominio;

/**
 * Material prestable del catalogo de Nexus Software.
 *
 * Define lo comun a todos los soportes: identificacion, disponibilidad y
 * el calculo de multas. Las subclases solo declaran en que se diferencian:
 * el plazo de prestamo y la tarifa diaria.
 */
public class Material {

    /** Plazo estandar de prestamo, en dias. Lo usan los libros. */
    public static final int DIAS_PRESTAMO = 15;

    /** Tarifa estandar por dia de retraso, en euros. */
    public static final double TARIFA_DIARIA = 0.25;

    /** Importe maximo de una multa, comun a todos los materiales. */
    public static final double MULTA_MAXIMA = 20.0;

    /** Retraso maximo, en dias, que se considera leve. */
    public static final int UMBRAL_LEVE = 7;

    public static int materialesCreados = 0;

    public final String titulo;
    public final String referencia;
    public boolean      disponible;

    public Material(String titulo, String referencia, boolean disponible) {
        this.titulo     = (titulo == null || titulo.isBlank()) ? "Sin titulo" : titulo.trim();
        this.referencia = (referencia == null || referencia.isBlank())
                          ? "000-0000000000" : referencia.trim();
        this.disponible = disponible;
        materialesCreados++;
    }

    // ---------- Parametros que cada soporte redefine ----------

    /** @return dias de plazo de este tipo de material. */
    public int getDiasPrestamo() {
        return DIAS_PRESTAMO;
    }

    /** @return coste por dia de retraso de este tipo de material. */
    public double getTarifaDiaria() {
        return TARIFA_DIARIA;
    }

    /** @return nombre legible del soporte, para listados. */
    public String getTipo() {
        return "Material";
    }

    // ---------- Reglas comunes, escritas UNA sola vez ----------

    /** @return dias de retraso, saturados a cero, segun el plazo de este material. */
    public int calcularDiasRetraso(int diasTranscurridos) {
        return Math.max(0, diasTranscurridos - getDiasPrestamo());
    }

    /** @return multa en euros, con el tope comun aplicado. */
    public double calcularMulta(int diasTranscurridos) {
        int retraso = calcularDiasRetraso(diasTranscurridos);
        return Math.min(retraso * getTarifaDiaria(), MULTA_MAXIMA);
    }

    /** @return "SIN RETRASO", "LEVE" o "GRAVE". */
    public String clasificarGravedad(int diasTranscurridos) {
        int retraso = calcularDiasRetraso(diasTranscurridos);
        if (retraso == 0)            return "SIN RETRASO";
        if (retraso <= UMBRAL_LEVE)  return "LEVE";
        return "GRAVE";
    }

    public boolean estaDisponible() { return disponible; }

    public void prestar() {
        if (!disponible) {
            System.out.println("AVISO: '" + titulo + "' ya estaba prestado.");
            return;
        }
        disponible = false;
    }

    public void devolver() {
        if (disponible) {
            System.out.println("AVISO: '" + titulo + "' ya estaba disponible.");
            return;
        }
        disponible = true;
    }

    /** @return descripcion legible; las subclases la amplian con super.describir(). */
    public String describir() {
        return titulo + " (ref. " + referencia + ")";
    }
}

Fíjate en la pieza clave del diseño: calcularMulta no usa las constantes directamente, sino que llama a getDiasPrestamo() y getTarifaDiaria(). Como esos métodos se pueden sobrescribir, la fórmula se escribe una sola vez y sirve para todos los soportes. Es el mecanismo que la lección siguiente formalizará con el nombre de despacho dinámico.

Las tres subclases

package com.nexussoftware.bibliotech.dominio;

/** Libro tecnico del catalogo. Usa el plazo y la tarifa estandar. */
public class Libro extends Material {

    public final String autor;
    public final int    anioPublicacion;

    public Libro(String titulo, String autor, String isbn,
                 int anioPublicacion, boolean disponible) {
        super(titulo, isbn, disponible);          // el ISBN es la referencia del libro
        this.autor = (autor == null || autor.isBlank()) ? "Desconocido" : autor.trim();

        if (anioPublicacion < 1450 || anioPublicacion > 2100) {
            System.out.println("AVISO: anio invalido en '" + titulo + "'. Se registra como 0.");
            this.anioPublicacion = 0;
        } else {
            this.anioPublicacion = anioPublicacion;
        }
    }

    /** Alta habitual: un libro nuevo entra disponible. */
    public Libro(String titulo, String autor, String isbn, int anioPublicacion) {
        this(titulo, autor, isbn, anioPublicacion, true);
    }

    /** El ISBN es la referencia de un libro; se mantiene el nombre del dominio. */
    public String getIsbn() { return referencia; }

    @Override
    public String getTipo() { return "Libro"; }

    @Override
    public String describir() {
        return super.describir() + " - " + autor + ", " + anioPublicacion;
    }

    // No sobrescribe getDiasPrestamo() ni getTarifaDiaria():
    // hereda los valores estandar de 15 dias y 0,25 EUR/dia.
}
package com.nexussoftware.bibliotech.dominio;

/** Revista tecnica. Circula mucho, asi que su plazo es mas corto y su tarifa menor. */
public class Revista extends Material {

    public static final int    DIAS_PRESTAMO_REVISTA = 7;
    public static final double TARIFA_DIARIA_REVISTA = 0.10;

    public final int    numero;
    public final String periodicidad;

    public Revista(String titulo, String referencia, int numero, String periodicidad) {
        super(titulo, referencia, true);
        this.numero       = Math.max(0, numero);
        this.periodicidad = (periodicidad == null || periodicidad.isBlank())
                            ? "Desconocida" : periodicidad.trim();
    }

    @Override
    public int getDiasPrestamo() { return DIAS_PRESTAMO_REVISTA; }

    @Override
    public double getTarifaDiaria() { return TARIFA_DIARIA_REVISTA; }

    @Override
    public String getTipo() { return "Revista"; }

    @Override
    public String describir() {
        return super.describir() + " - n." + numero + " (" + periodicidad + ")";
    }
}
package com.nexussoftware.bibliotech.dominio;

/** DVD de formacion. Pocas copias: plazo muy corto y tarifa doble. */
public class Dvd extends Material {

    public static final int    DIAS_PRESTAMO_DVD = 3;
    public static final double TARIFA_DIARIA_DVD = 0.50;

    public final int duracionMinutos;

    public Dvd(String titulo, String referencia, int duracionMinutos) {
        super(titulo, referencia, true);
        this.duracionMinutos = Math.max(0, duracionMinutos);
    }

    @Override
    public int getDiasPrestamo() { return DIAS_PRESTAMO_DVD; }

    @Override
    public double getTarifaDiaria() { return TARIFA_DIARIA_DVD; }

    @Override
    public String getTipo() { return "DVD"; }

    @Override
    public String describir() {
        return super.describir() + " - " + duracionMinutos + " min";
    }
}

Comparativa de los tres soportes

Soporte Plazo Tarifa/día Multa a los 10 días transcurridos Campos propios
Libro 15 días 0,25 € 0,00 € (aún en plazo) autor, anioPublicacion
Revista 7 días 0,10 € 0,30 € (3 días de retraso) numero, periodicidad
Dvd 3 días 0,50 € 3,50 € (7 días de retraso) duracionMinutos

Prueba en BiblioTechApp

Libro   javaEfectivo = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
Revista javaMagazine = new Revista("Java Magazine", "REV-2024-42", 42, "Bimestral");
Dvd     cursoSpring  = new Dvd("Curso de Spring", "DVD-0007", 240);

System.out.printf("%-9s %-22s %6s %8s %10s %10s%n",
                  "TIPO", "TITULO", "PLAZO", "TARIFA", "MULTA 10d", "GRAVEDAD");

imprimirFicha(javaEfectivo);
imprimirFicha(javaMagazine);
imprimirFicha(cursoSpring);

System.out.println();
System.out.println(javaEfectivo.describir());
System.out.println(javaMagazine.describir());
System.out.println(cursoSpring.describir());
private static void imprimirFicha(Material m) {
    System.out.printf("%-9s %-22s %4d d %7.2f %9.2f  %s%n",
                      m.getTipo(), m.titulo, m.getDiasPrestamo(), m.getTarifaDiaria(),
                      m.calcularMulta(10), m.clasificarGravedad(10));
}

Salida:

TIPO      TITULO                  PLAZO   TARIFA  MULTA 10d   GRAVEDAD
Libro     Java Efectivo            15 d     0,25       0,00  SIN RETRASO
Revista   Java Magazine             7 d     0,10       0,30  LEVE
DVD       Curso de Spring           3 d     0,50       3,50  LEVE

Java Efectivo (ref. 978-0000000001) - Joshua Bloch, 2018
Java Magazine (ref. REV-2024-42) - n.42 (Bimestral)
Curso de Spring (ref. DVD-0007) - 240 min

Observa algo que quizá te haya pasado desapercibido: el método imprimirFicha recibe un parámetro de tipo Material y le pasamos un Libro, una Revista y un Dvd. Funciona, y cada uno responde según su propio tipo. Eso es polimorfismo, el tema de la lección siguiente.

Nota sobre la evolución del proyecto. La clase Material tal como está escrita se puede instanciar (new Material(...)), y eso no tiene sentido: en la biblioteca no hay "materiales genéricos", hay libros, revistas y DVD. En la lección 04-02 convertirás Material en una clase abstracta, que impide crear instancias sueltas y permite declarar operaciones sin cuerpo que las subclases están obligadas a implementar. Por ahora, considera Material un andamio funcional.

Errores Comunes y Consejos

  • Olvidar @Override. Es el error que produce bugs más difíciles de encontrar: un método mal escrito no sobrescribe nada y la superclase sigue mandando. Pon @Override siempre.
  • Creer que la subclase puede acceder a los campos private del padre. Existen en el objeto, pero no son accesibles. Si la subclase los necesita, la superclase debe exponer un método (o declararlos protected, con cuidado; ver 03-07).
  • Esperar que los constructores se hereden. No se heredan. Cada subclase declara los suyos y llama a super(...).
  • Usar herencia para reutilizar código, sin relación "es un". El caso Prestamo extends Libro del apartado 11. Si solo quieres reutilizar, compón.
  • Llamar a métodos sobrescribibles desde el constructor. El campo aún vale null. Documentado en el apartado 6.
  • Cambiar la visibilidad a menos permisiva al sobrescribir. publicprotected no compila: rompería el contrato de la superclase.
  • Jerarquías de cinco o seis niveles. Cuesta más entenderlas que el problema que resuelven.
  • Consejo: di la frase en voz alta. "Un DVD es un material prestable": suena bien, herencia. "Un préstamo es un libro": suena mal, composición.
  • Consejo: sube a la superclase solo lo que sea común a todas las subclases. Si un campo lo usan dos de tres, no pertenece a la clase base.
  • Consejo: usa la vista de jerarquía del IDE (Ctrl+H en IntelliJ) para ver de un vistazo quién extiende a quién y qué métodos se sobrescriben.

Ejercicios

Ejercicio 1: añadir un nuevo soporte

Nexus Software incorpora audiolibros al catálogo: se prestan 10 días, con una tarifa de 0,15 €/día, y tienen un campo propio narrador y otro duracionMinutos. Crea la clase AudioLibro como subclase de Material, sobrescribiendo lo necesario y sin tocar ni una línea de Material, Libro, Revista o Dvd. Añádela a la tabla comparativa de BiblioTechApp y comprueba que la multa a los 20 días transcurridos es de 1,50 €.

Ejercicio 2: trazar el orden de construcción

Escribe una jerarquía de tres niveles (MaterialLibroLibroFirmado) donde cada clase tenga un bloque de inicialización de instancia y un constructor, ambos con un System.out.println identificativo. Predice la salida de new LibroFirmado(...) antes de ejecutarla y después verifícala. A continuación, provoca deliberadamente el problema del apartado 6: haz que el constructor de Material llame a un método que LibroFirmado sobrescriba usando uno de sus campos, y explica el resultado.

Ejercicio 3: detectar herencia mal aplicada

Un compañero propone estas cuatro jerarquías para BiblioTech. Para cada una, decide si la herencia es adecuada y, si no lo es, propón la alternativa correcta con composición:

class Empleado extends Material { }
class Bibliotecario extends Empleado { }
class Catalogo extends Libro { }
class LibroDescatalogado extends Libro { }

Soluciones

Solución 1

package com.nexussoftware.bibliotech.dominio;

/** Audiolibro del catalogo. Plazo intermedio y tarifa reducida. */
public class AudioLibro extends Material {

    public static final int    DIAS_PRESTAMO_AUDIO = 10;
    public static final double TARIFA_DIARIA_AUDIO = 0.15;

    public final String narrador;
    public final int    duracionMinutos;

    public AudioLibro(String titulo, String referencia,
                      String narrador, int duracionMinutos) {
        super(titulo, referencia, true);
        this.narrador = (narrador == null || narrador.isBlank())
                        ? "Desconocido" : narrador.trim();
        this.duracionMinutos = Math.max(0, duracionMinutos);
    }

    @Override
    public int getDiasPrestamo() { return DIAS_PRESTAMO_AUDIO; }

    @Override
    public double getTarifaDiaria() { return TARIFA_DIARIA_AUDIO; }

    @Override
    public String getTipo() { return "Audiolibro"; }

    @Override
    public String describir() {
        return super.describir() + " - narrado por " + narrador
               + ", " + duracionMinutos + " min";
    }
}

Uso:

AudioLibro audio = new AudioLibro("Refactorización", "AUD-0003", "Nuria Vidal", 610);
imprimirFicha(audio);
System.out.println(audio.describir());
System.out.printf("Multa a 20 dias: %.2f EUR (%s)%n",
                  audio.calcularMulta(20), audio.clasificarGravedad(20));

Salida:

Audiolibro Refactorización          10 d     0,15       0,00  SIN RETRASO
Refactorización (ref. AUD-0003) - narrado por Nuria Vidal, 610 min
Multa a 20 dias: 1,50 EUR (GRAVE)

Comprueba los dos cálculos a mano, porque son la mejor forma de verificar que la herencia funciona como esperas. Con 10 días transcurridos (los que usa imprimirFicha), el plazo del audiolibro es de 10 días, así que el retraso es 0: no hay multa y la gravedad es SIN RETRASO. Con 20 días transcurridos, el retraso es 20 − 10 = 10 días; 10 × 0,15 = 1,50 €, muy por debajo del tope de 20 €; y como 10 supera el UMBRAL_LEVE de 7 días, la gravedad es GRAVE.

Lo importante del ejercicio: para añadir un soporte nuevo has escrito una clase y cero modificaciones en el código existente. imprimirFicha funciona con audiolibros sin haberse enterado de que existen. Esa propiedad —extender sin modificar— es una de las razones por las que la POO se impuso en el software de gran escala, y la lección siguiente la explota a fondo.

Solución 2

package com.nexussoftware.bibliotech;

class MaterialTraza {
    { System.out.println("1. bloque de Material"); }

    MaterialTraza() {
        System.out.println("2. constructor de Material");
        System.out.println("   describir() -> " + describir());   // PELIGRO
    }

    String describir() { return "material generico"; }
}

class LibroTraza extends MaterialTraza {
    { System.out.println("3. bloque de Libro"); }

    LibroTraza() {
        super();
        System.out.println("4. constructor de Libro");
    }
}

class LibroFirmadoTraza extends LibroTraza {
    private final String firmante;

    { System.out.println("5. bloque de LibroFirmado"); }

    LibroFirmadoTraza(String firmante) {
        super();
        this.firmante = firmante;
        System.out.println("6. constructor de LibroFirmado");
    }

    @Override
    String describir() { return "libro firmado por " + firmante; }
}
new LibroFirmadoTraza("Martin Fowler");

Salida:

1. bloque de Material
2. constructor de Material
   describir() -> libro firmado por null
3. bloque de Libro
4. constructor de Libro
5. bloque de LibroFirmado
6. constructor de LibroFirmado

Explicación. La construcción va de la raíz a la hoja: los bloques y el constructor de cada nivel se ejecutan completos antes de pasar al siguiente. Por eso el orden es 1-2, 3-4, 5-6.

El problema del apartado 6 se ve en la tercera línea. El constructor de MaterialTraza llama a describir(), y como el objeto real es un LibroFirmadoTraza, la JVM ejecuta la versión sobrescrita (esto es el despacho dinámico de la lección siguiente). Pero en ese instante el campo firmante todavía no se ha asignado —su asignación está en el paso 6, cuatro pasos más tarde—, así que vale null y la salida es libro firmado por null. Y observa el detalle inquietante: firmante es final, un campo que "no puede cambiar", y sin embargo lo hemos observado con dos valores distintos durante la vida del objeto.

Solución 3

class Empleado extends Material — INCORRECTA. "Un empleado es un material prestable" es falso y, además, moralmente cuestionable. Empleado heredaría prestar(), devolver() y calcularMulta(), todos sin sentido, y podría acabar en un catálogo de materiales. Alternativa: ninguna relación. Empleado es una clase independiente que aparece asociada desde Prestamo.

class Bibliotecario extends Empleado — CORRECTA (con matices). "Un bibliotecario es un empleado" es verdadero, y un bibliotecario tendría comportamiento propio (dar de alta materiales, cancelar multas) además del heredado. Es un uso legítimo. El matiz: si la única diferencia fuese un campo String rol, la herencia sería excesiva; bastaría un atributo. Introduce la subclase solo cuando aporte comportamiento distinto, no solo un dato.

class Catalogo extends Libro — INCORRECTA, y de las peores. Un catálogo no es un libro: un catálogo contiene libros. Aquí se confunde el contenedor con el contenido, un error clásico. Catalogo heredaría prestar(), getIsbn() y autor, absurdos para una colección. Alternativa por composición, que es exactamente lo que harás en el módulo 5:

public class Catalogo {
    // Contiene materiales; el almacenamiento multiple llega en el modulo 5.
    // De momento seria un array; despues, un ArrayList o un HashMap.
}

class LibroDescatalogado extends Libro — DUDOSA. La frase "un libro descatalogado es un libro" es cierta, así que la herencia no es absurda. Pero pregúntate qué comportamiento cambia: probablemente solo que no se puede prestar. Eso es un estado, no un tipo:

public class Libro extends Material {
    private boolean descatalogado;

    @Override
    public void prestar() {
        if (descatalogado) {
            System.out.println("AVISO: '" + titulo + "' esta descatalogado.");
            return;
        }
        super.prestar();
    }
}

La señal de alarma general: si un objeto puede pasar de una subclase a otra durante su vida —un libro se descataloga y luego se recatologa—, la herencia es el mecanismo equivocado, porque en Java un objeto no puede cambiar de clase una vez creado. Los estados que cambian se modelan con campos.

Conclusión

Has aprendido el mecanismo con el que Java expresa la especialización. Sabes cuándo procede —solo cuando la frase "X es un Y" es verdadera siempre— y cómo se escribe con extends. Conoces con precisión qué se hereda y qué no: los campos privados existen pero no son accesibles, y los constructores no se heredan nunca. Dominas super(...) para encadenar constructores y super.metodo() para extender comportamiento sin duplicarlo, y has trazado el orden de construcción de una jerarquía completa, incluido el peligro real de llamar a métodos sobrescribibles desde un constructor y encontrarte campos final valiendo null. Tienes clara —y respaldada por una tabla— la diferencia entre sobrescribir y sobrecargar, y sabes que @Override no es decoración, sino la red de seguridad que convierte un error tipográfico silencioso en un error de compilación. Sabes usar final para prohibir la extensión, conoces la raíz universal Object y, sobre todo, tienes criterio para preferir composición a herencia y para reconocer las jerarquías profundas y la fragilidad de la clase base antes de sufrirlas.

BiblioTech ha cambiado de escala: ya no gestiona libros, gestiona materiales prestables. Material concentra la identificación, la disponibilidad y las reglas de multa escritas una sola vez, y Libro, Revista y Dvd declaran únicamente en qué se diferencian: su plazo y su tarifa. Añadir un audiolibro te ha costado una clase nueva y cero cambios en el código existente.

Y ha aparecido algo que aún no tiene nombre. imprimirFicha(Material m) recibe indistintamente un libro, una revista o un DVD, y cada uno responde según lo que realmente es. calcularMulta está escrito una sola vez, en Material, y sin embargo cobra 0,25 € al libro y 0,50 € al DVD. Ese mecanismo se llama polimorfismo, es el pilar que hace que la herencia valga la pena, y es el tema de la lección siguiente: verás cómo la JVM decide en tiempo de ejecución qué método ejecutar, qué son el upcasting y el downcasting, cómo instanceof con patrones de Java moderno sustituye a los antiguos if sobre el tipo, y por qué esos if eran, desde el principio, un olor de diseño.

Curso de Programación en Java

Módulo 1: Introducción a Java

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2026. Todos los derechos reservados