Llegas al cierre del módulo con dos deudas pendientes que arrastras desde el módulo 3. La primera: clasificarGravedad devuelve las cadenas "LEVE" y "GRAVE", que todo el proyecto compara con equals. Nada impide escribir "grave" en minúsculas, "GRAVISIMO" o "LEBE", y el compilador no dirá una palabra; el fallo aparecerá en producción, en forma de una multa mal clasificada. La segunda: has escrito a mano equals, hashCode y toString en Material, en Prestamo.Incidencia y en cada clase de datos del proyecto —las mismas veinte líneas, tres veces, con el riesgo de equivocarte en cualquiera de ellas.
Java tiene una respuesta exacta para cada deuda. Las enumeraciones (enum) convierten un conjunto de valores fijos en un tipo propio: Gravedad.GRAVE no es una cadena, es un valor de un conjunto cerrado que el compilador conoce, con el que un error de escritura es imposible y sobre el que un switch puede comprobar que has cubierto todos los casos. Los registros (record, Java 16) generan automáticamente el constructor, los accesores, equals, hashCode y toString de una clase inmutable de datos, reduciendo cincuenta líneas a una. Al terminar esta lección, BiblioTech estará completo como proyecto de POO avanzada, y verás con claridad qué le sigue faltando para ser un sistema de verdad.
Contenido
- El problema: constantes como
Stringoint - Qué es un
enum - Métodos implícitos:
values,valueOf,name,ordinal - Comparación de enums:
==frente aequals enumcon campos, constructor y métodosenumcon cuerpo por constanteenumenswitchy exhaustividad- El singleton con
enum - Registros: qué es un
record - Qué genera un
recordautomáticamente - Constructor compacto y validación
- Constructores adicionales y métodos propios
- Qué NO puede hacer un
recordy cuándo no usarlo - Records en BiblioTech: DTO y objetos de valor
- Tabla comparativa: clase,
recordyenum - Cierre del módulo: estado de BiblioTech
- Errores Comunes y Consejos
- Ejercicios
- El problema: constantes como
String o int
String o intEste es el método que llevas arrastrando desde 03-07:
public final String clasificarGravedad(int diasTranscurridos) {
int retraso = calcularDiasRetraso(diasTranscurridos);
if (retraso == 0) { return "SIN RETRASO"; }
if (retraso <= UMBRAL_LEVE) { return "LEVE"; }
return "GRAVE";
}Y así se usa por todo el proyecto:
Enumeremos los problemas, porque son cinco y todos serios:
| Problema | Consecuencia |
|---|---|
| Sin seguridad de tipos | equals("grave"), equals("GRABE") y equals("URGENTE") compilan sin protesta y devuelven false siempre |
| Sin exhaustividad | Si añades un tercer nivel de gravedad, nada te avisa de los diez if que hay que actualizar |
| Sin autocompletado | El IDE no puede sugerir los valores válidos: son cadenas cualesquiera |
| Sin comportamiento | La cadena "GRAVE" no sabe cuál es su canal de aviso ni su umbral |
| Coste de comparación | Comparar cadenas recorre caracteres; comparar referencias es una instrucción |
La alternativa tradicional era peor: constantes int.
public static final int GRAVEDAD_SIN_RETRASO = 0;
public static final int GRAVEDAD_LEVE = 1;
public static final int GRAVEDAD_GRAVE = 2;
// ...y nada impide esto:
procesarGravedad(47); // compila perfectamente
procesarGravedad(diasRetraso); // se cuela un valor de otro dominioEste antipatrón tiene nombre en la literatura: int enum pattern. Java 5 lo resolvió de una vez.
- Qué es un
enum
enumUn enum es un tipo cuyos valores posibles son un conjunto fijo y conocido en tiempo de compilación. En su forma más simple:
package com.nexussoftware.bibliotech.dominio;
/** Grado de gravedad del retraso de un prestamo. */
public enum Gravedad {
SIN_RETRASO,
LEVE,
GRAVE
}Con esa declaración, Gravedad es un tipo tan real como String o Material:
Gravedad g = Gravedad.GRAVE;
// Gravedad g2 = "GRAVE"; // NO COMPILA: String no es Gravedad
// Gravedad g3 = Gravedad.GRAVE2;// NO COMPILA: no existe esa constanteBajo el capó, un enum es una clase que hereda de java.lang.Enum y cuyas constantes son instancias public static final de esa clase, creadas por la JVM al cargar el tipo. Tres consecuencias:
- No puedes instanciar un enum con
new. Su constructor es implícitamenteprivate. - Existe exactamente una instancia de cada constante en toda la JVM. Son singletons por construcción.
- Es una clase de pleno derecho: puede tener campos, métodos, constructores e implementar interfaces (apartado 5).
Y clasificarGravedad mejora de inmediato:
public final Gravedad clasificarGravedad(int diasTranscurridos) {
int retraso = calcularDiasRetraso(diasTranscurridos);
if (retraso == 0) { return Gravedad.SIN_RETRASO; }
if (retraso <= UMBRAL_LEVE) { return Gravedad.LEVE; }
return Gravedad.GRAVE;
}if (prestamo.clasificarGravedad() == Gravedad.GRAVE) { // == en vez de equals
escalarIncidencia(prestamo);
}
// if (prestamo.clasificarGravedad() == Gravedad.URGENTE) // NO COMPILAEl error de escritura ha pasado de ser un fallo silencioso en producción a un error de compilación.
- Métodos implícitos:
values, valueOf, name, ordinal
values, valueOf, name, ordinalTodo enum recibe gratis un conjunto de métodos, algunos heredados de java.lang.Enum y otros generados por el compilador.
| Método | Tipo | Qué devuelve |
|---|---|---|
values() |
static |
Un array con todas las constantes, en orden de declaración |
valueOf(String) |
static |
La constante con ese nombre exacto (o IllegalArgumentException) |
name() |
instancia | El nombre exacto de la constante, como texto |
ordinal() |
instancia | Su posición (base 0) en la declaración |
compareTo(E) |
instancia | Comparación por ordinal |
toString() |
instancia | Por defecto igual que name(); sí se puede sobrescribir |
// Recorrer todas las constantes
for (Gravedad g : Gravedad.values()) {
System.out.printf("%d -> %s%n", g.ordinal(), g.name());
}// De texto a enum: util al leer configuracion o entrada del usuario
Gravedad leida = Gravedad.valueOf("GRAVE");
System.out.println(leida == Gravedad.GRAVE); // true
// Gravedad mala = Gravedad.valueOf("grave"); // IllegalArgumentException en ejecucionvalueOf distingue mayúsculas y lanza excepción si no encuentra la constante. Como las excepciones son el módulo 6, mientras tanto conviene un método de conversión tolerante escrito a mano:
/** Convierte texto a Gravedad sin lanzar excepciones. Devuelve SIN_RETRASO si no encaja. */
public static Gravedad desdeTexto(String texto) {
if (texto == null) { return SIN_RETRASO; }
String limpio = texto.trim().toUpperCase().replace(' ', '_');
for (Gravedad g : values()) {
if (g.name().equals(limpio)) { return g; }
}
return SIN_RETRASO;
}Y ahora la advertencia importante: no dependas de ordinal().
// MAL: la logica depende de la POSICION en la declaracion
if (gravedad.ordinal() >= 2) {
escalarIncidencia();
}Ese código funciona hoy. Pero si mañana alguien añade MUY_LEVE entre SIN_RETRASO y LEVE, todos los ordinales se desplazan y el >= 2 pasa a significar otra cosa sin que nada falle ni avise. Lo mismo vale para guardar el ordinal() en un fichero o una base de datos: reordenar las constantes corrompe los datos guardados.
La alternativa correcta es un campo explícito:
public enum Gravedad {
SIN_RETRASO(0),
LEVE(1),
GRAVE(2);
private final int nivel; // valor explicito y estable
Gravedad(int nivel) { this.nivel = nivel; }
public int getNivel() { return nivel; }
}El ordinal() existe porque las colecciones especializadas del JDK (EnumMap, EnumSet) lo usan internamente. Tu código no debería tocarlo nunca.
- Comparación de enums:
== frente a equals
== frente a equalsCon los enums, == es la forma correcta y preferida. Es la única situación del curso en que esto es cierto para objetos.
Gravedad a = Gravedad.GRAVE;
Gravedad b = prestamo.clasificarGravedad();
if (a == b) { } // CORRECTO y preferido
if (a.equals(b)) { } // funciona, pero innecesarioCuatro razones, que recuperan lo que aprendiste en 01-05 y 03-09:
- Solo hay una instancia de cada constante. La igualdad de referencia y la igualdad lógica coinciden por construcción.
Enum.equalsesfinaly está implementado exactamente comothis == other. ==es seguro frente anull.a == nulldafalse;a.equals(b)conanulo lanzaNullPointerException.==da seguridad de tipos. Comparar dos enums de tipos distintos con==no compila; conequalscompila y devuelvefalsesiempre, escondiendo el error.- Es más rápido: una comparación de referencias frente a una llamada a método.
Gravedad g = Gravedad.LEVE;
Periodicidad p = Periodicidad.MENSUAL;
// if (g == p) // NO COMPILA: tipos incomparables. Perfecto.
if (g.equals(p)) { } // compila y siempre es false: bug silencioso
enum con campos, constructor y métodos
enum con campos, constructor y métodosAquí es donde los enums de Java superan con creces a los de otros lenguajes. Una constante de enum puede llevar datos asociados.
Aplicado al proyecto: Libro, Revista y Dvd existen como clases porque cada una tiene su plazo y su tarifa. Pero si esa fuera toda la diferencia —solo datos, sin comportamiento propio ni campos exclusivos— la jerarquía entera sobraría.
package com.nexussoftware.bibliotech.dominio;
/**
* Tipos de material del catalogo, con sus parametros de prestamo.
*
* <p>Cada constante lleva sus datos asociados: sustituye a una jerarquia
* entera cuando lo unico que varia son valores, no comportamiento.</p>
*/
public enum TipoMaterial {
// Cada constante invoca al constructor con SUS valores
LIBRO ("Libro", 15, 0.25, "correo"),
REVISTA("Revista", 7, 0.10, "chat"),
DVD ("DVD", 3, 0.50, "telefono"),
AUDIO ("Audiolibro", 10, 0.15, "correo"); // el punto y coma es OBLIGATORIO
private final String etiqueta;
private final int diasPrestamo;
private final double tarifaDiaria;
private final String canalAviso;
/** El constructor de un enum es implicitamente private. */
TipoMaterial(String etiqueta, int diasPrestamo,
double tarifaDiaria, String canalAviso) {
this.etiqueta = etiqueta;
this.diasPrestamo = diasPrestamo;
this.tarifaDiaria = tarifaDiaria;
this.canalAviso = canalAviso;
}
public String getEtiqueta() { return etiqueta; }
public int getDiasPrestamo() { return diasPrestamo; }
public double getTarifaDiaria() { return tarifaDiaria; }
public String getCanalAviso() { return canalAviso; }
/** Metodo de negocio propio del tipo. */
public double multaPara(int diasRetraso) {
return Math.min(Math.max(0, diasRetraso) * tarifaDiaria, 20.0);
}
public boolean esPlazoCorto() { return diasPrestamo <= 7; }
@Override
public String toString() { return etiqueta; }
}Puntos de sintaxis que hay que fijar:
- Las constantes van primero, antes que cualquier campo o método.
- El punto y coma tras la última constante es obligatorio si hay algo más en el cuerpo. Es el error de compilación más común con enums.
- El constructor es implícitamente
privatey no puede ser otra cosa. Se invoca una sola vez por constante, al cargar la clase. - Los campos deben ser
final. Técnicamente se permiten mutables, pero un enum mutable es un error de diseño: la constante es única y compartida por todo el programa.
Y lo que ahora se puede hacer con él:
System.out.printf("%-12s %-6s %-8s %-10s %s%n",
"TIPO", "PLAZO", "TARIFA", "CANAL", "MULTA@17d");
for (TipoMaterial t : TipoMaterial.values()) {
System.out.printf("%-12s %-6d %-8.2f %-10s %.2f EUR%n",
t.getEtiqueta(), t.getDiasPrestamo(), t.getTarifaDiaria(),
t.getCanalAviso(), t.multaPara(17));
}TIPO PLAZO TARIFA CANAL MULTA@17d Libro 15 0,25 correo 4,25 EUR Revista 7 0,10 chat 1,70 EUR DVD 3 0,50 telefono 8,50 EUR Audiolibro 10 0,15 correo 2,55 EUR
¿Sustituye esto a la jerarquía Material? No del todo, y esa es la lección de diseño:
| Situación | Elige |
|---|---|
| Los tipos solo difieren en valores (plazo, tarifa, canal) | enum: cuatro líneas frente a cuatro clases |
| Los tipos tienen campos propios distintos | Jerarquía de clases |
| Los tipos tienen comportamiento estructuralmente distinto | Jerarquía de clases |
| Ambos | Jerarquía con un enum como campo |
En BiblioTech se cumple lo tercero: Libro tiene autor y anioPublicacion, Revista tiene numero y periodicidad, Dvd tiene duracionMinutos. La jerarquía se queda. Pero TipoMaterial sigue siendo útil como campo que centraliza los parámetros comunes, y lo aplicarás en el apartado 16.
enum con cuerpo por constante
enum con cuerpo por constanteUna vuelta de tuerca más: cada constante puede tener su propia implementación de un método. La sintaxis es un cuerpo { ... } tras los argumentos del constructor.
package com.nexussoftware.bibliotech.dominio;
/** Gravedad del retraso, con la accion que corresponde a cada nivel. */
public enum Gravedad {
SIN_RETRASO(0) {
@Override
public String accionRecomendada() {
return "Ninguna. El prestamo esta en plazo.";
}
@Override
public boolean requiereAviso() { return false; }
},
LEVE(1) {
@Override
public String accionRecomendada() {
return "Enviar recordatorio por el canal habitual.";
}
@Override
public boolean requiereAviso() { return true; }
},
GRAVE(2) {
@Override
public String accionRecomendada() {
return "Escalar al responsable y bloquear nuevos prestamos.";
}
@Override
public boolean requiereAviso() { return true; }
};
private final int nivel;
Gravedad(int nivel) { this.nivel = nivel; }
public int getNivel() { return nivel; }
/** Cada constante DEBE implementarlo: es abstracto. */
public abstract String accionRecomendada();
/** Cada constante lo implementa; podria tener implementacion por defecto. */
public abstract boolean requiereAviso();
/** Metodo comun a todas las constantes. */
public boolean esMasGraveQue(Gravedad otra) {
return this.nivel > otra.nivel;
}
}for (Gravedad g : Gravedad.values()) {
System.out.printf("%-12s nivel %d aviso=%-5b %s%n",
g, g.getNivel(), g.requiereAviso(), g.accionRecomendada());
}
System.out.println("GRAVE mas grave que LEVE: " + Gravedad.GRAVE.esMasGraveQue(Gravedad.LEVE));SIN_RETRASO nivel 0 aviso=false Ninguna. El prestamo esta en plazo. LEVE nivel 1 aviso=true Enviar recordatorio por el canal habitual. GRAVE nivel 2 aviso=true Escalar al responsable y bloquear nuevos prestamos. GRAVE mas grave que LEVE: true
¿Qué está pasando aquí realmente? Cada constante con cuerpo es una subclase anónima del enum (04-04). Puedes comprobarlo:
System.out.println(Gravedad.LEVE.getClass().getName()); // ...Gravedad$2
System.out.println(Gravedad.class.getName()); // ...GravedadLa consecuencia práctica es enorme: el compilador obliga a implementar el método abstracto en cada constante. Si mañana añades MUY_GRAVE sin accionRecomendada(), el código no compila. Compara eso con un switch sobre cadenas, donde el caso nuevo simplemente cae en el default y nadie se entera.
¿Cuerpo por constante o switch? Usa cuerpo por constante cuando la lógica pertenece al valor y varía en cada uno. Usa campos y métodos comunes cuando la lógica es la misma con datos distintos. Y si el cuerpo por constante empieza a tener veinte líneas, el enum probablemente esté asumiendo responsabilidades ajenas.
enum en switch y exhaustividad
enum en switch y exhaustividadLos enums y el switch están hechos el uno para el otro. Y hay un detalle de sintaxis que sorprende:
public String mensajePara(Gravedad gravedad) {
return switch (gravedad) {
case SIN_RETRASO -> "Todo en orden."; // sin 'Gravedad.'
case LEVE -> "Recordatorio enviado.";
case GRAVE -> "Incidencia escalada.";
};
}Dentro del case no se cualifica la constante: se escribe LEVE, no Gravedad.LEVE. El compilador ya sabe de qué tipo es la expresión del switch, y cualificarla es de hecho un error de compilación.
Y ahora lo verdaderamente valioso, retomando el switch de expresión de 02-03: la exhaustividad.
// switch de EXPRESION sobre un enum: no hace falta 'default'
return switch (gravedad) {
case SIN_RETRASO -> "Todo en orden.";
case LEVE -> "Recordatorio enviado.";
case GRAVE -> "Incidencia escalada.";
};Como un switch de expresión debe producir siempre un valor, el compilador verifica que has cubierto todas las constantes. Si mañana añades MUY_GRAVE al enum:
El compilador te lleva de la mano a cada lugar del proyecto que hay que actualizar. Eso es imposible con cadenas y es la razón número uno para usar enums en el dominio.
Una advertencia sobre el default:
// Con default: compila hoy y compilara manana... haciendo algo mal
return switch (gravedad) {
case SIN_RETRASO -> "Todo en orden.";
case LEVE -> "Recordatorio enviado.";
default -> "Incidencia escalada."; // MUY_GRAVE caeria aqui en silencio
};Regla: en un
switchde expresión sobre un enum, omite eldefaultsiempre que puedas. Es lo que te da la comprobación de exhaustividad, que es justamente lo que has venido a buscar.
En un switch clásico de sentencia (con case ... :) la exhaustividad no se comprueba, porque no hay obligación de producir un valor. Otro motivo para preferir la forma de flecha.
- El singleton con
enum
enumUna aplicación breve pero conocida: como la JVM garantiza una única instancia de cada constante, un enum de un solo valor es la forma más simple y segura de implementar el patrón Singleton (formalizado en 12-02), inmune a la reflexión y a la serialización.
public enum RegistroBiblioTech {
INSTANCIA;
private int operaciones;
public void registrar(String operacion) {
operaciones++;
System.out.println("[" + operaciones + "] " + operacion);
}
}
// Uso:
RegistroBiblioTech.INSTANCIA.registrar("Prestamo PR-0001 creado");
- Registros: qué es un
record
recordCambiamos de herramienta. Mira esta clase, que solo existe para transportar tres datos:
public final class Ficha {
private final String titulo;
private final String autor;
private final int anio;
public Ficha(String titulo, String autor, int anio) {
this.titulo = titulo;
this.autor = autor;
this.anio = anio;
}
public String getTitulo() { return titulo; }
public String getAutor() { return autor; }
public int getAnio() { return anio; }
@Override
public boolean equals(Object o) {
if (this == o) { return true; }
if (o == null || getClass() != o.getClass()) { return false; }
Ficha otra = (Ficha) o;
return anio == otra.anio
&& Objects.equals(titulo, otra.titulo)
&& Objects.equals(autor, otra.autor);
}
@Override
public int hashCode() { return Objects.hash(titulo, autor, anio); }
@Override
public String toString() {
return "Ficha[titulo=" + titulo + ", autor=" + autor + ", anio=" + anio + "]";
}
}Casi cuarenta líneas, y la única información real son tres nombres y tres tipos. Todo lo demás es mecánico, y por serlo es propenso a errores: olvidar un campo en equals, no actualizar hashCode al añadir uno, dejar un toString desfasado.
Un record (Java 16) expresa exactamente lo mismo:
Una línea. Y es completamente equivalente, con los tres métodos correctamente implementados.
La declaración se lee así:
A los parámetros de la cabecera se les llama componentes. Un record es, por diseño, un portador transparente de datos inmutables: transparente porque sus componentes son exactamente su estado y se pueden leer todos.
- Qué genera un
record automáticamente
record automáticamenteDe esa única línea, el compilador genera:
| Elemento generado | Detalle |
|---|---|
Un campo private final por componente |
No accesible directamente ni desde el propio record salvo por su nombre |
| Constructor canónico | Recibe todos los componentes en orden y los asigna |
| Un accesor por componente | Se llama como el componente: titulo(), no getTitulo() |
equals(Object) |
Compara todos los componentes; cumple el contrato de 03-09 |
hashCode() |
Derivado de todos los componentes; consistente con equals |
toString() |
Ficha[titulo=Java Efectivo, autor=Joshua Bloch, anio=2018] |
Ficha f1 = new Ficha("Java Efectivo", "Joshua Bloch", 2018);
Ficha f2 = new Ficha("Java Efectivo", "Joshua Bloch", 2018);
System.out.println(f1.titulo()); // Java Efectivo (sin 'get')
System.out.println(f1); // Ficha[titulo=Java Efectivo, ...]
System.out.println(f1.equals(f2)); // true
System.out.println(f1.hashCode() == f2.hashCode()); // true
System.out.println(f1 == f2); // false: son objetos distintosFíjate en el detalle de los accesores: se llaman titulo(), no getTitulo(). No es un capricho: es una señal deliberada del diseño del lenguaje de que un record no es un JavaBean, sino un valor. Si necesitas la convención getXxx porque un framework la exige (Hibernate, por ejemplo, módulo 11), puedes añadirla a mano, pero suele ser síntoma de que ahí querías una clase normal.
Y tres propiedades estructurales importantes:
- Un
recordes implícitamentefinal. No se puede extender. - Sus componentes son inmutables. No hay setters ni forma de reasignarlos.
- Hereda de
java.lang.Record, no deObjectdirectamente. Por eso unrecordno puede extender ninguna otra clase (apartado 13).
- Constructor compacto y validación
Un record sin validación aceptaría un título nulo o un año imposible. La solución es el constructor compacto, una forma abreviada exclusiva de los records:
public record Ficha(String titulo, String autor, int anio) {
/**
* Constructor COMPACTO: sin lista de parametros ni asignaciones.
* Se ejecuta antes de asignar los campos, sobre los parametros.
*/
public Ficha {
if (titulo == null || titulo.isBlank()) {
titulo = "Sin titulo"; // se reasigna el PARAMETRO
}
if (autor == null || autor.isBlank()) {
autor = "Desconocido";
}
if (anio < 1450 || anio > 2100) {
anio = 0;
}
titulo = titulo.trim();
autor = autor.trim();
// NO se escribe this.titulo = titulo; el compilador lo hace al final
}
}Tres reglas del constructor compacto:
- No lleva lista de parámetros: se escribe
public Ficha {, nopublic Ficha(String titulo, ...) {. - Dentro trabajas con los parámetros, no con los campos.
titulo = "Sin titulo"cambia el parámetro; el compilador asigna los campos al final, automáticamente. - No escribas
this.titulo = titulo: es innecesario y confuso.
Esto es exactamente el saneamiento de datos que hacías a mano en los constructores del módulo 3, ahora en su sitio natural. En cuanto tengas excepciones (módulo 6), lo habitual será lanzar IllegalArgumentException en lugar de sustituir valores; el mecanismo del constructor compacto es el mismo.
- Constructores adicionales y métodos propios
El cuerpo de un record admite mucho más que el constructor compacto.
Constructores adicionales, que deben delegar en el canónico con this(...) (03-04):
public record Ficha(String titulo, String autor, int anio) {
public Ficha {
if (titulo == null || titulo.isBlank()) { titulo = "Sin titulo"; }
}
/** Ficha sin año conocido. */
public Ficha(String titulo, String autor) {
this(titulo, autor, 0); // OBLIGATORIO delegar en el canonico
}
/** Ficha construida desde un material del catalogo. */
public static Ficha desde(Libro libro) {
return new Ficha(libro.getTitulo(), libro.getAutor(), libro.getAnioPublicacion());
}
}Métodos de instancia propios, que operan sobre los componentes:
public record Ficha(String titulo, String autor, int anio) {
public boolean esClasico() {
return anio > 0 && anio < 2000;
}
public String etiquetaCorta() {
String corto = titulo.length() > 20 ? titulo.substring(0, 17) + "..." : titulo;
return String.format("%-20s (%s)", corto, anio > 0 ? String.valueOf(anio) : "s.f.");
}
/** "Modificador" al estilo inmutable: devuelve una copia nueva. */
public Ficha conAnio(int nuevoAnio) {
return new Ficha(titulo, autor, nuevoAnio);
}
}Ese conAnio es el patrón estándar con tipos inmutables (03-07): no se modifica, se crea una copia con el cambio. Es lo que hace String con toUpperCase() y lo que harás con LocalDate en 10-05.
Constantes, métodos static y tipos anidados, todo permitido:
public record Ficha(String titulo, String autor, int anio) {
public static final Ficha VACIA = new Ficha("Sin titulo", "Desconocido", 0);
public static int comparar(Ficha a, Ficha b) { return a.titulo.compareTo(b.titulo); }
}Implementar interfaces, sí; extender clases, no:
public record Ficha(String titulo, String autor, int anio)
implements Comparable<Ficha> {
@Override
public int compareTo(Ficha otra) {
return this.titulo.compareToIgnoreCase(otra.titulo);
}
}Y también se puede sobrescribir un accesor o un método generado, aunque conviene hacerlo con moderación:
public record Ficha(String titulo, String autor, int anio) {
@Override
public String toString() {
return titulo + " - " + autor + (anio > 0 ? " (" + anio + ")" : "");
}
}
- Qué NO puede hacer un
record y cuándo no usarlo
record y cuándo no usarlo| No puede | Por qué |
|---|---|
| Extender una clase | Ya extiende java.lang.Record |
| Ser extendido | Es implícitamente final |
| Declarar campos de instancia propios | Su estado son exactamente sus componentes; es la garantía de transparencia |
| Tener componentes mutables | Son final por construcción |
Ser abstract |
No tiene sentido en un portador de datos |
Ese tercer punto sorprende a mucha gente:
public record Ficha(String titulo, String autor, int anio) {
// private int contadorAccesos; // NO COMPILA: campo de instancia
private static int fichasCreadas; // SI COMPILA: los static si se permiten
}La razón es el principio de diseño: el estado de un record es su lista de componentes, sin excepciones. Solo así equals, hashCode y toString generados pueden ser correctos por construcción.
Cuándo NO usar un record:
- Cuando el objeto tiene identidad, no valor. Un
EmpleadoconEMP-001sigue siendo el mismo empleado aunque cambie de nombre; dosFichacon los mismos datos son la misma ficha. La pregunta clave: ¿dos objetos con los mismos datos son el mismo objeto? Si sí,record. - Cuando el estado debe cambiar. Un
Prestamocambia de disponibilidad, acumula incidencias y registra devoluciones. No es un record. - Cuando quieres ocultar la representación interna. Un
recordexpone todos sus componentes. Si necesitas encapsular la representación (03-07), usa una clase. - Cuando necesitas heredar. Un
recordno puede extender ni ser extendido. - Cuando la API pública debe seguir la convención
getXxxpor exigencia de un framework antiguo.
flowchart TD
A["Necesito un tipo de datos"] --> B{"Su estado cambia con el tiempo?"}
B -- "Si" --> C["Clase normal"]
B -- "No" --> D{"Dos objetos con los mismos datos son el mismo?"}
D -- "No, tiene identidad propia" --> C
D -- "Si, es un valor" --> E{"Necesito heredar o ocultar campos?"}
E -- "Si" --> C
E -- "No" --> F["record"]
- Records en BiblioTech: DTO y objetos de valor
Dos usos concretos en el proyecto.
Objeto de valor: Ficha. Una ficha bibliográfica de catálogo, sin identidad propia.
package com.nexussoftware.bibliotech.dominio;
/** Ficha bibliografica de catalogo. Objeto de valor inmutable. */
public record Ficha(String titulo, String autor, int anio) implements Comparable<Ficha> {
public Ficha {
titulo = (titulo == null || titulo.isBlank()) ? "Sin titulo" : titulo.trim();
autor = (autor == null || autor.isBlank()) ? "Desconocido" : autor.trim();
anio = (anio < 1450 || anio > 2100) ? 0 : anio;
}
public Ficha(String titulo, String autor) { this(titulo, autor, 0); }
/** Fabrica desde un libro del catalogo. */
public static Ficha desde(Libro libro) {
return new Ficha(libro.getTitulo(), libro.getAutor(), libro.getAnioPublicacion());
}
public boolean esClasico() { return anio > 0 && anio < 2000; }
@Override
public int compareTo(Ficha otra) {
return this.titulo.compareToIgnoreCase(otra.titulo);
}
}DTO de salida: ResumenSesion. Un objeto que transporta el resultado de una sesión de devoluciones desde el servicio hasta la presentación.
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.Gravedad;
/**
* Resultado de una sesion de devoluciones.
*
* <p>Objeto de transferencia (DTO): agrupa datos calculados para entregarlos
* a la capa de presentacion sin exponer el dominio.</p>
*/
public record ResumenSesion(int devoluciones,
double totalMultas,
int incidenciasGraves,
Gravedad gravedadMaxima) {
public ResumenSesion {
devoluciones = Math.max(0, devoluciones);
totalMultas = Math.max(0.0, totalMultas);
incidenciasGraves = Math.max(0, incidenciasGraves);
gravedadMaxima = (gravedadMaxima == null) ? Gravedad.SIN_RETRASO : gravedadMaxima;
}
/** Sesion vacia: punto de partida de la acumulacion. */
public static ResumenSesion vacio() {
return new ResumenSesion(0, 0.0, 0, Gravedad.SIN_RETRASO);
}
/** Devuelve un resumen NUEVO con una devolucion mas. Estilo inmutable. */
public ResumenSesion mas(double multa, Gravedad gravedad) {
return new ResumenSesion(
devoluciones + 1,
totalMultas + multa,
incidenciasGraves + (gravedad == Gravedad.GRAVE ? 1 : 0),
gravedad.esMasGraveQue(gravedadMaxima) ? gravedad : gravedadMaxima);
}
public double multaMedia() {
return devoluciones == 0 ? 0.0 : totalMultas / devoluciones;
}
}Y el uso conjunto, con el enum y el record trabajando juntos:
Prestamo[] devueltos = { p1, p2, p3, p4 };
int[] dias = { 20, 40, 16, 12 };
ResumenSesion resumen = ResumenSesion.vacio();
for (int i = 0; i < devueltos.length; i++) {
double multa = devueltos[i].registrarDevolucion(dias[i]);
Gravedad gravedad = devueltos[i].clasificarGravedad();
resumen = resumen.mas(multa, gravedad); // se reasigna: cada 'mas' crea uno nuevo
}
System.out.println(resumen);
System.out.printf("Multa media: %.2f EUR%n", resumen.multaMedia());
System.out.println("Accion: " + resumen.gravedadMaxima().accionRecomendada());ResumenSesion[devoluciones=4, totalMultas=15.30, incidenciasGraves=2, gravedadMaxima=GRAVE] Multa media: 3,83 EUR Accion: Escalar al responsable y bloquear nuevos prestamos.
Fíjate en el toString() que no has escrito, en el equals que sería correcto si lo necesitaras, y en la última línea: gravedadMaxima() devuelve un Gravedad, y sobre él se llama directamente a accionRecomendada(). Un enum que lleva su comportamiento y un record que lleva sus datos.
- Tabla comparativa: clase,
record y enum
record y enum| Dimensión | Clase | record |
enum |
|---|---|---|---|
| Número de instancias | Ilimitado | Ilimitado | Fijo: una por constante |
| Estado mutable | Sí | No | No debería |
| Constructor | Escrito a mano | Canónico generado + compacto | Implícitamente private |
equals/hashCode/toString |
A mano | Generados | Heredados de Enum, equals es final |
| Accesores | getXxx() a mano |
xxx() generados |
A mano |
| Puede extender clases | Sí | No | No |
| Puede ser extendido | Sí (si no es final) |
No | No (salvo cuerpo por constante) |
| Puede implementar interfaces | Sí | Sí | Sí |
| Campos de instancia propios | Sí | No (solo componentes) | Sí |
| Comparación | equals |
equals |
== |
Exhaustividad en switch |
No | No | Sí |
| Úsalo para | Entidades con identidad y estado | Valores inmutables, DTO | Conjuntos cerrados de valores |
| En BiblioTech | Material, Prestamo, Empleado |
Ficha, ResumenSesion |
Gravedad, TipoMaterial |
- Cierre del módulo: estado de BiblioTech
Aplica los enums al proyecto y el módulo queda cerrado. Prestamo.Incidencia, que en 04-03 guardaba la gravedad como String, pasa a usar el tipo:
public static class Incidencia {
private final int dia;
private final String motivo;
private final Gravedad gravedad; // antes: String
public Incidencia(int dia, String motivo, Gravedad gravedad) {
this.dia = Math.max(0, dia);
this.motivo = (motivo == null || motivo.isBlank())
? "Sin especificar" : motivo.trim();
this.gravedad = (gravedad == null) ? Gravedad.LEVE : gravedad;
}
public int getDia() { return dia; }
public String getMotivo() { return motivo; }
public Gravedad getGravedad() { return gravedad; }
public boolean esGrave() { return gravedad == Gravedad.GRAVE; }
@Override
public String toString() {
return String.format("dia %d - %s [%s]", dia, motivo, gravedad);
}
}Y este es el estado completo del proyecto al terminar el módulo 4:
classDiagram
class Prestable {
<<interface>>
+prestar() boolean
+devolver() boolean
+estaDisponible() boolean
+getDiasPrestamo() int
+diasRestantes(int) int
}
class Notificable {
<<interface>>
+getCanalAviso() String
+generarAviso(int) String
}
class Material {
<<abstract>>
+calcularMulta(int) double
+clasificarGravedad(int) Gravedad
+getTipo()* String
}
class Gravedad {
<<enumeration>>
SIN_RETRASO
LEVE
GRAVE
+accionRecomendada() String
}
class TipoMaterial {
<<enumeration>>
LIBRO
REVISTA
DVD
}
class Ficha {
<<record>>
+titulo() String
+autor() String
}
class ResumenSesion {
<<record>>
+totalMultas() double
}
Prestable <|.. Material
Notificable <|.. Material
Prestable <|.. SalaReuniones
Material <|-- Libro
Material <|-- Revista
Material <|-- Dvd
Prestamo --> Material
Prestamo --> Empleado
Prestamo *-- Incidencia
Incidencia --> Gravedad
Material --> Gravedad
Inventario del proyecto tras el módulo 4:
| Elemento | Tipo | Miembros públicos destacados |
|---|---|---|
Prestable |
interfaz | prestar, devolver, estaDisponible, getDiasPrestamo; default diasRestantes, estaVencido; static plazoValido, contarDisponibles; PLAZO_MAXIMO_DIAS |
Notificable |
interfaz | getCanalAviso, generarAviso; default avisoUrgente, avisoRutinario; private cabecera |
Material |
clase abstracta | abstract getTipo/getDiasPrestamo/getTarifaDiaria; final calcularDiasRetraso/calcularMulta/clasificarGravedad; prestar, devolver, describir, equals, hashCode, toString |
Libro, Revista, Dvd |
clases concretas | Sus tres métodos obligatorios más sus campos propios |
SalaReuniones |
clase | Prestable sin ser Material |
Empleado |
clase | puedeTomarPrestado, registrarPrestamo, registrarDevolucion, getIniciales, getHistorial |
Empleado.Historial |
anidada estática | Contadores acumulados |
Prestamo |
clase | registrarDevolucion, calcularMulta, clasificarGravedad, anotarIncidencia, getIncidencias |
Prestamo.Incidencia |
anidada estática | getDia, getMotivo, getGravedad, esGrave |
Gravedad |
enum | SIN_RETRASO, LEVE, GRAVE; getNivel, accionRecomendada, requiereAviso, esMasGraveQue |
TipoMaterial |
enum | LIBRO, REVISTA, DVD, AUDIO; getEtiqueta, getDiasPrestamo, getTarifaDiaria, multaPara |
Ficha |
record | titulo(), autor(), anio(), esClasico, desde(Libro) |
ResumenSesion |
record | devoluciones(), totalMultas(), gravedadMaxima(), mas, multaMedia, vacio() |
ReglaTarifa, FiltroMaterial |
interfaces funcionales | Un método abstracto cada una |
Catalogo, GestorPrestamos |
servicios | Reciben Predicate, Function, Consumer, Comparator |
ReciboConsola, InformeMaterial |
presentación | Template Method en InformeMaterial |
Errores Comunes y Consejos
Olvidar el punto y coma tras la última constante. Si el enum tiene campos o métodos, la lista de constantes termina en ;. Es el error de compilación número uno con enums.
Depender de ordinal(). Reordenar las constantes cambia todos los ordinales en silencio. Usa un campo explícito.
Guardar ordinal() en un fichero o base de datos. Peor todavía: corrompe datos ya guardados. Guarda name(), que es estable, o un código propio.
Poner default en un switch de expresión sobre un enum. Anula la comprobación de exhaustividad, que es la principal ventaja. Omítelo.
Cualificar la constante dentro del case. Se escribe case LEVE ->, no case Gravedad.LEVE ->. La segunda forma no compila.
Usar equals con enums. Funciona, pero == es más seguro (frente a null), más rápido y detecta comparaciones entre tipos distintos en compilación.
Escribir getTitulo() en un record. El accesor generado se llama titulo(). Si escribes getTitulo(), estás añadiendo un método, no sobrescribiendo nada.
Poner la lista de parámetros en el constructor compacto. public Ficha(String titulo, ...) { es el constructor canónico explícito, no el compacto; entonces sí tienes que asignar los campos a mano.
Intentar declarar un campo de instancia en un record. No compila. Su estado son exactamente sus componentes. Los static sí se permiten.
Usar un record para una entidad con identidad. Un Empleado no es un valor: dos empleados con el mismo nombre no son el mismo empleado. Su igualdad va por identificador, no por todos los campos.
Consejo: enum para todo conjunto cerrado. Estados, tipos, canales, niveles, roles, monedas, días de la semana. Si los valores posibles son fijos y conocidos, es un enum. Convertirlos después cuesta mucho más que empezar bien.
Consejo: record para todo lo que sea un valor. Coordenadas, importes con divisa, rangos, resultados de cálculo, DTO entre capas, claves compuestas. Ahorras código y eliminas una fuente de bugs, porque equals y hashCode generados son correctos por construcción.
Consejo: combínalos. Un record cuyo componente es un enum es una pareja extraordinariamente expresiva, como ResumenSesion(..., Gravedad gravedadMaxima).
Ejercicios
Los ejercicios 2 y 3 se apoyan en los tipos del ejercicio 1, así que conviene hacerlos en orden.
Ejercicio 1: EstadoPrestamo
Crea un enum EstadoPrestamo con las constantes ACTIVO, VENCIDO, DEVUELTO y PERDIDO. Cada una debe llevar una etiqueta legible y un booleano que indique si cuenta para el límite de préstamos del empleado. Añade un método abstract String descripcion() implementado por cada constante y un switch de expresión que devuelva la acción a realizar, sin default. Comprueba qué ocurre al añadir una quinta constante.
Ejercicio 2: record LineaCatalogo
Crea un record LineaCatalogo(String referencia, String titulo, TipoMaterial tipo, boolean disponible) con un constructor compacto que valide, un método estático desde(Material), un método etiqueta() que devuelva una línea formateada y la implementación de Comparable<LineaCatalogo> por título. Genera un array de líneas a partir del catálogo y ordénalo.
Ejercicio 3: informe con enum y record
Escribe un método que recorra un array de préstamos y devuelva un record InformeGravedad(int sinRetraso, int leves, int graves, double totalMultas). Usa un switch de expresión sobre Gravedad sin default y el estilo inmutable (un método mas(...) que devuelva una copia nueva). Imprime el informe y la acción recomendada del nivel más alto detectado.
Soluciones
Solución 1
package com.nexussoftware.bibliotech.dominio;
/** Estado en que se encuentra un prestamo. */
public enum EstadoPrestamo {
ACTIVO("Activo", true) {
@Override public String descripcion() {
return "El material esta en poder del empleado y dentro de plazo.";
}
},
VENCIDO("Vencido", true) {
@Override public String descripcion() {
return "El plazo se ha superado y se acumula multa diaria.";
}
},
DEVUELTO("Devuelto", false) {
@Override public String descripcion() {
return "El material ha vuelto al catalogo y esta disponible.";
}
},
PERDIDO("Perdido", false) {
@Override public String descripcion() {
return "El material se da por perdido; se aplica la multa maxima.";
}
}; // <-- punto y coma OBLIGATORIO
private final String etiqueta;
private final boolean cuentaParaLimite;
EstadoPrestamo(String etiqueta, boolean cuentaParaLimite) {
this.etiqueta = etiqueta;
this.cuentaParaLimite = cuentaParaLimite;
}
public String getEtiqueta() { return etiqueta; }
public boolean cuentaParaLimite() { return cuentaParaLimite; }
/** Cada constante DEBE implementarlo. */
public abstract String descripcion();
@Override public String toString() { return etiqueta; }
/** Accion a realizar. switch de EXPRESION sin default: exhaustividad garantizada. */
public String accion() {
return switch (this) {
case ACTIVO -> "Ninguna. Revisar al vencimiento.";
case VENCIDO -> "Enviar aviso y calcular multa.";
case DEVUELTO -> "Archivar el prestamo y liberar el material.";
case PERDIDO -> "Aplicar multa maxima y dar de baja el material.";
};
}
public static void main(String[] args) {
System.out.printf("%-10s %-8s %s%n", "ESTADO", "LIMITE", "ACCION");
for (EstadoPrestamo e : EstadoPrestamo.values()) {
System.out.printf("%-10s %-8b %s%n", e, e.cuentaParaLimite(), e.accion());
}
System.out.println();
System.out.println("Detalle de VENCIDO: " + EstadoPrestamo.VENCIDO.descripcion());
System.out.println("Comparacion con ==: "
+ (EstadoPrestamo.valueOf("VENCIDO") == EstadoPrestamo.VENCIDO));
}
}ESTADO LIMITE ACCION Activo true Ninguna. Revisar al vencimiento. Vencido true Enviar aviso y calcular multa. Devuelto false Archivar el prestamo y liberar el material. Perdido false Aplicar multa maxima y dar de baja el material. Detalle de VENCIDO: El plazo se ha superado y se acumula multa diaria. Comparacion con ==: true
Qué ocurre al añadir una quinta constante. Si añades RENOVADO("Renovado", true), el compilador produce dos errores, y ambos son exactamente lo que quieres:
error: RENOVADO is not abstract and does not override abstract method descripcion() error: the switch expression does not cover all possible input values
El primero te obliga a describir el estado nuevo; el segundo, a decidir qué acción le corresponde. Ninguno de los dos existiría con cadenas: "RENOVADO" habría caído en un default silencioso y el sistema habría hecho algo incorrecto sin avisar. Esta es la razón número uno para usar enums en el dominio.
Solución 2
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.*;
import java.util.Arrays;
import java.util.Comparator;
/** Linea de catalogo lista para mostrar. Objeto de valor inmutable. */
public record LineaCatalogo(String referencia,
String titulo,
TipoMaterial tipo,
boolean disponible) implements Comparable<LineaCatalogo> {
/** Constructor compacto: valida y normaliza ANTES de asignar los campos. */
public LineaCatalogo {
referencia = (referencia == null || referencia.isBlank())
? "SIN-REF" : referencia.trim();
titulo = (titulo == null || titulo.isBlank())
? "Sin titulo" : titulo.trim();
tipo = (tipo == null) ? TipoMaterial.LIBRO : tipo;
// 'disponible' es boolean: no necesita validacion
}
/** Fabrica desde el dominio: traduce la clase concreta a la constante del enum. */
public static LineaCatalogo desde(Material m) {
TipoMaterial t = switch (m.getTipo()) {
case "Libro" -> TipoMaterial.LIBRO;
case "Revista" -> TipoMaterial.REVISTA;
case "DVD" -> TipoMaterial.DVD;
case "Audiolibro" -> TipoMaterial.AUDIO;
default -> TipoMaterial.LIBRO;
};
return new LineaCatalogo(m.getReferencia(), m.getTitulo(), t, m.estaDisponible());
}
public String etiqueta() {
return String.format("%-16s %-24s %-11s %s",
referencia, titulo, tipo.getEtiqueta(),
disponible ? "LIBRE" : "PRESTADO");
}
@Override
public int compareTo(LineaCatalogo otra) {
return this.titulo.compareToIgnoreCase(otra.titulo);
}
public static void main(String[] args) {
Material[] catalogo = {
new Dvd("Refactorizacion en vivo", "DVD-0007", 95),
new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018),
new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
new Libro("Patrones de Diseno", "Erich Gamma", "978-0000000002", 1994)
};
catalogo[1].prestar();
// Transformar el dominio en lineas de presentacion
LineaCatalogo[] lineas = new LineaCatalogo[catalogo.length];
for (int i = 0; i < catalogo.length; i++) {
lineas[i] = LineaCatalogo.desde(catalogo[i]);
}
// Orden natural (Comparable): por titulo
Arrays.sort(lineas);
System.out.println("--- Orden natural (por titulo) ---");
for (LineaCatalogo l : lineas) { System.out.println(" " + l.etiqueta()); }
// Orden alternativo con Comparator y referencias a metodo (04-06)
Arrays.sort(lineas, Comparator.comparing(LineaCatalogo::tipo)
.thenComparing(LineaCatalogo::titulo));
System.out.println("--- Por tipo y titulo ---");
for (LineaCatalogo l : lineas) { System.out.println(" " + l.etiqueta()); }
// equals y hashCode generados: funcionan sin escribir nada
LineaCatalogo a = new LineaCatalogo("DVD-0007", "Refactorizacion en vivo",
TipoMaterial.DVD, true);
LineaCatalogo b = LineaCatalogo.desde(catalogo[0]);
System.out.println("--- Igualdad por valor ---");
System.out.println(" a.equals(b) = " + a.equals(b));
System.out.println(" toString = " + a);
}
}--- Orden natural (por titulo) --- 978-0000000001 Java Efectivo Libro PRESTADO REV-2024-03 Java Magazine Revista LIBRE 978-0000000002 Patrones de Diseno Libro LIBRE DVD-0007 Refactorizacion en vivo DVD LIBRE --- Por tipo y titulo --- 978-0000000001 Java Efectivo Libro PRESTADO 978-0000000002 Patrones de Diseno Libro LIBRE REV-2024-03 Java Magazine Revista LIBRE DVD-0007 Refactorizacion en vivo DVD LIBRE --- Igualdad por valor --- a.equals(b) = true toString = LineaCatalogo[referencia=DVD-0007, titulo=Refactorizacion en vivo, tipo=DVD, disponible=true]
Tres observaciones. Primera: el orden "por tipo y título" coloca los libros antes que la revista y el DVD porque Comparator.comparing sobre un enum usa su orden natural, que es el de declaración (LIBRO, REVISTA, DVD, AUDIO); es el único uso legítimo del ordinal, y lo hace el JDK por ti. Segunda: a.equals(b) es true sin haber escrito una línea de equals, porque el record compara todos sus componentes. Tercera: LineaCatalogo::tipo y LineaCatalogo::titulo son referencias a los accesores generados, sin get.
Solución 3
package com.nexussoftware.bibliotech.servicio;
import com.nexussoftware.bibliotech.dominio.Gravedad;
import com.nexussoftware.bibliotech.dominio.Prestamo;
public class AnalizadorGravedad {
/** Recuento de prestamos por gravedad. Objeto de valor inmutable. */
public record InformeGravedad(int sinRetraso, int leves, int graves, double totalMultas) {
public InformeGravedad {
sinRetraso = Math.max(0, sinRetraso);
leves = Math.max(0, leves);
graves = Math.max(0, graves);
totalMultas = Math.max(0.0, totalMultas);
}
public static InformeGravedad vacio() {
return new InformeGravedad(0, 0, 0, 0.0);
}
/** Devuelve un informe NUEVO con un prestamo mas. Estilo inmutable. */
public InformeGravedad mas(Gravedad g, double multa) {
// switch de EXPRESION sin default: si se anade una constante, no compila
return switch (g) {
case SIN_RETRASO -> new InformeGravedad(sinRetraso + 1, leves, graves,
totalMultas + multa);
case LEVE -> new InformeGravedad(sinRetraso, leves + 1, graves,
totalMultas + multa);
case GRAVE -> new InformeGravedad(sinRetraso, leves, graves + 1,
totalMultas + multa);
};
}
public int total() { return sinRetraso + leves + graves; }
/** Nivel maximo alcanzado, deducido del recuento. */
public Gravedad gravedadMaxima() {
if (graves > 0) { return Gravedad.GRAVE; }
if (leves > 0) { return Gravedad.LEVE; }
return Gravedad.SIN_RETRASO;
}
public String informeTexto() {
return String.format(
"Prestamos analizados: %d%n"
+ " Sin retraso: %d%n"
+ " Leves: %d%n"
+ " Graves: %d%n"
+ " Multas: %.2f EUR (media %.2f EUR)%n"
+ " Nivel maximo: %s -> %s",
total(), sinRetraso, leves, graves, totalMultas,
total() == 0 ? 0.0 : totalMultas / total(),
gravedadMaxima(), gravedadMaxima().accionRecomendada());
}
}
/** Analiza los prestamos indicados a los dias transcurridos correspondientes. */
public static InformeGravedad analizar(Prestamo[] prestamos, int[] dias) {
InformeGravedad informe = InformeGravedad.vacio();
for (int i = 0; i < prestamos.length; i++) {
double multa = prestamos[i].getMaterial().calcularMulta(dias[i]);
Gravedad gravedad = prestamos[i].getMaterial().clasificarGravedad(dias[i]);
informe = informe.mas(gravedad, multa); // cada 'mas' crea uno nuevo
}
return informe;
}
}Empleado marta = new Empleado("Marta Ruiz", "EMP-001");
Empleado diego = new Empleado("Diego Alonso", "EMP-002");
Prestamo[] prestamos = {
new Prestamo(new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018), marta, 100),
new Prestamo(new Dvd("Refactorizacion en vivo", "DVD-0007", 95), diego, 100),
new Prestamo(new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"), marta, 100),
new Prestamo(new Libro("Patrones de Diseno", "Erich Gamma", "978-0000000002", 1994), diego, 100)
};
int[] dias = { 20, 25, 9, 10 };
System.out.println(AnalizadorGravedad.analizar(prestamos, dias).informeTexto());Prestamos analizados: 4 Sin retraso: 1 Leves: 2 Graves: 1 Multas: 12,45 EUR (media 3,11 EUR) Nivel maximo: GRAVE -> Escalar al responsable y bloquear nuevos prestamos.
Tres puntos de diseño. Primero, el switch no lleva default: si mañana añades MUY_GRAVE a Gravedad, este método no compilará y el compilador te llevará al punto exacto que hay que revisar. Segundo, mas(...) no modifica el informe, devuelve uno nuevo; por eso el bucle escribe informe = informe.mas(...). Es el estilo inmutable de 03-07, natural con records. Tercero, gravedadMaxima().accionRecomendada() encadena el enum con su comportamiento por constante: el informe no tiene ni un solo if sobre el nivel de gravedad.
Conclusión
Has cerrado el módulo con las dos herramientas que convierten datos sueltos en tipos con significado. Sabes que un enum es un tipo cuyos valores posibles forman un conjunto cerrado y conocido en compilación, y qué problemas resuelve frente a las constantes String o int: seguridad de tipos —Gravedad.GRAVISIMO no compila, "GRAVISIMO" sí—, autocompletado, comportamiento asociado y, sobre todo, exhaustividad: un switch de expresión sin default sobre un enum obliga al compilador a llevarte de la mano a cada punto del proyecto que hay que actualizar cuando añades una constante. Conoces sus métodos implícitos —values(), valueOf(), name(), ordinal()— y por qué ordinal() no debe aparecer nunca en tu lógica ni en tus datos guardados: reordenar las constantes lo cambia todo en silencio. Sabes que con enums == es la comparación correcta, más segura frente a null y capaz de detectar en compilación una comparación entre tipos distintos que equals escondería.
Dominas el enum con campos, constructor y métodos, que sustituye una jerarquía entera cuando lo único que varía son valores —y sabes cuándo no debe sustituirla, porque en BiblioTech cada soporte tiene campos propios—; y el enum con cuerpo por constante, donde cada valor implementa a su manera un método abstracto y el compilador exige que ninguno se quede sin implementar.
Sabes que un record declara en una línea lo que costaba cuarenta: genera el constructor canónico, un accesor por componente con el nombre del componente —titulo(), no getTitulo()—, y los tres métodos de Object que tanto trabajo dieron en 03-09, correctos por construcción. Usas el constructor compacto para validar sin repetir la lista de parámetros ni asignar campos a mano, añades constructores adicionales que delegan en el canónico, métodos propios, constantes e interfaces. Y conoces sus límites y su criterio de uso: un record no extiende ni es extendido, su estado son exactamente sus componentes, y la pregunta que decide es siempre la misma: ¿dos objetos con los mismos datos son el mismo objeto? Si sí, es un valor y es un record; si tiene identidad propia o cambia con el tiempo —Empleado, Prestamo—, es una clase.
BiblioTech, tras el módulo 4, es un proyecto de orientación a objetos completo. Material es una clase abstracta que no se puede instanciar, obliga a cada soporte a declarar su tipo, su plazo y su tarifa, y aplica un calcularMulta final en forma de Template Method. Firma dos interfaces, Prestable y Notificable, que una SalaReuniones puede firmar sin entrar en la familia. Las incidencias son una clase anidada estática inmutable, y la gravedad ya no es una cadena sino el enum Gravedad, que además lleva la acción recomendada de cada nivel. El catálogo se ordena y se filtra con comparadores, predicados y funciones que llegan como parámetros, de modo que criterios nuevos no tocan una línea del código existente. Y Ficha y ResumenSesion transportan datos entre capas como valores inmutables, sin una sola línea de código repetitivo.
Y sin embargo, mira de cerca cualquiera de las clases que has escrito y verás el mismo remiendo repetido: Material[] catalogo = new Material[10], Incidencia[] ampliado = Arrays.copyOf(incidencias, incidencias.length + 1), un Prestamo[] de tamaño fijo, una búsqueda de grupos en bucle anidado O(n²) porque no hay forma de buscar por clave. Cada vez que BiblioTech necesita guardar muchas cosas, recurre a un array de tamaño fijo que hay que copiar entero para añadir un elemento, recorrer completo para encontrar uno y recortar a mano para devolver un resultado. Lo has ido marcando como provisional lección tras lección, y ya es lo más urgente que le falta al proyecto.
En el módulo 5, Estructuras de Datos y Colecciones, eso desaparece. Empezarás por dominar los arrays de verdad —incluidos los multidimensionales y la clase Arrays— y entrarás en el Framework de Colecciones de Java: ArrayList para listas que crecen solas, LinkedList para inserciones rápidas, HashMap para buscar por clave en tiempo constante (y ahí verás por fin por qué equals y hashCode tenían que ir siempre juntos), HashSet para conjuntos sin duplicados, colas, pilas y Deque, y los algoritmos de ordenación y búsqueda que aplicarán todos los comparadores que has escrito en este módulo. Los arrays provisionales de BiblioTech se convertirán en colecciones, y aquel informePorTipo de veinte líneas con bucles anidados se quedará en tres.
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
