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
- La relación "es un"
extends: sintaxis y primer ejemplo- Qué se hereda y qué no
super: el constructor de la superclasesuper: acceder a miembros de la superclase- Orden de construcción en una jerarquía
- Sobrescritura de métodos y
@Override - Sobrescribir frente a sobrecargar
finalen clases y métodos- Toda clase hereda de
Object - Herencia frente a composición
- Jerarquías profundas y la fragilidad de la clase base
- BiblioTech: la jerarquía de materiales prestables
- Errores Comunes y Consejos
- Ejercicios
- 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.
extends: sintaxis y primer ejemplo
extends: sintaxis y primer ejemplopublic 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); // falseLa 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.
- 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 |
Sí | 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 |
Sí | 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.
super: el constructor de la superclase
super: el constructor de la superclasesuper(...) 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:
super(...)debe ser la primera sentencia del constructor.- No pueden coexistir
super(...)ythis(...)en el mismo constructor: si usasthis(...), la cadena acabará en algún constructor que llame asuper(...). - Si la superclase no tiene constructor sin argumentos y la subclase no llama explícitamente a
super(...), hay error de compilación:
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".
super: acceder a miembros de la superclase
super: acceder a miembros de la superclasesuper 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
- 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"); }
}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; }
}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.
- Sobrescritura de métodos y
@Override
@OverrideSobrescribir (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 (protected → public sí; public → private 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:
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.
- 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.
final en clases y métodos
final en clases y métodosfinal 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);
}
}¿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.
- Toda clase hereda de
Object
ObjectUna regla del lenguaje que probablemente ya has notado: si una clase no declara extends, hereda implícitamente de java.lang.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()); // 460141958La 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.
- 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:
- 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.
- Hereda operaciones absurdas.
prestamo.prestar()yprestamo.devolver()pasan a existir, y no significan nada. Estás ampliando la superficie pública con métodos sin sentido. - Rompe el polimorfismo. Cualquier código que reciba un
Libroaceptaría unPrestamo, 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.
- 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.
- 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
Materialtal 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ásMaterialen una clase abstracta, que impide crear instancias sueltas y permite declarar operaciones sin cuerpo que las subclases están obligadas a implementar. Por ahora, consideraMaterialun 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@Overridesiempre. - Creer que la subclase puede acceder a los campos
privatedel padre. Existen en el objeto, pero no son accesibles. Si la subclase los necesita, la superclase debe exponer un método (o declararlosprotected, 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 Librodel 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.
public→protectedno 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+Hen 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 (Material → Libro → LibroFirmado) 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; }
}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
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
