Al cerrar la lección anterior quedó una grieta abierta en BiblioTech: Material sigue siendo una clase instanciable. Nada impide escribir new Material("Algo", "REF-1", true) y obtener un objeto que no es un libro, ni una revista, ni un DVD; un objeto sin autor, sin número y sin duración, que responde "Material" cuando le preguntas su tipo. Es un objeto que no debería existir, y su existencia es un accidente del diseño, no una decisión.
La clase abstracta cierra esa puerta. Es una clase que declara explícitamente que está incompleta: sirve como base común —con campos, constructor y código compartido— pero no se puede instanciar, y puede obligar a sus subclases a implementar los métodos que solo ellas pueden decidir. Donde la interfaz dice "esto es lo que hay que saber hacer", la clase abstracta dice "esto es lo que ya está hecho, y esto es lo que te toca a ti". Al terminar esta lección, Material será abstracta, calcularMulta estará escrito una sola vez para todo el sistema mediante el patrón Template Method, y sabrás elegir con criterio entre interfaz, clase abstracta o —lo más frecuente en el diseño profesional— las dos a la vez.
Contenido
abstracten clases: qué significa y qué impide- Métodos abstractos: la obligación que se hereda
- Una clase abstracta CON estado: campos, constructor y métodos concretos
- ¿Para qué sirve el constructor de una clase que no se instancia?
- La subclase incompleta también debe ser abstracta
- Interfaz frente a clase abstracta: la tabla completa
- Regla práctica de decisión
- El patrón Template Method
- Combinar ambas:
AbstractXxx - BiblioTech:
Materialse vuelve abstracta - Refactor de
Libro,RevistayDvd - Errores Comunes y Consejos
- Ejercicios
abstract en clases: qué significa y qué impide
abstract en clases: qué significa y qué impideEl modificador abstract aplicado a una clase declara que esa clase es conceptualmente incompleta: representa una idea general de la que no existen ejemplares concretos.
Con esa sola palabra, esto deja de compilar:
Es importante entender qué se prohíbe y qué no:
| Operación | ¿Permitida en una clase abstracta? |
|---|---|
new Material(...) |
No. Error de compilación |
Material m = new Libro(...) |
Sí. Es un tipo válido para declarar |
Material[] catalogo = new Material[10] |
Sí. El array guarda referencias, no instancias |
Tener campos, incluso private |
Sí |
| Tener constructor | Sí, y es fundamental (apartado 4) |
| Tener métodos concretos con cuerpo | Sí |
Tener métodos abstract sin cuerpo |
Sí, y es lo característico |
Tener métodos static y final |
Sí |
| Extender otra clase | Sí |
| Implementar interfaces | Sí, y es muy habitual (apartado 9) |
| No tener ningún método abstracto | Sí, es legal (aunque poco común) |
Ese último punto sorprende: una clase puede ser abstract sin tener un solo método abstracto. Es perfectamente válido y a veces útil: la marcas como abstracta simplemente para declarar que instanciarla no tiene sentido, aunque técnicamente esté completa.
La ganancia inmediata es de modelado. "Material" es una abstracción: en la biblioteca de Nexus Software hay libros, revistas y DVDs, pero no hay "materiales" a secas. El código pasa a decir la verdad sobre el dominio, y el compilador la hace cumplir.
- Métodos abstractos: la obligación que se hereda
Un método abstracto es un método declarado sin cuerpo, terminado en punto y coma, que la clase promete que existirá pero se niega a implementar:
public abstract class Material {
/** Cada soporte declara su etiqueta legible. */
public abstract String getTipo();
/** Cada soporte declara su plazo de prestamo en dias. */
public abstract int getDiasPrestamo();
/** Cada soporte declara su tarifa diaria de multa. */
public abstract double getTarifaDiaria();
}Las reglas son estrictas y merece la pena tenerlas todas juntas:
- Un método abstracto solo puede existir dentro de una clase abstracta (o de una interfaz). Si pones un método abstracto en una clase normal, el error es
missing method body, or declare abstract. - Toda subclase concreta debe implementarlos todos. Si falta uno, no compila.
abstractes incompatible conprivate,staticyfinal. Conprivateporque la subclase no lo vería; constaticporque los métodos estáticos no son polimórficos (03-06); confinalporquefinalprohíbe exactamente lo queabstractexige.
La diferencia con el diseño anterior es de garantías. Compara las dos versiones de getDiasPrestamo:
// ANTES (modulo 3): implementacion de relleno
public int getDiasPrestamo() { return DIAS_PRESTAMO; } // 15 dias "por si acaso"
// AHORA: obligacion
public abstract int getDiasPrestamo();Con la primera, si mañana añades AudioLibro extends Material y olvidas su plazo, el programa compila y aplica silenciosamente 15 días. El error no se detecta hasta que un empleado se queja de una multa mal calculada. Con la segunda, no compila: el error llega en el segundo cero, con el nombre del método y el número de línea.
Esta es la diferencia entre un valor por defecto y un contrato. El valor por defecto oculta el olvido; el contrato lo denuncia.
- Una clase abstracta CON estado: campos, constructor y métodos concretos
Aquí está la diferencia esencial con una interfaz: una clase abstracta puede guardar datos. Y por tanto puede escribir código compartido de verdad, no solo código que se apoya en llamadas al contrato.
public abstract class Material {
// 1. Constantes compartidas
public static final double MULTA_MAXIMA = 20.0;
public static final int UMBRAL_LEVE = 7;
// 2. Estado de instancia: IMPOSIBLE en una interfaz
private final String titulo;
private final String referencia;
private boolean disponible;
// 3. Constructor: IMPOSIBLE en una interfaz
protected 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;
}
// 4. Metodos concretos que operan sobre ese estado
public String getTitulo() { return titulo; }
public String getReferencia() { return referencia; }
public boolean estaDisponible() { return disponible; }
public boolean prestar() {
if (!disponible) { return false; }
disponible = false;
return true;
}
// 5. Metodos abstractos que las subclases deben rellenar
public abstract String getTipo();
public abstract int getDiasPrestamo();
public abstract double getTarifaDiaria();
}Las cinco secciones numeradas resumen el reparto: la clase abstracta aporta lo que es común (2, 3, 4), y delega lo que varía (5). Ninguna interfaz puede ofrecer los puntos 2 y 3.
Es la respuesta directa a una pregunta que quizá te hiciste en 04-01: si un método default puede tener cuerpo, ¿para qué necesito una clase abstracta? Porque prestar() necesita el campo disponible. Un default no puede tenerlo: tendría que llamar a estaDisponible() y a un hipotético setDisponible(), ampliando el contrato con un mutador que rompe el encapsulamiento que tanto cuidaste en 03-07. La clase abstracta guarda el campo, lo mantiene private y expone solo las operaciones de negocio.
- ¿Para qué sirve el constructor de una clase abstracta?
Es la duda clásica: si new Material(...) está prohibido, ¿qué sentido tiene ese constructor?
La respuesta está en la cadena de inicialización que estudiaste en 03-04: el constructor de una subclase siempre invoca primero al de su superclase, explícitamente con super(...) o implícitamente. Ese constructor sí se ejecuta, simplemente nunca por sí solo.
public class Libro extends Material {
public Libro(String titulo, String autor, String isbn, int anio) {
super(titulo, isbn, true); // ejecuta el constructor de Material
this.autor = autor;
this.anioPublicacion = anio;
}
}flowchart TD
A["new Libro(...)"] --> B["se reserva memoria para el objeto completo"]
B --> C["constructor de Libro: super(titulo, isbn, true)"]
C --> D["constructor de Object"]
D --> E["cuerpo del constructor de Material: valida e inicializa titulo, referencia, disponible"]
E --> F["cuerpo del constructor de Libro: inicializa autor y anioPublicacion"]
F --> G["objeto Libro completamente construido"]
Así que el constructor de una clase abstracta tiene tres funciones muy concretas:
- Inicializar el estado común que la subclase no debe tocar (los campos son
private). - Centralizar la validación. La comprobación de título y referencia se escribe una vez y la aplican
Libro,RevistayDvd, sin posibilidad de saltársela. - Garantizar las invariantes de la parte común desde el primer instante de vida del objeto.
Y hay una decisión de diseño en la firma: declararlo protected en lugar de public. En una clase abstracta, public en el constructor es engañoso —sugiere que alguien puede llamarlo desde fuera, y no puede—, mientras que protected dice exactamente la verdad: este constructor existe para las subclases. Es el convenio que sigue el propio JDK.
- La subclase incompleta también debe ser abstracta
¿Qué pasa si una subclase implementa solo una parte de los métodos abstractos? Que sigue siendo incompleta, y Java te obliga a decirlo:
/** Base comun de los soportes que se consultan en sala y no salen del edificio. */
public abstract class MaterialDeConsulta extends Material {
protected MaterialDeConsulta(String titulo, String referencia) {
super(titulo, referencia, true);
}
/** Todos los materiales de consulta comparten plazo y tarifa... */
@Override public int getDiasPrestamo() { return 1; }
@Override public double getTarifaDiaria() { return 1.0; }
// ... pero getTipo() sigue sin implementar: la clase sigue siendo abstracta
}Si le quitaras el abstract a MaterialDeConsulta, el compilador diría:
error: MaterialDeConsulta is not abstract and does not override
abstract method getTipo() in MaterialLa obligación se hereda hacia abajo hasta que alguien la cumple. Esto permite jerarquías de varios niveles donde cada nivel resuelve lo que sabe y deja lo demás pendiente:
classDiagram
class Material {
<<abstract>>
+getTipo()* String
+getDiasPrestamo()* int
+getTarifaDiaria()* double
}
class MaterialDeConsulta {
<<abstract>>
+getDiasPrestamo() int
+getTarifaDiaria() double
}
class Atlas {
+getTipo() String
}
Material <|-- MaterialDeConsulta
MaterialDeConsulta <|-- Atlas
Atlas es la primera clase concreta de la rama: implementa lo único que quedaba pendiente y ya se puede instanciar.
- Interfaz frente a clase abstracta: la tabla completa
Esta es la tabla que se anunció en 04-01. Estúdiala entera: cubre las seis dimensiones que realmente deciden.
| Dimensión | Interfaz | Clase abstracta |
|---|---|---|
| Estado de instancia | Imposible. Solo public static final |
Campos de cualquier tipo y visibilidad |
| Constructor | No tiene | Sí, y se ejecuta vía super(...) |
| Herencia múltiple | Una clase implementa varias | Una clase extiende una sola |
| Visibilidad de miembros | Todo public (salvo private de Java 9, invisible fuera) |
public, protected, package, private |
| Métodos con cuerpo | Sí: default, static, private |
Sí, sin restricciones |
| Bloques de inicialización | No | Sí, estáticos y de instancia |
Miembros protected |
No | Sí: el mecanismo natural para las subclases |
| Evolución de la API | Añadir un abstracto rompe; añadir un default no |
Añadir un método concreto no rompe; uno abstracto sí |
| Relación que expresa | "puede hacer" (capacidad) | "es un" (identidad) |
| Acoplamiento que impone | Mínimo: no consume la herencia | Alto: consume la única herencia disponible |
| Ejemplo del JDK | Comparable, Runnable, List |
AbstractList, InputStream, Number |
Tres filas merecen comentario adicional:
Evolución de la API. Es asimétrica y suele confundirse. En una interfaz, añadir un método abstracto rompe a todos los implementadores, pero un default no rompe a nadie. En una clase abstracta, añadir un método concreto es seguro (todas las subclases lo heredan), pero añadir uno abstracto rompe a todas las subclases concretas. Cada una tiene su vía segura de crecer.
Acoplamiento. Es el argumento decisivo y a menudo el olvidado. Si obligas a tus usuarios a extender tu clase abstracta, les gastas su única herencia y no podrán extender nada más. Una interfaz no cuesta nada en ese sentido. Por eso la recomendación profesional es: el tipo público es la interfaz; la clase abstracta es una ayuda opcional.
Miembros protected. Una clase abstracta puede ofrecer a sus subclases herramientas que el resto del mundo no ve. Una interfaz no: todo lo que declara es público. Cuando necesitas ese "canal privado" hacia las subclases, la clase abstracta es la única opción.
- Regla práctica de decisión
flowchart TD
A["Necesito un tipo comun"] --> B{"Hace falta estado compartido o un constructor?"}
B -- "No" --> C{"Lo van a firmar clases sin parentesco?"}
B -- "Si" --> D["Clase abstracta"]
C -- "Si" --> E["Interfaz"]
C -- "No" --> F{"Necesito codigo compartido?"}
F -- "No" --> E
F -- "Si" --> G["Interfaz mas clase abstracta base"]
D --> H{"Tambien quiero que lo firmen otros?"}
H -- "Si" --> G
H -- "No" --> D
Y en tres frases memorizables:
- Interfaz cuando defines qué se puede hacer y quieres que lo firmen clases que no comparten familia. Es la opción por defecto: empieza siempre por aquí.
- Clase abstracta cuando compartes estado, constructor y código real entre clases que sí forman una familia genuina.
- Ambas cuando quieres las dos cosas: la interfaz como tipo público y la clase abstracta como implementación base opcional. Es lo que hace el JDK y lo que harás en BiblioTech.
- El patrón Template Method
Ahora llega uno de los usos más potentes de las clases abstractas. Observa este método, que ya existe en tu Material desde el módulo 3:
public double calcularMulta(int diasTranscurridos) {
return Math.min(calcularDiasRetraso(diasTranscurridos) * getTarifaDiaria(),
MULTA_MAXIMA);
}Lo interesante es su estructura: define el esqueleto invariable del algoritmo —calcular el retraso, multiplicarlo por la tarifa, aplicar el tope— pero delega en getTarifaDiaria(), que cada subclase implementa a su manera. El esqueleto se escribe una vez; los pasos variables, tantas veces como soportes haya.
Eso es exactamente el patrón Template Method: un método concreto de la clase base define la secuencia de pasos, y los pasos que varían son métodos abstractos que las subclases rellenan.
Y hay un detalle clave que hace el patrón sólido: el método plantilla se declara final.
/**
* PLANTILLA: la secuencia del calculo es politica de la empresa
* y ninguna subclase puede alterarla.
*/
public final double calcularMulta(int diasTranscurridos) {
int retraso = calcularDiasRetraso(diasTranscurridos); // paso fijo
double bruta = retraso * getTarifaDiaria(); // PASO VARIABLE
return Math.min(bruta, MULTA_MAXIMA); // paso fijo (tope)
}¿Por qué final? Porque el tope de 20 € y la fórmula son política de Nexus Software, no una decisión de cada soporte. Sin final, un Dvd podría sobrescribir calcularMulta y saltarse el tope. Con final, el compilador lo impide: las subclases solo pueden influir a través de los huecos que la plantilla les ofrece.
flowchart TD
A["calcularMulta(dias) FINAL en Material"] --> B["paso fijo: calcularDiasRetraso(dias)"]
B --> C["PASO VARIABLE: getTarifaDiaria()"]
C --> D["paso fijo: aplicar MULTA_MAXIMA"]
D --> E["resultado"]
C -.-> F["Libro devuelve 0.25"]
C -.-> G["Revista devuelve 0.10"]
C -.-> H["Dvd devuelve 0.50"]
Repartir así las responsabilidades tiene un nombre en la literatura: el principio de Hollywood — "no nos llames, nosotros te llamamos". La subclase no controla el flujo; se limita a rellenar los huecos que la clase base le reserva.
Este patrón se llama así formalmente porque es uno de los patrones de diseño clásicos. Aquí lo has descubierto de forma natural, escribiendo la clase que hacía falta; en la lección 12-02 lo verás formalizado junto a Strategy, Factory, Observer y los demás, con su ficha completa y sus alternativas.
- Combinar ambas:
AbstractXxx
AbstractXxxCasi nunca eliges entre interfaz y clase abstracta: usas las dos en capas.
- La interfaz es el tipo público. Lo que declaran las variables, los parámetros y los retornos.
- La clase abstracta implementa la interfaz y ofrece una base parcial con el trabajo repetitivo ya resuelto.
- Las clases concretas extienden la base y solo escriben lo suyo.
Y quien no quiera la base, implementa la interfaz directamente. La ayuda es opcional, y ahí está toda la gracia.
El JDK está construido así, y el convenio de nombres lo delata:
| Interfaz (tipo público) | Base parcial (ayuda opcional) | Clase concreta |
|---|---|---|
List |
AbstractList |
ArrayList, LinkedList |
Map |
AbstractMap |
HashMap, TreeMap |
Set |
AbstractSet |
HashSet |
ArrayList es un AbstractList y es una List. Pero puedes escribir tu propia List sin tocar AbstractList. Todas estas clases llegan en el módulo 5; lo que importa hoy es reconocer el patrón estructural.
En BiblioTech el reparto queda exactamente igual:
classDiagram
class Prestable {
<<interface>>
+prestar() boolean
+devolver() boolean
+estaDisponible() boolean
+getDiasPrestamo() int
}
class Notificable {
<<interface>>
+getCanalAviso() String
+generarAviso(int) String
}
class Material {
<<abstract>>
-titulo String
-referencia String
-disponible boolean
+calcularMulta(int)$ double
+getTipo()* String
}
class SalaReuniones
Prestable <|.. Material
Notificable <|.. Material
Prestable <|.. SalaReuniones
Material <|-- Libro
Material <|-- Revista
Material <|-- Dvd
SalaReuniones implementa Prestable sin pasar por Material: no quiere la base, y no la necesita. Es la prueba de que la ayuda es opcional.
- BiblioTech:
Material se vuelve abstracta
Material se vuelve abstractaEste es el estado final de la clase tras aplicar todo lo aprendido.
package com.nexussoftware.bibliotech.dominio;
/**
* Base abstracta de todo material del catalogo de Nexus Software.
*
* <p>Define el estado comun (titulo, referencia, disponibilidad), las reglas
* de negocio compartidas y la plantilla del calculo de multas. Cada soporte
* concreto aporta su tipo, su plazo y su tarifa.</p>
*/
public abstract class Material implements Prestable, Notificable {
public static final double MULTA_MAXIMA = 20.0;
public static final int UMBRAL_LEVE = 7;
private static int materialesCreados = 0;
private final String titulo;
private final String referencia;
private boolean disponible;
/** Constructor protected: existe para las subclases, no para el exterior. */
protected 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++;
}
// ---------- Lo que CADA soporte debe declarar ----------
/** @return etiqueta legible del soporte: "Libro", "Revista", "DVD". */
public abstract String getTipo();
/** @return dias de plazo de prestamo de este soporte. */
@Override public abstract int getDiasPrestamo();
/** @return euros de multa por dia de retraso de este soporte. */
public abstract double getTarifaDiaria();
// ---------- Estado comun (implementado una sola vez) ----------
public String getTitulo() { return titulo; }
public String getReferencia() { return referencia; }
@Override public boolean estaDisponible() { return disponible; }
public static int getMaterialesCreados() { return materialesCreados; }
// ---------- Reglas de negocio: PLANTILLAS final ----------
/** Dias de retraso sobre el plazo del soporte. Nunca negativo. */
public final int calcularDiasRetraso(int diasTranscurridos) {
return Math.max(0, diasTranscurridos - getDiasPrestamo());
}
/**
* TEMPLATE METHOD. El esqueleto del calculo es politica de empresa
* y por eso es final; el paso variable es getTarifaDiaria().
*/
public final double calcularMulta(int diasTranscurridos) {
int retraso = calcularDiasRetraso(diasTranscurridos);
double bruta = retraso * getTarifaDiaria();
return Math.min(bruta, MULTA_MAXIMA);
}
/** Clasificacion del retraso. En 04-07 dejara de devolver String. */
public final String clasificarGravedad(int diasTranscurridos) {
int retraso = calcularDiasRetraso(diasTranscurridos);
if (retraso == 0) { return "SIN RETRASO"; }
if (retraso <= UMBRAL_LEVE) { return "LEVE"; }
return "GRAVE";
}
// ---------- Operaciones de negocio (contrato Prestable) ----------
@Override
public boolean prestar() {
if (!disponible) {
System.out.println("AVISO: '" + titulo + "' ya estaba prestado.");
return false;
}
disponible = false;
return true;
}
@Override
public boolean devolver() {
if (disponible) {
System.out.println("AVISO: '" + titulo + "' ya estaba disponible.");
return false;
}
disponible = true;
return true;
}
// ---------- Contrato Notificable ----------
@Override public String getCanalAviso() { return "correo"; }
public int getDiasPreaviso() { return 2; }
@Override
public String generarAviso(int diasTranscurridos) {
return String.format("Aviso por %s con %d dias de preaviso: '%s' acumula %.2f EUR.",
getCanalAviso(), getDiasPreaviso(), titulo,
calcularMulta(diasTranscurridos));
}
// ---------- Representacion e identidad ----------
public String describir() {
return getTipo() + " \"" + titulo + "\" (ref. " + referencia + ")";
}
@Override
public boolean equals(Object o) {
if (this == o) { return true; }
if (o == null || getClass() != o.getClass()) { return false; }
Material otro = (Material) o;
return referencia.equals(otro.referencia);
}
@Override
public int hashCode() { return referencia.hashCode(); }
@Override
public String toString() {
return getTipo() + "[ref=" + referencia + ", titulo=" + titulo
+ ", disponible=" + disponible + "]";
}
}Cuatro cambios que merecen atención:
abstract class:new Material(...)ya no compila.- Constructor
protected: dice la verdad sobre quién puede llamarlo. getTipo(),getDiasPrestamo()ygetTarifaDiaria()son abstractos: desaparecen las constantes de rellenoDIAS_PRESTAMO = 15yTARIFA_DIARIA = 0.25deMaterial. Cada soporte declara las suyas y nadie puede olvidarlas.calcularDiasRetraso,calcularMultayclasificarGravedadsonfinal: son la política de la empresa, no negociable por subclase.
Y observa describir(): ahora llama a getTipo(), un método abstracto. Un método concreto de la clase base invocando a un método que aún no existe. Funciona gracias al despacho dinámico de 03-06: en tiempo de ejecución, this es siempre un objeto concreto, y su getTipo() está implementado.
- Refactor de
Libro, Revista y Dvd
Libro, Revista y DvdAhora cada soporte declara sus tres decisiones, y ninguna se puede omitir.
package com.nexussoftware.bibliotech.dominio;
/** Libro tecnico del catalogo. Plazo largo y tarifa estandar. */
public class Libro extends Material {
public static final int DIAS_PRESTAMO_LIBRO = 15;
public static final double TARIFA_DIARIA_LIBRO = 0.25;
private final String autor;
private final int anioPublicacion;
public Libro(String titulo, String autor, String isbn,
int anioPublicacion, boolean disponible) {
super(titulo, isbn, disponible);
this.autor = (autor == null || autor.isBlank()) ? "Desconocido" : autor.trim();
this.anioPublicacion = (anioPublicacion < 1450 || anioPublicacion > 2100)
? 0 : anioPublicacion;
}
public Libro(String titulo, String autor, String isbn, int anioPublicacion) {
this(titulo, autor, isbn, anioPublicacion, true);
}
public String getAutor() { return autor; }
public int getAnioPublicacion() { return anioPublicacion; }
public String getIsbn() { return getReferencia(); }
// --- Las tres decisiones OBLIGATORIAS ---
@Override public String getTipo() { return "Libro"; }
@Override public int getDiasPrestamo() { return DIAS_PRESTAMO_LIBRO; }
@Override public double getTarifaDiaria() { return TARIFA_DIARIA_LIBRO; }
@Override
public String describir() {
return super.describir() + " - " + autor + ", " + anioPublicacion;
}
}package com.nexussoftware.bibliotech.dominio;
/** Revista tecnica. Circula mucho: plazo corto y tarifa baja. */
public class Revista extends Material {
public static final int DIAS_PRESTAMO_REVISTA = 7;
public static final double TARIFA_DIARIA_REVISTA = 0.10;
private final int numero;
private 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();
}
public int getNumero() { return numero; }
public String getPeriodicidad() { return periodicidad; }
@Override public String getTipo() { return "Revista"; }
@Override public int getDiasPrestamo() { return DIAS_PRESTAMO_REVISTA; }
@Override public double getTarifaDiaria() { return TARIFA_DIARIA_REVISTA; }
@Override public String getCanalAviso() { return "chat"; }
@Override public int getDiasPreaviso() { return 1; }
@Override
public String describir() {
return super.describir() + " - n." + numero + " (" + periodicidad + ")";
}
}package com.nexussoftware.bibliotech.dominio;
/** DVD de formacion. Pocas copias: plazo minimo 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;
private final int duracionMinutos;
public Dvd(String titulo, String referencia, int duracionMinutos) {
super(titulo, referencia, true);
this.duracionMinutos = Math.max(0, duracionMinutos);
}
public int getDuracionMinutos() { return duracionMinutos; }
@Override public String getTipo() { return "DVD"; }
@Override public int getDiasPrestamo() { return DIAS_PRESTAMO_DVD; }
@Override public double getTarifaDiaria() { return TARIFA_DIARIA_DVD; }
@Override public String getCanalAviso() { return "telefono"; }
@Override public int getDiasPreaviso() { return 1; }
@Override
public String describir() {
return super.describir() + " - " + duracionMinutos + " min";
}
}Prueba del resultado:
Material[] catalogo = {
new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Dvd("Refactorizacion en vivo", "DVD-0007", 95)
};
System.out.printf("%-10s %-6s %-8s %-10s %s%n",
"TIPO", "PLAZO", "TARIFA", "MULTA@20d", "GRAVEDAD");
for (Material m : catalogo) {
System.out.printf("%-10s %-6d %-8.2f %-10.2f %s%n",
m.getTipo(), m.getDiasPrestamo(), m.getTarifaDiaria(),
m.calcularMulta(20), m.clasificarGravedad(20));
}
// Material generico = new Material("X", "REF", true); // NO COMPILATIPO PLAZO TARIFA MULTA@20d GRAVEDAD Libro 15 0,25 1,25 LEVE Revista 7 0,10 1,30 GRAVE DVD 3 0,50 8,50 GRAVE
Un único calcularMulta, escrito una sola vez, produciendo tres resultados distintos y correctos. Y la línea comentada es la mejora principal: el objeto sin sentido ya no se puede crear.
Errores Comunes y Consejos
Poner un método abstracto en una clase no abstracta. El error es missing method body, or declare abstract. Si un método no puede implementarse en la clase base, la clase base es abstracta. No hay término medio.
Intentar new sobre la clase abstracta. Material m = new Material(...) no compila. Lo que sí es legal, y confunde a mucha gente, es Material m = new Libro(...): la clase abstracta es un tipo perfectamente válido para declarar.
Declarar el constructor public en una clase abstracta. Compila, pero miente: sugiere un acceso que no existe. Usa protected.
Combinar abstract con final, static o private. Las tres combinaciones son contradictorias y el compilador las rechaza. abstract significa "hay que sobrescribirlo"; final significa "no se puede", static significa "no es polimórfico" y private significa "no se ve".
Olvidar que la subclase parcial debe seguir siendo abstracta. Si implementas dos de tres métodos abstractos, la clase resultante sigue incompleta.
Elegir clase abstracta por costumbre. Es la trampa de diseño más cara del módulo. Al obligar a extends, gastas la única herencia de tus usuarios. Empieza siempre por la interfaz y añade la base abstracta solo si hay estado o código real que compartir.
Llamar a un método abstracto (o sobrescribible) desde el constructor de la base. Es un error sutil y grave: cuando se ejecuta el constructor de Material, los campos de Libro aún no están inicializados. Si Material llamara en su constructor a un describir() que usa autor, obtendrías null. Regla: desde un constructor, invoca solo métodos private, static o final.
Consejo: final en el método plantilla. Es lo que convierte una jerarquía en un Template Method de verdad. Si el esqueleto no es final, cualquier subclase puede saltarse la política.
Consejo: documenta el contrato de cada método abstracto. El Javadoc de un método abstracto es el único sitio donde puedes decir a quien lo implemente qué se espera de él: qué debe devolver, qué rangos son válidos, si puede devolver null. Es diseño por contrato (03-08) aplicado al punto exacto donde más falta hace.
Ejercicios
Ejercicio 1: AudioLibro
Añade a BiblioTech un soporte AudioLibro extends Material con un campo propio duracionHoras (double) y un método getNarrador(). Plazo: 10 días. Tarifa: 0,15 €/día. Canal de aviso: "correo" (el heredado). Comprueba que el compilador te obliga a implementar los tres métodos abstractos e integra el nuevo soporte en el array catalogo sin tocar el bucle que lo recorre.
Ejercicio 2: Template Method para el recibo
Crea una clase abstracta InformeMaterial en el paquete presentacion con:
- Un método
public final String generar(Material m, int diasTranscurridos)que devuelvacabecera() + cuerpo(m, dias) + pie(). cabecera()ypie()como métodos concretos con una implementación por defecto.cuerpo(Material, int)como método abstracto.
Crea dos subclases: InformeBreve (una línea con título y multa) e InformeDetallado (título, tipo, plazo, retraso, multa y gravedad, una por línea). Comprueba que el esqueleto se escribe una sola vez.
Ejercicio 3: elegir interfaz o clase abstracta
Para cada caso, decide si usarías interfaz, clase abstracta o ambas, y justifica en una frase:
- Todo objeto de BiblioTech que se pueda exportar a un fichero de texto.
- La base común de tres tipos de empleado (
Interno,Externo,Becario) que comparten nombre, identificador y el límite de préstamos, pero con distinto máximo. - La capacidad de comparar dos materiales para ordenarlos.
- Un catálogo de préstamos con distintas implementaciones futuras: en memoria, en fichero y en base de datos.
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;
private final String narrador;
private final double duracionHoras;
public AudioLibro(String titulo, String referencia,
String narrador, double duracionHoras) {
super(titulo, referencia, true); // validacion centralizada en Material
this.narrador = (narrador == null || narrador.isBlank())
? "Desconocido" : narrador.trim();
this.duracionHoras = Math.max(0.0, duracionHoras);
}
public String getNarrador() { return narrador; }
public double getDuracionHoras() { return duracionHoras; }
// Los tres metodos que el compilador EXIGE:
@Override public String getTipo() { return "Audiolibro"; }
@Override public int getDiasPrestamo() { return DIAS_PRESTAMO_AUDIO; }
@Override public double getTarifaDiaria() { return TARIFA_DIARIA_AUDIO; }
@Override
public String describir() {
return super.describir() + " - narrado por " + narrador
+ ", " + duracionHoras + " h";
}
}Si omites, por ejemplo, getTarifaDiaria():
error: AudioLibro is not abstract and does not override
abstract method getTarifaDiaria() in MaterialY la integración:
Material[] catalogo = {
new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Dvd("Refactorizacion en vivo", "DVD-0007", 95),
new AudioLibro("Patrones de Diseno", "AUD-0002", "Nuria Vidal", 12.5) // unica linea nueva
};TIPO PLAZO TARIFA MULTA@20d GRAVEDAD Libro 15 0,25 1,25 LEVE Revista 7 0,10 1,30 GRAVE DVD 3 0,50 8,50 GRAVE Audiolibro 10 0,15 1,50 GRAVE
El bucle no se ha tocado. Un soporte nuevo cuesta una clase y una línea de datos: cero modificaciones en la lógica existente. Y lo nuevo respecto al módulo 3 es que el compilador ha garantizado que declaraste plazo, tarifa y tipo. Con la versión antigua de Material, olvidar la tarifa habría cobrado silenciosamente 0,25 €/día.
Solución 2
package com.nexussoftware.bibliotech.presentacion;
import com.nexussoftware.bibliotech.dominio.Material;
/** Base de todos los informes de material. Define el esqueleto; no el contenido. */
public abstract class InformeMaterial {
private static final String LINEA = "----------------------------------------";
/**
* TEMPLATE METHOD: la estructura del informe es fija (final)
* y el unico paso variable es el cuerpo.
*/
public final String generar(Material m, int diasTranscurridos) {
return cabecera() + cuerpo(m, diasTranscurridos) + pie();
}
/** Paso concreto: valido por defecto, sobrescribible si hace falta. */
protected String cabecera() {
return LINEA + System.lineSeparator()
+ " BIBLIOTECH - NEXUS SOFTWARE" + System.lineSeparator()
+ LINEA + System.lineSeparator();
}
/** Paso concreto. */
protected String pie() {
return LINEA + System.lineSeparator();
}
/** PASO VARIABLE: cada informe decide que muestra. */
protected abstract String cuerpo(Material m, int diasTranscurridos);
}package com.nexussoftware.bibliotech.presentacion;
import com.nexussoftware.bibliotech.dominio.Material;
/** Una sola linea: para listados largos. */
public class InformeBreve extends InformeMaterial {
@Override
protected String cuerpo(Material m, int diasTranscurridos) {
return String.format(" %-28s %6.2f EUR%n",
m.getTitulo(), m.calcularMulta(diasTranscurridos));
}
}package com.nexussoftware.bibliotech.presentacion;
import com.nexussoftware.bibliotech.dominio.Material;
/** Informe completo: para incidencias y reclamaciones. */
public class InformeDetallado extends InformeMaterial {
@Override
protected String cuerpo(Material m, int diasTranscurridos) {
StringBuilder sb = new StringBuilder();
sb.append(String.format(" %-14s %s%n", "Titulo:", m.getTitulo()));
sb.append(String.format(" %-14s %s%n", "Tipo:", m.getTipo()));
sb.append(String.format(" %-14s %d dias%n", "Plazo:", m.getDiasPrestamo()));
sb.append(String.format(" %-14s %d dias%n", "Retraso:", m.calcularDiasRetraso(diasTranscurridos)));
sb.append(String.format(" %-14s %.2f EUR%n", "Multa:", m.calcularMulta(diasTranscurridos)));
sb.append(String.format(" %-14s %s%n", "Gravedad:", m.clasificarGravedad(diasTranscurridos)));
return sb.toString();
}
}Material dvd = new Dvd("Refactorizacion en vivo", "DVD-0007", 95);
InformeMaterial breve = new InformeBreve();
InformeMaterial detallado = new InformeDetallado();
System.out.print(breve.generar(dvd, 20));
System.out.print(detallado.generar(dvd, 20));---------------------------------------- BIBLIOTECH - NEXUS SOFTWARE ---------------------------------------- Refactorizacion en vivo 8,50 EUR ---------------------------------------- ---------------------------------------- BIBLIOTECH - NEXUS SOFTWARE ---------------------------------------- Titulo: Refactorizacion en vivo Tipo: DVD Plazo: 3 dias Retraso: 17 dias Multa: 8,50 EUR Gravedad: GRAVE ----------------------------------------
Cuatro detalles de diseño: generar es final (la estructura no se negocia); cuerpo es protected porque es un punto de extensión para las subclases y no parte de la API pública; cabecera y pie son concretos y protected, así que una futura subclase puede sobrescribirlos sin verse obligada; y la variable se declara InformeMaterial, no InformeBreve, así que cambiar de informe cuesta una palabra.
Solución 3
| Caso | Decisión | Justificación |
|---|---|---|
| 1. Exportable a texto | Interfaz (Exportable con String aLinea()) |
Es una capacidad que deben firmar clases sin parentesco (Material, Empleado, Prestamo, SalaReuniones), y no requiere estado compartido |
| 2. Base de tres tipos de empleado | Clase abstracta (Empleado con getMaxPrestamos() abstracto) |
Comparten campos (nombre, identificador, prestamosAcumulados), constructor con validación y lógica real; forman una familia genuina y solo varía un valor |
| 3. Comparar materiales | Interfaz, y además una del JDK: Comparable<Material> |
Es puro comportamiento sin estado, ya existe en la biblioteca estándar y no debe consumir la herencia. Se aplica en 04-04 y 05-09 |
| 4. Catálogo con varias implementaciones | Ambas: interfaz CatalogoPrestamos más base AbstractCatalogoPrestamos |
La interfaz es el tipo público del que dependen las reglas de negocio (inversión de dependencias, 04-01); la base abstracta ahorra a las tres implementaciones el código repetido de validación y formateo, sin obligar a nadie a usarla |
El caso 4 es el arquetipo profesional y el que verás una y otra vez a partir del módulo 5: la interfaz define el tipo, la clase abstracta ofrece ayuda opcional, y las clases concretas eligen.
Conclusión
Has completado el par que sostiene el diseño en Java. Sabes que abstract en una clase declara que está conceptualmente incompleta y que impide new, sin impedir que la uses como tipo de variables, parámetros y arrays. Sabes que un método abstracto es una obligación que se transmite hacia abajo hasta que una subclase concreta la cumple, y —lo más importante— por qué esa obligación vale más que cualquier implementación de relleno: convierte un olvido silencioso, que aparece semanas después como una multa mal calculada, en un error de compilación instantáneo.
Entiendes lo que una clase abstracta ofrece y una interfaz jamás podrá ofrecer: campos de instancia, constructor y miembros protected. Sabes para qué sirve el constructor de una clase que no se instancia —se ejecuta siempre vía super(...), centraliza la validación común y garantiza las invariantes de la parte compartida— y por qué se declara protected. Tienes la tabla comparativa completa entre interfaz y clase abstracta, incluida la fila que más pesa en la práctica: la clase abstracta consume la única herencia de quien la usa, y por eso el camino profesional empieza siempre por la interfaz.
Y has descubierto el Template Method escribiéndolo, no leyéndolo: un método final que fija el esqueleto del algoritmo y delega los pasos variables en métodos abstractos. Es lo que permite que la fórmula de la multa —retraso por tarifa, con tope de 20 €— exista una sola vez en todo BiblioTech y produzca resultados distintos y correctos para un libro, una revista, un DVD y cualquier soporte que añadas mañana. Ese patrón se formaliza en 12-02. Por último, sabes reconocer el patrón AbstractXxx del JDK, donde interfaz y clase abstracta no compiten, sino que se reparten el trabajo: la primera es el tipo público, la segunda una ayuda opcional.
BiblioTech queda con una jerarquía honesta: Material es abstracta y firma Prestable y Notificable; Libro, Revista, Dvd declaran obligatoriamente su tipo, su plazo y su tarifa; y SalaReuniones demuestra que se puede firmar el contrato sin entrar en la familia.
Pero fíjate en una pieza que arrastras desde 03-07 y que sigue siendo poco satisfactoria: las incidencias de un Prestamo son un String[] de textos sueltos. Una incidencia real tiene un día, un motivo y una gravedad, y merece ser un tipo propio. Ahora bien, ¿una clase de primer nivel, en su propio fichero, para algo que solo tiene sentido dentro de un préstamo? En la lección 04-03, Clases Internas, verás los cuatro tipos de clase anidada que ofrece Java, aprenderás cuándo cada uno es la elección correcta —incluida la que causa fugas de memoria reales en producción— y convertirás esas cadenas sueltas en una Prestamo.Incidencia con nombre, estructura y encapsulamiento propios.
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
