Módulo 14: Enumeraciones, records y clases selladas
Objetivos del módulo
- Entender por qué las constantes
intson una mala forma de representar un conjunto cerrado de valores - Declarar y usar enumeraciones (
enum) y sus métodosvalues(),valueOf(),ordinal()yname() - Usar enumeraciones en
switchclásicos y en expresionesswitch - Añadir atributos, constructores y métodos a un
enum, incluidos métodos abstractos por constante - Conocer
EnumSetyEnumMap - Crear records (Java 16): componentes, métodos generados, constructor canónico y compacto con validación
- Decidir cuándo usar un record y cuándo una clase normal
- Diseñar jerarquías cerradas con clases e interfaces selladas (Java 17):
sealed,permits,finalynon-sealed - Aplicar pattern matching:
instanceofcon patrón,switchcon patrones de tipo (Java 21), guardaswhen, record patterns y variables sin nombre_(Java 22) - Aprovechar la exhaustividad del compilador con jerarquías selladas
1. El problema: representar un conjunto cerrado de valores
Muchos datos de un programa solo pueden tomar unos pocos valores conocidos de antemano: el día de la semana, el estado de un pedido (pendiente, pagado, enviado…), el palo de una baraja o el color de un semáforo.
Con lo que sabes hasta ahora, la solución más inmediata es usar constantes enteras (static final, módulo 11):
public class ConstantesInt {
// La forma "antigua": constantes enteras
public static final int LUNES = 0, MARTES = 1, MIERCOLES = 2, JUEVES = 3,
VIERNES = 4, SABADO = 5, DOMINGO = 6;
public static final int ROJO = 0; // otra constante con el mismo valor
static boolean esFinDeSemana(int dia) {
return dia == SABADO || dia == DOMINGO;
}
public static void main(String[] args) {
System.out.println(esFinDeSemana(SABADO)); // true, correcto
System.out.println(esFinDeSemana(ROJO)); // compila: ¿un color es un día?
System.out.println(esFinDeSemana(42)); // compila: ¿el día 42?
System.out.println("Hoy es " + MARTES); // imprime un número, no un nombre
}
}
Salida:
true
false
false
Hoy es 1
El programa compila y se ejecuta, pero tiene cuatro problemas graves:
- No hay seguridad de tipos. El parámetro es un
intcualquiera: el compilador aceptaROJOo42como si fueran días. - Los valores no se explican solos. Al imprimir obtienes
1, noMARTES. En una traza o en un fichero, un1no dice nada. - No hay forma de recorrerlos. Si quieres listar todos los días, tienes que escribirlos a mano.
- No se les puede añadir comportamiento. ¿Dónde pondrías el número de horas laborables de cada día o su nombre en español?
📘 Concepto: Una enumeración (
enum) es un tipo de dato cuyo conjunto de valores posibles está cerrado y fijado en el código. Cada valor es una constante con nombre y el compilador no deja usar nada que no sea uno de esos valores.
2. Enumeraciones básicas
2.1. Declarar y usar un enum
enum Dia {
LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO
}
- La palabra reservada es
enum(en lugar declass). - Las constantes se escriben en MAYÚSCULAS separadas por comas (convención de constantes).
- Un
enumes un tipo, igual que una clase: puedes declarar variables, parámetros y atributos de tipoDia. - Normalmente va en su propio fichero
Dia.java(comopublic enum Dia). En los ejemplos de este módulo lo ponemos en el mismo fichero que el programa para que puedas ejecutarlo con un solojava Fichero.java.
enum Dia {
LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO
}
public class DiasSemana {
static boolean esFinDeSemana(Dia dia) {
return dia == Dia.SABADO || dia == Dia.DOMINGO;
}
public static void main(String[] args) {
Dia hoy = Dia.MIERCOLES;
System.out.println("Hoy es " + hoy); // toString() -> nombre
System.out.println("name(): " + hoy.name());
System.out.println("ordinal(): " + hoy.ordinal()); // posición, empieza en 0
System.out.println("¿Fin de semana? " + esFinDeSemana(hoy));
// values(): array con todas las constantes, en orden de declaración
System.out.print("Todos los días:");
for (Dia d : Dia.values()) {
System.out.print(" " + d);
}
System.out.println();
// valueOf(): de texto a constante (distingue mayúsculas)
Dia otro = Dia.valueOf("VIERNES");
System.out.println("valueOf(\"VIERNES\"): " + otro);
// Comparación: con == (cada constante es un objeto único)
System.out.println("¿otro == Dia.VIERNES? " + (otro == Dia.VIERNES));
}
}
Salida:
Hoy es MIERCOLES
name(): MIERCOLES
ordinal(): 2
¿Fin de semana? false
Todos los días: LUNES MARTES MIERCOLES JUEVES VIERNES SABADO DOMINGO
valueOf("VIERNES"): VIERNES
¿otro == Dia.VIERNES? true
Ahora esFinDeSemana(42) o esFinDeSemana(ROJO) no compilan: el parámetro es de tipo Dia y solo admite las siete constantes (o null).
2.2. Qué es realmente una constante de un enum
📘 Concepto: Un
enumes una clase especial. Cada constante es un objeto de esa clase que Java crea una sola vez, al cargar la clase. No puedes crear más connew: el conjunto de objetos está cerrado.
Figura 1. Cada constante de un enum es un objeto único; las variables solo guardan referencias a esos objetos.
Como cada constante es un objeto único, comparar con == es correcto y recomendable (a diferencia de los String, módulo 09). Además, == nunca lanza NullPointerException, mientras que hoy.equals(...) sí lo haría si hoy fuese null.
Todos los enum heredan automáticamente de la clase java.lang.Enum, que les aporta estos métodos:
| Método | Qué devuelve | Ejemplo con Dia.MIERCOLES |
|---|---|---|
name() | El nombre exacto de la constante (String) | "MIERCOLES" |
toString() | Por defecto, lo mismo que name() (se puede sobrescribir) | "MIERCOLES" |
ordinal() | Posición en la declaración, empezando en 0 | 2 |
compareTo(otro) | Negativo, 0 o positivo según el orden de declaración | MIERCOLES.compareTo(VIERNES) → -2 |
Dia.values() | Array nuevo con todas las constantes (método estático) | [LUNES, ..., DOMINGO] |
Dia.valueOf(texto) | La constante con ese nombre exacto (método estático) | Dia.valueOf("LUNES") → LUNES |
2.3. Cuidado con valueOf() y ordinal()
valueOf() exige el nombre exacto, con las mismas mayúsculas. Si no existe, lanza una excepción IllegalArgumentException (las excepciones se estudian a fondo en el módulo 15):
enum Dia {
LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO
}
public class ValueOfError {
public static void main(String[] args) {
System.out.println(Dia.valueOf("lunes".toUpperCase())); // LUNES
System.out.println(Dia.valueOf("lunes")); // excepción
}
}
Salida:
LUNES
Exception in thread "main" java.lang.IllegalArgumentException: No enum constant Dia.lunes
at java.base/java.lang.Enum.valueOf(Enum.java:293)
at Dia.valueOf(ValueOfError.java:1)
at ValueOfError.main(ValueOfError.java:8)
💡 Consejo: Cuando el texto venga del usuario, normalízalo antes con
strip().toUpperCase()y prepárate para el caso de que no exista (lo harás contry/catchen el módulo 15, o recorriendovalues()y comparando).
⚠️ Importante: No uses
ordinal()para guardar datos (en un fichero o una base de datos) ni para hacer cálculos de negocio. Si mañana alguien añade una constante en medio de la lista, todos los ordinales cambian y los datos guardados dejan de significar lo mismo. Si necesitas un número asociado a cada constante, añádelo como atributo (sección 3).
2.4. Enumeraciones en switch
Los enum encajan de maravilla con switch. Dentro de los case se escribe el nombre de la constante sin el prefijo Dia.:
enum Dia {
LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO
}
public class SwitchEnum {
// switch clásico (con dos puntos; aquí return hace de break)
static String plan(Dia dia) {
switch (dia) {
case SABADO:
case DOMINGO:
return "descansar";
default:
return "trabajar";
}
}
// switch como expresión (Java 14): sin break y sin default
static int horasLaborables(Dia dia) {
return switch (dia) {
case LUNES, MARTES, MIERCOLES, JUEVES -> 8;
case VIERNES -> 6;
case SABADO, DOMINGO -> 0;
}; // el compilador comprueba que están TODOS los casos
}
public static void main(String[] args) {
int total = 0;
for (Dia d : Dia.values()) {
System.out.printf("%-10s %-10s %d h%n", d, plan(d), horasLaborables(d));
total += horasLaborables(d);
}
System.out.println("Total semanal: " + total + " h");
}
}
Salida:
LUNES trabajar 8 h
MARTES trabajar 8 h
MIERCOLES trabajar 8 h
JUEVES trabajar 8 h
VIERNES trabajar 6 h
SABADO descansar 0 h
DOMINGO descansar 0 h
Total semanal: 38 h
Fíjate en que la expresión switch de horasLaborables no tiene default. No le hace falta: el compilador sabe que Dia solo tiene siete valores y comprueba que todos están cubiertos. Esto se llama exhaustividad, y es una gran ayuda: si se te olvida un caso, el programa no compila.
Por ejemplo, si borras la línea case SABADO, DOMINGO -> 0;:
SwitchIncompleto.java:7: error: the switch expression does not cover all possible input values
return switch (dia) {
^
1 error
error: compilation failed
💡 Consejo: En las expresiones
switchsobre unenum, evita ponerdefaultsi cubres todas las constantes. Así, cuando alguien añada una constante nueva alenum, el compilador te avisará de todos losswitchque tienes que actualizar. Con undefaultel fallo pasaría desapercibido.
⚠️ Importante: Si la variable del
switchvalenull, se lanzaNullPointerException. Lo verás con más detalle en la sección 7.
3. Enumeraciones con atributos, constructor y métodos
Un enum es una clase, así que puede tener atributos, constructores y métodos. Esto lo convierte en una herramienta mucho más potente que una lista de constantes.
3.1. Atributos y constructor
enum Planeta {
// Cada constante llama al constructor con sus argumentos
MERCURIO("Mercurio", 3.70),
VENUS("Venus", 8.87),
TIERRA("Tierra", 9.81),
MARTE("Marte", 3.71),
JUPITER("Júpiter", 24.79);
// Atributos: final, porque una constante no debe cambiar
private final String nombre;
private final double gravedad; // m/s²
// El constructor de un enum es siempre privado
Planeta(String nombre, double gravedad) {
this.nombre = nombre;
this.gravedad = gravedad;
}
public String getNombre() {
return nombre;
}
// Peso (en newtons) de una masa en este planeta
public double peso(double masaKg) {
return masaKg * gravedad;
}
// Peso "de báscula" equivalente en kg (lo que marcaría una báscula terrestre)
public double kilosEquivalentes(double masaKg) {
return masaKg * gravedad / TIERRA.gravedad;
}
}
public class Planetas {
public static void main(String[] args) {
double masa = 70;
System.out.println("Una persona de " + masa + " kg:");
for (Planeta p : Planeta.values()) {
System.out.printf(" En %-9s pesa %7.1f N (como %5.1f kg en la Tierra)%n",
p.getNombre(), p.peso(masa), p.kilosEquivalentes(masa));
}
}
}
Salida:
Una persona de 70.0 kg:
En Mercurio pesa 259.0 N (como 26.4 kg en la Tierra)
En Venus pesa 620.9 N (como 63.3 kg en la Tierra)
En Tierra pesa 686.7 N (como 70.0 kg en la Tierra)
En Marte pesa 259.7 N (como 26.5 kg en la Tierra)
En Júpiter pesa 1735.3 N (como 176.9 kg en la Tierra)
Reglas que debes recordar:
- La lista de constantes va primero y termina en
;si después hay más código. - El constructor es siempre privado (aunque no lo escribas): nadie puede hacer
new Planeta(...). - Los atributos deberían ser
final: una constante que cambia es una trampa. - Un
enumno puede heredar (ya hereda dejava.lang.Enum), pero sí puede implementar interfaces.
💡 Consejo: Sobrescribe
toString()en elenumsi quieres un texto “bonito” para el usuario (por ejemplo,"Júpiter"con tilde), pero no cambiesname(), que es el que usavalueOf().
3.2. Métodos abstractos por constante
A veces cada constante necesita un comportamiento distinto. Podrías usar un switch (this) dentro de un método, pero hay otra opción elegante: declarar un método abstracto en el enum y que cada constante lo implemente en su propio cuerpo { ... }.
import java.util.EnumMap;
import java.util.EnumSet;
enum Operacion {
SUMA("+") {
public double aplicar(double a, double b) { return a + b; }
},
RESTA("-") {
public double aplicar(double a, double b) { return a - b; }
},
MULTIPLICACION("*") {
public double aplicar(double a, double b) { return a * b; }
},
DIVISION("/") {
public double aplicar(double a, double b) { return a / b; }
};
private final String simbolo;
Operacion(String simbolo) {
this.simbolo = simbolo;
}
public String getSimbolo() {
return simbolo;
}
// Método abstracto: cada constante DEBE implementarlo
public abstract double aplicar(double a, double b);
// Búsqueda por símbolo (valueOf solo busca por nombre)
public static Operacion desdeSimbolo(String s) {
for (Operacion op : values()) {
if (op.simbolo.equals(s)) {
return op;
}
}
throw new IllegalArgumentException("Operador desconocido: " + s);
}
}
public class OperacionesEnum {
public static void main(String[] args) {
double a = 12, b = 4;
for (Operacion op : Operacion.values()) {
System.out.printf("%.1f %s %.1f = %.1f%n", a, op.getSimbolo(), b, op.aplicar(a, b));
}
System.out.println("desdeSimbolo(\"*\") -> " + Operacion.desdeSimbolo("*"));
// EnumSet: conjunto de constantes muy eficiente
EnumSet<Operacion> conmutativas = EnumSet.of(Operacion.SUMA, Operacion.MULTIPLICACION);
System.out.println("Conmutativas: " + conmutativas);
System.out.println("No conmutativas: " + EnumSet.complementOf(conmutativas));
// EnumMap: mapa cuyas claves son constantes de un enum
EnumMap<Operacion, Integer> usos = new EnumMap<>(Operacion.class);
usos.put(Operacion.DIVISION, 3);
usos.put(Operacion.SUMA, 10);
System.out.println("Usos: " + usos); // ordenado según el enum
}
}
Salida:
12.0 + 4.0 = 16.0
12.0 - 4.0 = 8.0
12.0 * 4.0 = 48.0
12.0 / 4.0 = 3.0
desdeSimbolo("*") -> MULTIPLICACION
Conmutativas: [SUMA, MULTIPLICACION]
No conmutativas: [RESTA, DIVISION]
Usos: {SUMA=10, DIVISION=3}
Es polimorfismo (módulo 13) aplicado a constantes: op.aplicar(a, b) ejecuta un código distinto según la constante. Si añades una operación nueva y olvidas implementar aplicar, el programa no compila.
💡 Consejo: Si solo cambian los datos de cada constante, usa atributos (como en
Planeta). Si la lógica es corta, unswitch (this)dentro de un método se lee de un vistazo. Reserva los métodos abstractos por constante para cuando cada una tenga un algoritmo propio algo más largo.
3.3. EnumSet y EnumMap
El ejemplo anterior usa dos colecciones especializadas del paquete java.util:
EnumSet<E>: un conjunto de constantes de un mismoenum(por ejemplo, los días en que abre una tienda). Se crea conEnumSet.of(...),EnumSet.allOf(Dia.class),EnumSet.noneOf(Dia.class),EnumSet.range(Dia.LUNES, Dia.VIERNES)oEnumSet.complementOf(otro).EnumMap<K, V>: un mapa cuyas claves son constantes de unenum(por ejemplo, horario por día). Mantiene las claves en el orden de declaración.
Internamente son muy rápidos (un EnumSet se guarda como una secuencia de bits). Los conjuntos y los mapas se estudian a fondo en el módulo 16; de momento basta con que sepas que existen y que son la opción recomendada cuando las claves o elementos son de un enum.
4. Records (Java 16)
4.1. El problema: clases que solo guardan datos
En los módulos 10 a 12 escribiste muchas clases cuyo único propósito era agrupar datos: un punto, una fecha, un producto, un alumno con su nota. Para que una clase así esté “bien hecha” necesitas atributos private final, constructor, getters, equals(), hashCode() y toString():
final class Punto {
private final int x;
private final int y;
public Punto(int x, int y) { this.x = x; this.y = y; }
public int getX() { return x; }
public int getY() { return y; }
@Override public boolean equals(Object o) { /* comparar x e y */ }
@Override public int hashCode() { return Objects.hash(x, y); }
@Override public String toString() { return "Punto[x=" + x + ", y=" + y + "]"; }
}
Escrita con el formato habitual (un método por bloque, como en el módulo 12) son más de treinta líneas para decir “un punto tiene una x y una y”. Y es código repetitivo y propenso a errores: si añades un atributo z y olvidas actualizar equals(), tendrás un error difícil de encontrar.
4.2. Declarar un record
📘 Concepto: Un record (Java 16) es una clase especial pensada para ser un portador inmutable de datos. Declaras sus componentes en la cabecera y el compilador genera automáticamente los atributos, el constructor, los accesores,
equals(),hashCode()ytoString().
// La misma clase, en una línea
record Punto(int x, int y) {
// Un record puede tener métodos propios
double distanciaAlOrigen() {
return Math.sqrt(x * x + y * y);
}
// "Modificar" un record = crear otro con el cambio
Punto conX(int nuevaX) {
return new Punto(nuevaX, y);
}
}
public class PuntoRecord {
public static void main(String[] args) {
Punto p1 = new Punto(3, 4);
Punto p2 = new Punto(3, 4);
Punto p3 = p1.conX(10);
System.out.println("p1 = " + p1); // toString() generado
System.out.println("p1.x() = " + p1.x()); // accesor (sin "get")
System.out.println("p1.y() = " + p1.y());
System.out.println("p1 == p2: " + (p1 == p2)); // objetos distintos
System.out.println("p1.equals(p2): " + p1.equals(p2)); // mismo contenido
System.out.println("Distancia: " + p1.distanciaAlOrigen());
System.out.println("p3 = " + p3 + ", p1 sigue siendo " + p1);
}
}
Salida:
p1 = Punto[x=3, y=4]
p1.x() = 3
p1.y() = 4
p1 == p2: false
p1.equals(p2): true
Distancia: 5.0
p3 = Punto[x=10, y=4], p1 sigue siendo Punto[x=3, y=4]
Figura 2. Todo lo que escribes a mano en una clase de datos lo genera el compilador en un record.
4.3. Qué genera el compilador
A partir de record Punto(int x, int y), Java genera lo que ves en la figura 2: un atributo private final por componente, el constructor canónico (recibe todos los componentes en orden), un accesor por componente que se llama igual que él (x(), no getX()), y equals(), hashCode() y toString() basados en todos los componentes.
Además, un record es implícitamente final (nadie puede heredar de él) y hereda de java.lang.Record, por lo que no puede usar extends. Sí puede implementar interfaces, tener métodos de instancia y estáticos, constantes estáticas y constructores adicionales. Lo que no puede tener son atributos de instancia extra: todo su estado está en los componentes.
⚠️ Importante: Los accesores no empiezan por
get. Esp.x(), nop.getX(). Es un error muy frecuente al pasar de clases a records.
4.4. Inmutabilidad
Los atributos de un record son final: una vez creado, no puedes cambiar sus valores. No hay setters. Para “modificar” un record se crea otro con el valor cambiado, como hace el método conX() del ejemplo. Es el mismo enfoque que String (módulo 09) y que las clases inmutables del módulo 11.
¿Por qué es bueno? Un objeto inmutable se puede compartir entre varias partes del programa sin miedo a que alguien lo cambie “por detrás”, y es seguro usarlo como clave en un mapa (módulo 16).
Pero ojo: la inmutabilidad de un record es superficial. Si un componente es un objeto mutable (un array, una lista), el record guarda la referencia y el contenido sí se puede modificar:
record Notas(String alumno, int[] valores) { }
int[] array = {7, 8, 9};
Notas n = new Notas("Ana", array);
array[0] = 0; // modificamos el array original...
n.valores()[1] = 0; // ...y el que devuelve el accesor
System.out.println(n.alumno() + " " + Arrays.toString(n.valores())); // Ana [0, 0, 9]
La solución es hacer copias defensivas:
record NotasSeguras(String alumno, int[] valores) {
NotasSeguras {
valores = valores.clone(); // copia defensiva al construir
}
@Override
public int[] valores() {
return valores.clone(); // copia defensiva al devolver
}
}
// Con el mismo código de antes, NotasSeguras sigue mostrando [7, 8, 9]
💡 Consejo: Si un record tiene componentes mutables, haz copias defensivas en el constructor compacto (y, si hace falta, en el accesor). Con listas, en el módulo 16 verás
List.copyOf(lista), que crea una copia inmutable en una sola llamada. Recuerda también queequals()en arrays compara referencias, no contenido, así que dosNotascon arrays iguales pero distintos no seránequals.
4.5. Constructor compacto: validar y normalizar
Un record con datos absurdos (un precio negativo, un nombre vacío) es tan peligroso como una clase sin validar. Para validar, los records ofrecen el constructor compacto: se escribe sin lista de parámetros y se ejecuta antes de que Java asigne los atributos.
record Producto(String nombre, double precio, int stock) {
// Constante estática: permitida
static final double IVA = 0.21;
// Constructor COMPACTO: sin lista de parámetros.
// Se ejecuta antes de asignar los campos; sirve para validar y normalizar.
Producto {
if (nombre == null || nombre.isBlank()) {
throw new IllegalArgumentException("El nombre es obligatorio");
}
if (precio < 0) {
throw new IllegalArgumentException("Precio negativo: " + precio);
}
nombre = nombre.strip(); // normaliza: reasigna el PARÁMETRO
} // aquí Java hace this.nombre = nombre; this.precio = precio; ...
// Constructor adicional: debe delegar en el canónico con this(...)
Producto(String nombre, double precio) {
this(nombre, precio, 0);
}
double precioConIva() {
return precio * (1 + IVA);
}
}
public class RecordValidacion {
public static void main(String[] args) {
Producto p = new Producto(" Teclado ", 25.0, 10);
System.out.println(p);
System.out.printf("Con IVA: %.2f €%n", p.precioConIva());
System.out.println(new Producto("Ratón", 12.5));
try {
new Producto("Monitor", -100, 3);
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Salida:
Producto[nombre=Teclado, precio=25.0, stock=10]
Con IVA: 30.25 €
Producto[nombre=Ratón, precio=12.5, stock=0]
Error: Precio negativo: -100.0
(El bloque try/catch captura la excepción para que el programa continúe; lo estudiarás en el módulo 15.)
Hay tres formas de escribir constructores en un record:
| Forma | Sintaxis | Uso |
|---|---|---|
| Implícito | No escribes nada | Lo genera el compilador; basta si no hay que validar |
| Compacto | Producto { ... } | Validar y normalizar. No se escribe this.x = x: lo hace Java al final |
| Canónico explícito | Producto(String nombre, double precio, int stock) { this.nombre = nombre; ... } | Igual que el compacto pero asignando tú cada atributo. Más largo; casi nunca hace falta |
| Adicional | Producto(String nombre, double precio) { this(nombre, precio, 0); } | Valores por defecto. Debe empezar llamando a otro constructor con this(...) |
⚠️ Importante: En el constructor compacto se trabaja con los parámetros (
nombre = nombre.strip();), no con los atributos. Escribirthis.nombre = ...dentro de un constructor compacto es un error de compilación.
4.6. ¿Record o clase?
| Usa un record cuando… | Usa una clase cuando… |
|---|---|
| El objeto es, ante todo, un conjunto de datos (punto, fecha, coordenada, línea de pedido, resultado de una operación) | El objeto tiene estado que cambia con el tiempo (una cuenta bancaria con saldo, un carrito de la compra) |
| Dos objetos con los mismos datos deben ser iguales | La identidad importa más que el contenido (dos clientes distintos con el mismo nombre no son el mismo) |
| Quieres inmutabilidad sin esfuerzo | Necesitas heredar de otra clase |
| Vas a usarlo como clave de un mapa o elemento de un conjunto | Necesitas ocultar la representación interna (los componentes de un record son públicos a través de sus accesores) |
| Vas a modelar una jerarquía sellada de casos (sección 5) | Es una entidad con ciclo de vida (se crea, se modifica, se da de baja) |
💡 Consejo: Los records también pueden declararse dentro de un método (records locales). Son útiles para agrupar temporalmente dos o tres valores que solo usa ese método, por ejemplo para devolver un nombre y una nota juntos.
✏️ Ejercicio: Crea un
record Rectangulo(double ancho, double alto)que rechace medidas no positivas, con los métodosarea(),perimetro()yesCuadrado(). Comprueba quenew Rectangulo(2, 3).equals(new Rectangulo(2, 3))estrue.
5. Clases e interfaces selladas (Java 17)
5.1. El problema: jerarquías demasiado abiertas
En el módulo 13 viste que cualquier clase puede heredar de una clase no final o implementar una interfaz. Casi siempre es lo que quieres, pero a veces el modelo del problema tiene un número fijo de variantes: una figura de tu aplicación es un círculo, un rectángulo o un triángulo, y nada más; un método de pago es tarjeta, Bizum o efectivo.
Con herencia “abierta”, el compilador no puede saber cuántos subtipos hay, así que te obliga a escribir default en todos los switch y no te avisa cuando aparece un subtipo nuevo. Con final cierras del todo y no puedes tener subclases. Faltaba un punto intermedio.
📘 Concepto: Una clase o interfaz sellada (
sealed, Java 17) declara explícitamente qué clases pueden heredar de ella con la cláusulapermits. Ninguna otra clase puede extenderla o implementarla. El resultado es una jerarquía cerrada que el compilador conoce por completo.
5.2. Sintaxis: sealed, permits, final y non-sealed
// Interfaz sellada: solo Coche, Moto y Camion pueden implementarla
sealed interface Vehiculo permits Coche, Moto, Camion { }
// final: nadie puede heredar de Coche
final class Coche implements Vehiculo {
int plazas = 5;
}
// non-sealed: Moto vuelve a quedar abierta; cualquiera puede heredar de ella
non-sealed class Moto implements Vehiculo { }
class MotoElectrica extends Moto { } // permitido, Moto es non-sealed
// sealed: Camion restringe a su vez sus subclases.
// abstract: no se pueden crear camiones "genéricos", solo Trailer o Furgoneta
abstract sealed class Camion implements Vehiculo permits Trailer, Furgoneta { }
final class Trailer extends Camion { }
final class Furgoneta extends Camion { }
public class Vehiculos {
static int ruedas(Vehiculo v) {
return switch (v) {
case Coche c -> 4;
case Moto m -> 2;
case Trailer t -> 18;
case Furgoneta f -> 4;
}; // sin default: el compilador sabe que no hay más casos
}
public static void main(String[] args) {
Vehiculo[] flota = { new Coche(), new MotoElectrica(), new Trailer(), new Furgoneta() };
for (Vehiculo v : flota) {
System.out.println(v.getClass().getSimpleName() + ": " + ruedas(v) + " ruedas");
}
}
}
Salida:
Coche: 4 ruedas
MotoElectrica: 2 ruedas
Trailer: 18 ruedas
Furgoneta: 4 ruedas
Figura 3. Una jerarquía sellada. Cada subclase permitida elige si cierra (
final), restringe (sealed) o reabre (non-sealed) su rama.
Cada subclase permitida debe declarar uno de los tres modificadores de la figura: final (la rama termina), sealed ... permits ... (solo las subclases que enumere) o non-sealed (rama reabierta a cualquiera).
Los records son implícitamente final y los enum también cumplen la regla, así que pueden ser subtipos permitidos sin escribir nada más. Por eso la combinación “interfaz sellada + records” es tan habitual (sección 8).
Si te saltas una regla, el compilador te avisa:
sealed interface Vehiculo permits Coche, Moto { }
final class Coche implements Vehiculo { }
final class Moto implements Vehiculo { }
// Bicicleta no está en la lista permits
final class Bicicleta implements Vehiculo { }
SelladaError.java:6: error: class is not allowed to extend sealed class: Vehiculo (as it is not listed in its 'permits' clause)
final class Bicicleta implements Vehiculo { }
^
1 error
error: compilation failed
Y si una clase permitida no declara ninguno de los tres modificadores (class Coche implements Vehiculo { }), el error es sealed, non-sealed or final modifiers expected.
⚠️ Importante: Fíjate en el
abstractdeCamion. SiCamionno fuese abstracta, se podría crear unnew Camion()“a secas”, que no es niTrailerniFurgoneta, y elswitchderuedas()dejaría de ser exhaustivo (el compilador diría the switch expression does not cover all possible input values). Cuando una clase sellada intermedia solo sirve para agrupar, hazlaabstract.
5.3. Reglas prácticas
- Las clases permitidas deben estar en el mismo paquete que la clase sellada (o en el mismo módulo, si usas módulos de Java, que verás en el Curso Avanzado).
- Si la clase sellada y todas sus subclases están en el mismo fichero, puedes omitir
permits: el compilador deduce la lista. - Sellar una jerarquía no cambia cómo se usa: sigue habiendo polimorfismo, upcasting, métodos abstractos, etc. Lo que ganas es que el compilador conoce todos los casos, y eso lo aprovecha el
switchcon patrones.
6. Pattern matching con instanceof (Java 16)
En el módulo 13 hiciste downcasting así: comprobar el tipo con instanceof, convertir con un cast y guardar el resultado en una variable. Son tres pasos para una sola idea.
📘 Concepto: El pattern matching (coincidencia de patrones) permite comprobar la forma de un objeto y extraer sus partes en un solo paso. Un patrón de tipo como
String ssignifica: “si el objeto es unString, conviértelo y llámalos”.
public class InstanceofPatron {
static void describir(Object obj) {
// Antes de Java 16 eran tres pasos:
// if (obj instanceof String) { String s = (String) obj; ... }
// Java 16: instanceof con patrón (comprueba, convierte y declara a la vez)
if (obj instanceof String s) {
System.out.println("Texto de " + s.length() + " caracteres");
} else if (obj instanceof Integer n && n > 0) { // n ya se puede usar tras &&
System.out.println("Entero positivo: " + n);
} else if (obj instanceof Integer n) {
System.out.println("Entero no positivo: " + n);
} else {
System.out.println("Otra cosa: " + obj);
}
}
public static void main(String[] args) {
describir("programación");
describir(42);
describir(-7);
describir(3.14);
describir(null); // instanceof con null siempre es false
}
}
Salida:
Texto de 12 caracteres
Entero positivo: 42
Entero no positivo: -7
Otra cosa: 3.14
Otra cosa: null
Detalles importantes:
- La variable de patrón (
s,n) solo existe donde el compilador está seguro de que el patrón ha coincidido: dentro delif, o a la derecha de un&&. - Con
||no se puede usar: enobj instanceof String s || s.isEmpty()no se sabe sisexiste. instanceofconnullsiempre esfalse, así que el patrón nunca te da una variablenull.- Funciona también “en negativo”: tras
if (!(obj instanceof String s)) return;puedes usarsen el resto del método.
💡 Consejo: El caso típico es
equals(). Lo que en el módulo 12 escribías comoif (!(o instanceof Punto)) return false; Punto otro = (Punto) o;ahora esreturn o instanceof Punto otro && x == otro.x && y == otro.y;.
7. switch con patrones (Java 21)
7.1. Patrones de tipo en case
Desde Java 21, un switch puede trabajar con cualquier tipo de objeto (no solo enteros, String o enum) y cada case puede ser un patrón de tipo:
public class SwitchPatrones {
static String describir(Object obj) {
return switch (obj) {
case null -> "es null";
case Integer n when n < 0 -> "entero negativo (" + n + ")";
case Integer n -> "entero " + n;
case String s when s.isBlank() -> "texto vacío";
case String s -> "texto \"" + s + "\" en mayúsculas: " + s.toUpperCase();
case int[] array -> "array de " + array.length + " enteros";
default -> "objeto de tipo " + obj.getClass().getSimpleName();
};
}
public static void main(String[] args) {
Object[] cosas = { 5, -3, "Java", " ", new int[] {1, 2, 3}, 2.5, null };
for (Object c : cosas) {
System.out.println(describir(c));
}
}
}
Salida:
entero 5
entero negativo (-3)
texto "Java" en mayúsculas: JAVA
texto vacío
array de 3 enteros
objeto de tipo Double
es null
7.2. Guardas con when
Una guarda añade una condición extra a un patrón: case Integer n when n < 0 solo coincide si el objeto es un Integer y además es negativo. Dentro de la guarda ya puedes usar la variable del patrón.
Los case se prueban de arriba abajo, así que el orden importa: los casos más específicos (con guarda) deben ir antes que los generales. Si lo haces al revés, el compilador lo detecta, porque el segundo caso nunca se podría alcanzar (se dice que está dominado):
DominanciaError.java:5: error: this case label is dominated by a preceding case label
case Integer n when n < 0 -> "negativo"; // nunca se alcanzaría
^
1 error
error: compilation failed
7.3. null en el switch
Si la variable vale null y ningún case lo contempla, el switch lanza NullPointerException, aunque tenga default (el default no recoge el null). Si null es un valor posible, trátalo explícitamente con case null -> ... (como en SwitchPatrones) o combínalo con el caso por defecto: case null, default -> "otro";.
7.4. Exhaustividad con jerarquías selladas
Un switch con patrones debe ser exhaustivo: tiene que cubrir todos los valores posibles. Con Object eso obliga a poner default. Pero con un tipo sellado, el compilador conoce todos los subtipos y no necesitas default. Fíjate en lo que pasa si se te olvida un caso con la interfaz sellada Figura (permits Circulo, Rectangulo, Triangulo), que verás completa en la sección 8.2:
// Con las figuras de la sección 8.2 (Circulo, Rectangulo y Triangulo):
static double area(Figura f) {
return switch (f) {
case Circulo c -> Math.PI * c.radio() * c.radio();
case Rectangulo r -> r.base() * r.altura();
};
}
ExhaustividadError.java:8: error: the switch expression does not cover all possible input values
return switch (f) {
^
1 error
error: compilation failed
Falta Triangulo y el compilador lo sabe. Este es el gran beneficio de combinar clases selladas y patrones: si mañana añades Hexagono a la lista permits, todos los switch que no lo traten dejarán de compilar, y el compilador te llevará de la mano a cada sitio que tienes que actualizar.
💡 Consejo: ¿Polimorfismo (un método abstracto
area()en cada clase, módulo 13) oswitchcon patrones? Si las operaciones son pocas y los tipos cambian a menudo, el polimorfismo es más natural. Si los tipos son fijos (jerarquía sellada) y vas a añadir muchas operaciones distintas sobre ellos (área, perímetro, dibujar, exportar…), elswitchcon patrones evita llenar cada clase de métodos. Ambos enfoques son correctos.
8. Record patterns y desestructuración (Java 21)
8.1. Desestructurar un record
Un record pattern (patrón de record) no solo comprueba el tipo: además extrae los componentes en variables. A esto se le llama desestructurar.
// Con patrón de tipo: hay que llamar a los accesores
case Rectangulo r -> r.base() * r.altura();
// Con record pattern: los componentes salen ya en variables
case Rectangulo(double b, double h) -> b * h;
Puedes usar var en lugar del tipo de cada componente: case Rectangulo(var b, var h).
8.2. Ejemplo completo: figuras selladas
sealed interface Figura permits Circulo, Rectangulo, Triangulo { }
record Circulo(double radio) implements Figura { }
record Rectangulo(double base, double altura) implements Figura { }
record Triangulo(double base, double altura) implements Figura { }
public class FigurasSelladas {
// Versión 1: patrones de tipo
static double area(Figura f) {
return switch (f) {
case Circulo c -> Math.PI * c.radio() * c.radio();
case Rectangulo r -> r.base() * r.altura();
case Triangulo t -> t.base() * t.altura() / 2;
};
}
// Versión 2: record patterns (desestructuración) + guardas
static String describir(Figura f) {
return switch (f) {
case Circulo(double r) when r == 0 -> "un punto";
case Circulo(double r) -> "círculo de radio " + r;
case Rectangulo(double b, double h) when b == h -> "cuadrado de lado " + b;
case Rectangulo(double b, double h) -> "rectángulo de " + b + " x " + h;
case Triangulo(var b, var h) -> "triángulo de base " + b + " y altura " + h;
};
}
public static void main(String[] args) {
Figura[] figuras = {
new Circulo(1), new Circulo(0), new Rectangulo(2, 3),
new Rectangulo(4, 4), new Triangulo(6, 2)
};
double total = 0;
for (Figura f : figuras) {
double a = area(f);
total += a;
System.out.printf("%-36s área = %6.2f%n", describir(f), a);
}
System.out.printf("Área total: %.2f%n", total);
}
}
Salida:
círculo de radio 1.0 área = 3.14
un punto área = 0.00
rectángulo de 2.0 x 3.0 área = 6.00
cuadrado de lado 4.0 área = 16.00
triángulo de base 6.0 y altura 2.0 área = 6.00
Área total: 31.14
Ni area() ni describir() tienen default, y no les hace falta: la interfaz sellada garantiza que no hay más figuras.
8.3. Patrones anidados y variables sin nombre _ (Java 22)
Los record patterns se pueden anidar: si un componente es a su vez un record, puedes desestructurarlo en el mismo patrón. Y cuando un componente no te interesa, escribe _ (variable sin nombre, Java 22) en lugar de inventarte un nombre que no vas a usar.
record Punto(int x, int y) { }
record Linea(Punto inicio, Punto fin) { }
public class RecordPatterns {
static String analizar(Object obj) {
// record pattern en instanceof: desestructura y comprueba a la vez
if (obj instanceof Punto(int x, int y) && x == y && x != 0) {
return "punto en la diagonal (" + x + ")";
}
return switch (obj) {
case Punto(int x, int y) when x == 0 && y == 0 -> "el origen";
case Punto(int x, int y) -> "punto (" + x + ", " + y + ")";
// patrones ANIDADOS: un record dentro de otro
case Linea(Punto(var x1, var y1), Punto(var x2, var y2)) when x1 == x2 ->
"línea vertical en x=" + x1;
// _ (Java 22): no nos interesa ese componente
case Linea(Punto(_, var y1), Punto(_, var y2)) when y1 == y2 ->
"línea horizontal en y=" + y1;
case Linea(Punto inicio, _) -> "línea que empieza en " + inicio;
default -> "desconocido";
};
}
public static void main(String[] args) {
System.out.println(analizar(new Punto(0, 0)));
System.out.println(analizar(new Punto(5, 5)));
System.out.println(analizar(new Punto(2, 7)));
System.out.println(analizar(new Linea(new Punto(3, 0), new Punto(3, 9))));
System.out.println(analizar(new Linea(new Punto(0, 4), new Punto(8, 4))));
System.out.println(analizar(new Linea(new Punto(1, 2), new Punto(5, 6))));
System.out.println(analizar("hola"));
}
}
Salida:
el origen
punto en la diagonal (5)
punto (2, 7)
línea vertical en x=3
línea horizontal en y=4
línea que empieza en Punto[x=1, y=2]
desconocido
La variable sin nombre _ también sirve fuera de los patrones, en cualquier sitio donde Java te obliga a declarar una variable que no vas a usar:
public class SinNombre {
public static void main(String[] args) {
// Variable de bucle que no usamos: solo queremos repetir 3 veces
for (int _ : new int[3]) {
System.out.print("¡Hola! ");
}
System.out.println();
// Excepción que no necesitamos nombrar
String texto = "12a";
try {
System.out.println(Integer.parseInt(texto));
} catch (NumberFormatException _) {
System.out.println("\"" + texto + "\" no es un número");
}
}
}
Salida:
¡Hola! ¡Hola! ¡Hola!
"12a" no es un número
8.4. Resumen de la evolución del pattern matching
| Versión | Novedad | Ejemplo |
|---|---|---|
| Java 14 | Expresiones switch con -> | case LUNES, MARTES -> 8; |
| Java 16 | instanceof con patrón de tipo y records | if (obj instanceof String s) |
| Java 17 | Clases e interfaces selladas | sealed interface Figura permits ... |
| Java 21 | switch con patrones, guardas when, case null, record patterns | case Integer n when n < 0 -> |
| Java 22 | Variables y patrones sin nombre _ | case Tarjeta(_, boolean ext) |
| Java 25 | Patrones con tipos primitivos (case int i) | Todavía en fase preview: no lo uses en código de producción |
Este curso usa Java 25 LTS. Aunque ya existe el JDK 27, no es LTS; todo lo de este módulo funciona igual en ambas versiones.
9. Ejemplos completos
9.1. Expresiones aritméticas como árbol de records
Una expresión como (2 + 3) * 4 se puede representar como un árbol: la multiplicación tiene como hijos una suma y un número; la suma tiene como hijos dos números. Con una interfaz sellada y records, modelarlo es casi inmediato, y las operaciones (evaluar, mostrar) son switch con patrones recursivos (módulo 07).
Figura 4. La expresión (2 + 3) * 4 representada como un árbol de records.
// Una expresión aritmética representada como un ÁRBOL de records
sealed interface Expr permits Num, Suma, Mult, Neg { }
record Num(double valor) implements Expr { }
record Suma(Expr izq, Expr der) implements Expr { }
record Mult(Expr izq, Expr der) implements Expr { }
record Neg(Expr expr) implements Expr { }
public class Expresiones {
// Evalúa recursivamente el árbol
static double evaluar(Expr e) {
return switch (e) {
case Num(double v) -> v;
case Suma(Expr a, Expr b) -> evaluar(a) + evaluar(b);
case Mult(Expr a, Expr b) -> evaluar(a) * evaluar(b);
case Neg(Expr a) -> -evaluar(a);
};
}
// Convierte el árbol en texto con paréntesis
static String mostrar(Expr e) {
return switch (e) {
case Num(double v) when v == Math.floor(v) -> String.valueOf((long) v);
case Num(double v) -> String.valueOf(v);
case Suma(Expr a, Expr b) -> "(" + mostrar(a) + " + " + mostrar(b) + ")";
case Mult(Expr a, Expr b) -> mostrar(a) + " * " + mostrar(b);
case Neg(Expr a) -> "-" + mostrar(a);
};
}
public static void main(String[] args) {
// (2 + 3) * 4
Expr e1 = new Mult(new Suma(new Num(2), new Num(3)), new Num(4));
System.out.println(mostrar(e1) + " = " + evaluar(e1));
// -(1.5 + 2) * 2
Expr e2 = new Mult(new Neg(new Suma(new Num(1.5), new Num(2))), new Num(2));
System.out.println(mostrar(e2) + " = " + evaluar(e2));
System.out.println(e1); // toString() generado: el árbol completo
}
}
Salida:
(2 + 3) * 4 = 20.0
-(1.5 + 2) * 2 = -7.0
Mult[izq=Suma[izq=Num[valor=2.0], der=Num[valor=3.0]], der=Num[valor=4.0]]
Observa varias cosas:
- La desestructuración
Suma(Expr a, Expr b)saca los dos hijos y la recursión hace el resto: cada función tiene una línea por tipo de nodo. - La guarda de
mostrar(when v == Math.floor(v)) imprime2en lugar de2.0cuando el número es entero. - El
toString()generado de los records muestra el árbol completo, muy útil para depurar. - Ningún
switchtienedefault: si añadesrecord Div(Expr izq, Expr der)a la jerarquía, el compilador te señalará las dos funciones.
9.2. Estados de un pedido con enum
Una máquina de estados describe por qué estados pasa un objeto y qué transiciones están permitidas. Un enum con métodos es la forma natural de modelarla. Un pedido empieza PENDIENTE; puede pasar a PAGADO o CANCELADO; de PAGADO a ENVIADO o CANCELADO; de ENVIADO a ENTREGADO; y ENTREGADO y CANCELADO son estados finales:
enum EstadoPedido {
PENDIENTE, PAGADO, ENVIADO, ENTREGADO, CANCELADO;
// Reglas de transición: desde cada estado, a cuáles se puede pasar
public boolean puedePasarA(EstadoPedido nuevo) {
return switch (this) {
case PENDIENTE -> nuevo == PAGADO || nuevo == CANCELADO;
case PAGADO -> nuevo == ENVIADO || nuevo == CANCELADO;
case ENVIADO -> nuevo == ENTREGADO;
case ENTREGADO, CANCELADO -> false; // estados finales
};
}
public boolean esFinal() {
return this == ENTREGADO || this == CANCELADO;
}
}
class Pedido {
private final int numero;
private EstadoPedido estado = EstadoPedido.PENDIENTE;
Pedido(int numero) {
this.numero = numero;
}
public void cambiarEstado(EstadoPedido nuevo) {
if (!estado.puedePasarA(nuevo)) {
System.out.println(" ✗ Pedido " + numero + ": no se puede pasar de "
+ estado + " a " + nuevo);
return;
}
System.out.println(" ✓ Pedido " + numero + ": " + estado + " -> " + nuevo);
estado = nuevo;
}
public EstadoPedido getEstado() {
return estado;
}
}
public class Pedidos {
public static void main(String[] args) {
Pedido p1 = new Pedido(1001);
p1.cambiarEstado(EstadoPedido.PAGADO);
p1.cambiarEstado(EstadoPedido.ENVIADO);
p1.cambiarEstado(EstadoPedido.CANCELADO); // no permitido
p1.cambiarEstado(EstadoPedido.ENTREGADO);
System.out.println("Estado: " + p1.getEstado() + " (¿final? " + p1.getEstado().esFinal() + ")");
Pedido p2 = new Pedido(1002);
p2.cambiarEstado(EstadoPedido.ENVIADO); // no permitido: no está pagado
p2.cambiarEstado(EstadoPedido.CANCELADO);
p2.cambiarEstado(EstadoPedido.PAGADO); // no permitido: ya es final
}
}
Salida:
✓ Pedido 1001: PENDIENTE -> PAGADO
✓ Pedido 1001: PAGADO -> ENVIADO
✗ Pedido 1001: no se puede pasar de ENVIADO a CANCELADO
✓ Pedido 1001: ENVIADO -> ENTREGADO
Estado: ENTREGADO (¿final? true)
✗ Pedido 1002: no se puede pasar de PENDIENTE a ENVIADO
✓ Pedido 1002: PENDIENTE -> CANCELADO
✗ Pedido 1002: no se puede pasar de CANCELADO a PAGADO
Las reglas de negocio están en un solo sitio (el enum), la clase Pedido no necesita saber cuáles son, y el switch (this) es exhaustivo: si añades el estado DEVUELTO, el compilador te obligará a decidir sus transiciones. En el módulo 15 cambiarás el mensaje ✗ por una excepción propia.
10. Errores comunes
| Error | Síntoma | Solución |
|---|---|---|
Comparar constantes de enum con equals() sobre una variable null | NullPointerException | Usa ==: estado == EstadoPedido.PAGADO |
Dia.valueOf("lunes") | IllegalArgumentException: No enum constant | Normaliza el texto (toUpperCase()) y controla el caso de que no exista |
Guardar ordinal() en un fichero | Los datos cambian de significado al reordenar constantes | Guarda name() o un atributo propio (código) |
Llamar p.getX() en un record | cannot find symbol | Los accesores se llaman como el componente: p.x() |
Asignar this.nombre = ... en el constructor compacto | Error de compilación | Reasigna el parámetro: nombre = nombre.strip(); |
| Pensar que un record con un array es totalmente inmutable | El contenido del array cambia | Copias defensivas (clone(), List.copyOf) |
Subclase permitida sin final/sealed/non-sealed | sealed, non-sealed or final modifiers expected | Añade uno de los tres modificadores |
| Clase sellada intermedia no abstracta | El switch deja de ser exhaustivo | Hazla abstract si solo agrupa subtipos |
| Caso general antes que el caso con guarda | this case label is dominated by a preceding case label | Ordena de lo más específico a lo más general |
switch con patrones que recibe null | NullPointerException | Añade case null -> o case null, default -> |
11. Ejercicios
Ejercicio 1: Semáforo
Crea un enum Semaforo con las constantes VERDE (30 s), AMBAR (5 s) y ROJO (40 s). Cada constante guarda su duración y tiene un método siguiente() que devuelve la luz que viene después (rojo → verde → ámbar → rojo). En main, simula 6 cambios empezando en ROJO, mostrando el instante en que empieza cada luz.
Ver solución
enum Semaforo {
VERDE(30), AMBAR(5), ROJO(40);
private final int segundos;
Semaforo(int segundos) {
this.segundos = segundos;
}
public int getSegundos() {
return segundos;
}
public Semaforo siguiente() {
return switch (this) {
case VERDE -> AMBAR;
case AMBAR -> ROJO;
case ROJO -> VERDE;
};
}
}
public class E1Semaforo {
public static void main(String[] args) {
Semaforo s = Semaforo.ROJO;
int tiempo = 0;
for (int i = 0; i < 6; i++) {
System.out.printf("t=%3d s %-5s (%d s)%n", tiempo, s, s.getSegundos());
tiempo += s.getSegundos();
s = s.siguiente();
}
}
}
Salida:
t= 0 s ROJO (40 s)
t= 40 s VERDE (30 s)
t= 70 s AMBAR (5 s)
t= 75 s ROJO (40 s)
t=115 s VERDE (30 s)
t=145 s AMBAR (5 s)
El método siguiente() usa un switch (this) exhaustivo, sin default. Una alternativa es values()[(ordinal() + 1) % values().length], pero depende del orden de declaración (aquí VERDE, AMBAR, ROJO), y es más frágil: si alguien reordena las constantes, el semáforo cambia de comportamiento sin que nadie lo note.
Ejercicio 2: Meses y estaciones
Crea un enum Mes con los doce meses y sus días (en un año no bisiesto) y un método estacion() que devuelva un enum Estacion { INVIERNO, PRIMAVERA, VERANO, OTONO } (estaciones meteorológicas: diciembre, enero y febrero son invierno, etc.). Usa un switch exhaustivo, sin default. En main, calcula cuántos días tiene cada estación usando un array de enteros indexado por ordinal().
Ver solución
enum Estacion { INVIERNO, PRIMAVERA, VERANO, OTONO }
enum Mes {
ENERO(31), FEBRERO(28), MARZO(31), ABRIL(30), MAYO(31), JUNIO(30),
JULIO(31), AGOSTO(31), SEPTIEMBRE(30), OCTUBRE(31), NOVIEMBRE(30), DICIEMBRE(31);
private final int dias; // en un año no bisiesto
Mes(int dias) {
this.dias = dias;
}
public int getDias() {
return dias;
}
// Estación "meteorológica" en el hemisferio norte
public Estacion estacion() {
return switch (this) {
case DICIEMBRE, ENERO, FEBRERO -> Estacion.INVIERNO;
case MARZO, ABRIL, MAYO -> Estacion.PRIMAVERA;
case JUNIO, JULIO, AGOSTO -> Estacion.VERANO;
case SEPTIEMBRE, OCTUBRE, NOVIEMBRE -> Estacion.OTONO;
};
}
}
public class E2Meses {
public static void main(String[] args) {
int[] diasPorEstacion = new int[Estacion.values().length];
for (Mes m : Mes.values()) {
diasPorEstacion[m.estacion().ordinal()] += m.getDias();
}
for (Estacion e : Estacion.values()) {
System.out.println(e + ": " + diasPorEstacion[e.ordinal()] + " días");
}
}
}
Salida:
INVIERNO: 90 días
PRIMAVERA: 92 días
VERANO: 92 días
OTONO: 91 días
Aquí usar ordinal() como índice del array es aceptable: es un uso interno y momentáneo, no se guarda en ningún sitio. (En el módulo 16 verás que un EnumMap<Estacion, Integer> lo haría todavía más limpio.) Por convención, los nombres de las constantes se escriben sin tildes ni Ñ (Java las admite, pero se evitan), por eso OTONO.
Ejercicio 3: Fracciones con record
Crea un record Fraccion(int num, int den) que, en su constructor compacto:
- Rechace el denominador 0 con
IllegalArgumentException. - Ponga el signo siempre en el numerador (
1/-4se guarda como-1/4). - Simplifique la fracción dividiendo por el máximo común divisor.
Añade un método sumar y un toString() que muestre 3/4 o 2 si el denominador es 1. Comprueba que 2/4 es equals a 1/2.
Ver solución
record Fraccion(int num, int den) {
Fraccion {
if (den == 0) {
throw new IllegalArgumentException("El denominador no puede ser 0");
}
if (den < 0) { // el signo siempre en el numerador
num = -num;
den = -den;
}
int m = mcd(Math.abs(num), den);
num = num / m;
den = den / m;
}
private static int mcd(int a, int b) {
return b == 0 ? (a == 0 ? 1 : a) : mcd(b, a % b);
}
Fraccion sumar(Fraccion o) {
return new Fraccion(num * o.den + o.num * den, den * o.den);
}
@Override
public String toString() {
return den == 1 ? String.valueOf(num) : num + "/" + den;
}
}
public class E3Fraccion {
public static void main(String[] args) {
Fraccion a = new Fraccion(6, 8);
Fraccion b = new Fraccion(1, -4);
System.out.println("a = " + a + ", b = " + b);
System.out.println("a + b = " + a.sumar(b));
System.out.println("2/4 equals 1/2: " + new Fraccion(2, 4).equals(new Fraccion(1, 2)));
try {
new Fraccion(3, 0);
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Salida:
a = 3/4, b = -1/4
a + b = 1/2
2/4 equals 1/2: true
Error: El denominador no puede ser 0
Como el constructor compacto normaliza siempre, dos fracciones equivalentes acaban con los mismos componentes y el equals() generado funciona sin escribirlo. El MCD recursivo es el algoritmo de Euclides del módulo 07; el caso a == 0 evita dividir entre 0 cuando el numerador es 0 (0/5 queda como 0/1).
Ejercicio 4: Baraja española
Crea un enum Palo (oros, copas, espadas, bastos), un enum Valor (as, dos, tres, cuatro, cinco, seis, siete, sota, caballo, rey) y un record Carta(Valor valor, Palo palo) con toString() “SOTA de COPAS”. Genera la baraja de 40 cartas en un array, muestra la primera y la última y comprueba con equals() que la carta de la posición 17 es la sota de copas.
Ver solución
enum Palo { OROS, COPAS, ESPADAS, BASTOS }
enum Valor { AS, DOS, TRES, CUATRO, CINCO, SEIS, SIETE, SOTA, CABALLO, REY }
record Carta(Valor valor, Palo palo) {
@Override
public String toString() {
return valor + " de " + palo;
}
}
public class E4Baraja {
public static void main(String[] args) {
Carta[] baraja = new Carta[Palo.values().length * Valor.values().length];
int i = 0;
for (Palo p : Palo.values()) {
for (Valor v : Valor.values()) {
baraja[i++] = new Carta(v, p);
}
}
System.out.println("Cartas: " + baraja.length);
System.out.println("Primera: " + baraja[0] + ", última: " + baraja[baraja.length - 1]);
System.out.println("¿baraja[17] es la sota de copas? "
+ baraja[17].equals(new Carta(Valor.SOTA, Palo.COPAS)));
}
}
Salida:
Cartas: 40
Primera: AS de OROS, última: REY de BASTOS
¿baraja[17] es la sota de copas? true
El tamaño del array se calcula con values().length en lugar de escribir 40: si un día usas una baraja de 48 cartas (añadiendo OCHO y NUEVE), el código sigue funcionando. Como ampliación, añade a Valor un atributo con los puntos de la brisca (as 11, tres 10, rey 4, caballo 3, sota 2, el resto 0) y suma los de toda la baraja (deben salir 120). La comparación con equals() funciona porque el record compara sus componentes, y los componentes son constantes de enum (objetos únicos).
Ejercicio 5: Métodos de pago sellados
Modela los métodos de pago de una tienda con una interfaz sellada MetodoPago y tres records: Tarjeta(String numero, boolean extranjera), Bizum(String telefono) y Efectivo(). Escribe con un switch con record patterns, sin default, el método comision(metodo, importe): tarjeta nacional 1 %, tarjeta extranjera 3 %, Bizum gratis pero no admitido por encima de 1000 € (devuelve -1), efectivo gratis.
Ver solución
sealed interface MetodoPago permits Tarjeta, Bizum, Efectivo { }
record Tarjeta(String numero, boolean extranjera) implements MetodoPago { }
record Bizum(String telefono) implements MetodoPago { }
record Efectivo() implements MetodoPago { }
public class E5Pagos {
static double comision(MetodoPago metodo, double importe) {
return switch (metodo) {
case Tarjeta(_, boolean extranjera) when extranjera -> importe * 0.03;
case Tarjeta t -> importe * 0.01;
case Bizum b when importe > 1000 -> -1; // no se admite
case Bizum b -> 0;
case Efectivo() -> 0;
};
}
public static void main(String[] args) {
MetodoPago[] metodos = {
new Tarjeta("4111222233334444", false),
new Tarjeta("5500111122223333", true),
new Bizum("600123456"),
new Efectivo()
};
double[] importes = {100, 1500};
for (double imp : importes) {
System.out.println("Importe: " + imp + " €");
for (MetodoPago m : metodos) {
double c = comision(m, imp);
String texto = c < 0 ? "no admitido" : String.format("comisión %.2f €", c);
System.out.printf(" %-10s %s%n", m.getClass().getSimpleName(), texto);
}
}
}
}
Salida:
Importe: 100.0 €
Tarjeta comisión 1.00 €
Tarjeta comisión 3.00 €
Bizum comisión 0.00 €
Efectivo comisión 0.00 €
Importe: 1500.0 €
Tarjeta comisión 15.00 €
Tarjeta comisión 45.00 €
Bizum no admitido
Efectivo comisión 0.00 €
Efectivo() es un record sin componentes: es válido y su patrón es Efectivo(). En comision el caso de la tarjeta extranjera va primero porque tiene guarda; case Tarjeta t recoge el resto. Devolver -1 como “no admitido” es una solución provisional: en el módulo 15 aprenderás a lanzar una excepción, que es la forma correcta de indicar un error.
Ejercicio 6: Clasificar cualquier valor
Escribe static String clasificar(Object o) con un único switch con patrones y guardas que distinga: null; enteros pares e impares; decimales Double (redondeados); cadenas vacías, cadenas formadas solo por dígitos y el resto de cadenas; caracteres; booleanos; y cualquier otro tipo (mostrando su nombre de clase).
Ver solución
public class E6Describir {
static String clasificar(Object o) {
return switch (o) {
case null -> "nada";
case Integer i when i % 2 == 0 -> "entero par";
case Integer i -> "entero impar";
case Double d -> String.format("decimal redondeado: %d", Math.round(d));
case String s when s.isEmpty() -> "cadena vacía";
case String s when s.matches("\\d+") -> "cadena numérica"; // regex, módulo 09
case String s -> "cadena de " + s.length() + " letras";
case Character c -> "carácter '" + c + "'";
case Boolean b -> b ? "verdadero" : "falso";
default -> "tipo no contemplado: " + o.getClass().getSimpleName();
};
}
public static void main(String[] args) {
Object[] valores = { 4, 7, 2.6, "", "2026", "hola", 'x', true, null, 10L };
for (Object v : valores) {
System.out.printf("%-6s -> %s%n", v, clasificar(v));
}
}
}
Salida:
4 -> entero par
7 -> entero impar
2.6 -> decimal redondeado: 3
-> cadena vacía
2026 -> cadena numérica
hola -> cadena de 4 letras
x -> carácter 'x'
true -> verdadero
null -> nada
10 -> tipo no contemplado: Long
Los literales 4, 2.6, 'x' y true se convierten automáticamente a Integer, Double, Character y Boolean al guardarse en un Object[] (autoboxing, módulo 11). 10L es un Long, que no tiene case propio, así que cae en default. Aquí el default es obligatorio porque Object no es sellado. El orden de los tres casos de String es importante: si case String s fuese el primero, dominaría a los otros dos y no compilaría.
Ejercicios propuestos
✏️ Ejercicio 7: Tallas de ropa. Crea un
enum Talla(XS a XXL) con el contorno de pecho mínimo y máximo de cada talla. Añadestatic Talla paraPecho(int cm)y untoString()que muestre “M (96-103 cm)”. Escribe un programa que pida al usuario su contorno conScannery le recomiende una talla.
✏️ Ejercicio 8: Calculadora con enum. Amplía el
enum Operacionde la sección 3.2 conPOTENCIA("^")yMODULO("%"). Escribe un programa que lea expresiones como7 ^ 2(tres partes separadas por espacios), busque la operación condesdeSimbolo()y muestre el resultado. Repite hasta que el usuario escribafin.
✏️ Ejercicio 9: Intervalos. Crea un
record Intervalo(int inicio, int fin)que obligue ainicio <= fin(si vienen al revés, intercámbialos en el constructor compacto). Añadelongitud(),contiene(int x),seSolapa(Intervalo otro)yunion(Intervalo otro)(solo si se solapan; si no, lanzaIllegalArgumentException).
✏️ Ejercicio 10: Impuesto de circulación. Crea una jerarquía sellada
Vehiculocon los recordsTurismo(int caballosFiscales),Motocicleta(int cc),Camion(double toneladas)yBicicleta(). Calcula el impuesto anual con unswitchcon record patterns y guardas (por ejemplo: turismos de menos de 8 CV pagan 25 €, de 8 a 12 CV pagan 68 €, más de 12 CV pagan 145 €; las motos de más de 125 cc pagan 20 €, el resto 8 €; camiones 90 € por tonelada; bicicletas 0 €).
✏️ Ejercicio 11: Más expresiones. Amplía el ejemplo de la sección 9.1 con
record Resta(Expr izq, Expr der),record Div(Expr izq, Expr der)yrecord Var(String nombre).evaluarrecibirá además el valor de la variablex(evaluar(Expr e, double x)). Comprueba que el compilador te señala los dosswitchque hay que completar. Añade también un métodosimplificar(Expr e)con patrones anidados que apliquex * 1 = x,x + 0 = xy--x = x(cuidado con el orden de loscase).
✏️ Ejercicio 12: Reproductor de música. Modela los estados de un reproductor (
DETENIDO,REPRODUCIENDO,PAUSADO) y las acciones (PLAY,PAUSA,STOP) como dosenum. Añade aEstadoun métodoEstado aplicar(Accion a)que devuelva el estado siguiente (por ejemplo,PAUSAestandoDETENIDOno hace nada). Lee una secuencia de acciones del teclado y muestra el estado tras cada una.
Resumen
| Concepto | Sintaxis / ejemplo | Para qué sirve |
|---|---|---|
| Enumeración | enum Dia { LUNES, MARTES } | Conjunto cerrado de constantes con seguridad de tipos |
Métodos de enum | values(), valueOf("X"), name(), ordinal() | Recorrer, convertir desde texto, nombre y posición |
| Comparar constantes | dia == Dia.LUNES | Cada constante es un objeto único |
enum en switch | case LUNES, MARTES -> 8; | Exhaustivo en expresiones switch, sin default |
enum con datos | TIERRA("Tierra", 9.81); + constructor privado | Asociar datos y comportamiento a cada constante |
| Método abstracto por constante | SUMA("+") { double aplicar(...) {...} } | Comportamiento distinto en cada constante |
EnumSet / EnumMap | EnumSet.of(A, B), new EnumMap<>(Dia.class) | Conjuntos y mapas eficientes de constantes |
| Record (Java 16) | record Punto(int x, int y) { }, accesor p.x() | Portador inmutable de datos con equals, hashCode, toString |
| Constructor compacto | Punto { if (x < 0) throw ...; } | Validar y normalizar |
| Clase/interfaz sellada (Java 17) | sealed interface F permits A, B { } | Jerarquía cerrada y conocida por el compilador |
| Subclases permitidas | final, sealed, non-sealed | Cerrar, restringir o reabrir cada rama |
instanceof con patrón (Java 16) | if (o instanceof String s) | Comprobar y convertir en un paso |
switch con patrones (Java 21) | case Circulo c -> ... | Elegir según el tipo del objeto |
Guarda y null | case Integer n when n < 0 ->, case null -> | Condición adicional en un case; tratar null |
| Record pattern (Java 21) | case Rectangulo(double b, double h) -> | Desestructurar un record |
| Variable sin nombre (Java 22) | case Linea(Punto p, _), catch (Exception _) | Ignorar componentes o variables que no se usan |
| Exhaustividad | switch sobre tipo sellado sin default | El compilador avisa si falta un caso |