Home / Lenguajes / Enums en Java: guía completa con ejemplos, switch, EnumSet y más
Enums en Java: guía completa con ejemplos, switch, EnumSet y más

Enums en Java: guía completa con ejemplos, switch, EnumSet y más

19 October 2026 · 18 min de lectura ··

Por qué los enums merecen más atención de la que reciben

Casi todas las aplicaciones manejan conjuntos cerrados de valores: los estados de un pedido, los roles de un usuario, los días de la semana, los niveles de un registro o los métodos de pago aceptados. Durante años, muchos programas representaron esos valores con constantes numéricas o cadenas de texto, una solución que funciona hasta que alguien escribe "enviadoo" con una letra de más o pasa un 7 donde solo se esperaban valores del 1 al 5.

Java resolvió ese problema desde la versión 5 con los enums, o enumeraciones, un tipo especial que define un conjunto fijo de constantes con nombre. Lo que mucha gente desconoce es que los enums de Java van mucho más allá de una simple lista de nombres. Son clases completas: pueden tener campos, constructores, métodos, implementar interfaces e incluso comportarse de forma distinta en cada constante.

En este artículo vamos a recorrer las enumeraciones de Java desde lo básico hasta los patrones avanzados. Veremos cómo declararlas, qué métodos ofrecen de serie, cómo usarlas con las expresiones switch modernas, cómo añadirles datos y comportamiento, qué colecciones especializadas existen para ellas y cómo se comparan con alternativas recientes como las interfaces selladas. Todos los ejemplos funcionan con Java 25, la versión de soporte a largo plazo más reciente, y con JDK 27, la última versión publicada de la plataforma.

El problema que resuelven los enums

Para apreciar su valor, conviene ver primero el código que sustituyen. Imagina un sistema de pedidos que representa los estados con constantes enteras:

public class Pedido {
    public static final int PENDIENTE = 0;
    public static final int PAGADO = 1;
    public static final int ENVIADO = 2;

    private int estado;

    public void cambiarEstado(int nuevoEstado) {
        this.estado = nuevoEstado;
    }
}

Este enfoque tiene varios problemas graves. El compilador acepta cualquier entero, así que pedido.cambiarEstado(42) compila sin errores. Al imprimir el estado solo se ve un número, sin significado para quien lee los registros. Nada impide comparar un estado de pedido con una constante de otra clase que casualmente valga lo mismo. Y si alguien añade un estado nuevo, no hay forma de saber qué partes del código deberían tratarlo.

Con un enum, el compilador se convierte en un aliado:

public enum EstadoPedido {
    PENDIENTE, PAGADO, ENVIADO, ENTREGADO, CANCELADO
}

public class Pedido {
    private EstadoPedido estado = EstadoPedido.PENDIENTE;

    public void cambiarEstado(EstadoPedido nuevoEstado) {
        this.estado = nuevoEstado;
    }
}

Ahora solo se pueden pasar valores válidos, los registros muestran nombres legibles y las herramientas del IDE pueden listar todos los estados posibles. Esta es la esencia de los tipos seguros en Java: convertir errores que antes aparecían en producción en errores de compilación.

Cómo declarar un enum

La sintaxis básica es muy parecida a la de una clase, pero con la palabra clave enum y una lista de constantes separadas por comas:

public enum NivelLog {
    TRACE, DEBUG, INFO, WARN, ERROR
}

Por convención, las constantes se escriben en mayúsculas, con palabras separadas por guiones bajos si es necesario, como TARJETA_CREDITO. Técnicamente el compilador acepta minúsculas, pero romper la convención dificulta la lectura y confunde a otros desarrolladores.

Un enum puede declararse en su propio archivo, como cualquier clase pública, o anidado dentro de otra clase cuando solo tiene sentido en ese contexto:

public class Notificacion {

    public enum Canal { EMAIL, SMS, PUSH }

    private final Canal canal;
    private final String mensaje;

    public Notificacion(Canal canal, String mensaje) {
        this.canal = canal;
        this.mensaje = mensaje;
    }
}

Los enums anidados son implícitamente estáticos, por lo que se accede a ellos con el nombre de la clase contenedora: Notificacion.Canal.SMS.

Para usar una constante, se asigna a una variable del tipo del enum mediante la notación de punto:

NivelLog nivel = NivelLog.WARN;
System.out.println(nivel);
WARN

Una estructura de proyecto típica sitúa los enums de dominio junto a las entidades que los usan:

src/main/java/com/tienda/
├── pedidos/
│   ├── Pedido.java
│   ├── EstadoPedido.java
│   └── ServicioPedidos.java
├── pagos/
│   ├── MetodoPago.java
│   └── ProcesadorPagos.java
└── comun/
    └── NivelLog.java

Métodos que todo enum incluye de serie

Todos los enums heredan implícitamente de la clase java.lang.Enum, que les proporciona varios métodos útiles sin escribir una sola línea.

El método name() devuelve el nombre exacto de la constante tal como se declaró, y toString() hace lo mismo por defecto, aunque puede sobrescribirse. El método ordinal() devuelve la posición de la constante en la declaración, empezando por cero:

EstadoPedido estado = EstadoPedido.ENVIADO;

System.out.println(estado.name());
System.out.println(estado.ordinal());
ENVIADO
2

El compilador añade además dos métodos estáticos a cada enum. Con los métodos values y valueOf puedes recorrer todas las constantes o convertir una cadena en la constante correspondiente:

for (EstadoPedido e : EstadoPedido.values()) {
    System.out.print(e + " ");
}
System.out.println();

EstadoPedido desdeTexto = EstadoPedido.valueOf("PAGADO");
System.out.println(desdeTexto);
PENDIENTE PAGADO ENVIADO ENTREGADO CANCELADO
PAGADO

El método values() devuelve un arreglo nuevo en cada llamada, con las constantes en el orden de declaración. El método valueOf() es estricto: distingue mayúsculas y minúsculas, y si el texto no coincide exactamente con ninguna constante, lanza una excepción:

Exception in thread "main" java.lang.IllegalArgumentException: No enum constant com.tienda.pedidos.EstadoPedido.pagado

Los enums también implementan Comparable, de modo que compareTo compara las constantes según su orden de declaración, y son Serializable de forma segura, como veremos más adelante.

Comparar enums: siempre con el operador de igualdad

Como cada constante de un enum es una única instancia en toda la aplicación, los enums pueden compararse con == en lugar de equals(). Es la forma recomendada por dos razones:

EstadoPedido estado = null;

// Seguro: devuelve false sin lanzar excepciones
if (estado == EstadoPedido.ENVIADO) { /* ... */ }

// Peligroso: lanza NullPointerException si estado es null
if (estado.equals(EstadoPedido.ENVIADO)) { /* ... */ }

La primera es la seguridad frente a valores nulos, como se ve en el ejemplo. La segunda es que == se verifica en tiempo de compilación: si por error comparas un enum con un valor de otro tipo incompatible, el compilador lo detecta, mientras que equals() simplemente devolvería false.

Enums y switch: del clásico a las expresiones modernas

Uno de los usos más naturales de un enum es decidir qué hacer según su valor. La sentencia switch clásica funciona con enums desde su introducción, y dentro de cada case se escribe directamente el nombre de la constante, sin prefijo:

switch (estado) {
    case PENDIENTE:
        System.out.println("Esperando el pago");
        break;
    case ENVIADO:
        System.out.println("En camino");
        break;
    default:
        System.out.println("Otro estado");
}

Este estilo es propenso a errores: olvidar un break provoca que la ejecución continúe en el siguiente caso, y nada obliga a tratar todos los valores.

Las expresiones switch con enum, estables desde Java 14, resuelven ambos problemas. Usan flechas en lugar de dos puntos, no tienen caída entre casos, permiten agrupar varias constantes en un mismo caso y, sobre todo, devuelven un valor:

String mensaje = switch (estado) {
    case PENDIENTE, PAGADO -> "Preparando tu pedido";
    case ENVIADO -> "En camino";
    case ENTREGADO -> "Entregado";
    case CANCELADO -> "Cancelado";
};
System.out.println(mensaje);
En camino

Hay un detalle muy valioso en este ejemplo: no hay rama default. Cuando una expresión switch cubre todas las constantes de un enum, el compilador lo reconoce como exhaustiva. Si mañana alguien añade un estado DEVUELTO al enum, todas las expresiones switch que no lo traten dejarán de compilar, señalando exactamente los lugares que hay que revisar:

Pedido.java:24: error: the switch expression does not cover all possible input values
        String mensaje = switch (estado) {
                         ^

Por esa razón, en las expresiones switch sobre enums conviene evitar la rama default siempre que sea posible: es precisamente su ausencia la que activa la comprobación de exhaustividad.

Si un caso necesita varias instrucciones, se usa un bloque con yield para devolver el valor:

int diasEstimados = switch (estado) {
    case PENDIENTE, PAGADO -> {
        registrar("Pedido aún no enviado");
        yield 5;
    }
    case ENVIADO -> 2;
    case ENTREGADO, CANCELADO -> 0;
};

Enums con campos, constructores y métodos

Aquí es donde los enums de Java se distinguen de los de muchos otros lenguajes. Cada constante es una instancia de la clase del enum, y esa clase puede tener campos y métodos como cualquier otra. Un enum con campos y constructores permite asociar datos a cada valor:

public enum MetodoPago {
    TARJETA("Tarjeta de crédito", 0.015),
    TRANSFERENCIA("Transferencia bancaria", 0.0),
    PAYPAL("PayPal", 0.029),
    CONTRAREEMBOLSO("Contra reembolso", 0.02);

    private final String descripcion;
    private final double comision;

    MetodoPago(String descripcion, double comision) {
        this.descripcion = descripcion;
        this.comision = comision;
    }

    public String descripcion() {
        return descripcion;
    }

    public double calcularComision(double importe) {
        return importe * comision;
    }
}
for (MetodoPago m : MetodoPago.values()) {
    System.out.printf("%-25s %.2f €%n", m.descripcion(), m.calcularComision(200));
}
Tarjeta de crédito        3.00 €
Transferencia bancaria    0.00 €
PayPal                    5.80 €
Contra reembolso          4.00 €

Hay algunas reglas a tener en cuenta. La lista de constantes debe aparecer al principio del cuerpo, terminada en punto y coma si después hay más miembros. El constructor es implícitamente privado: no se puede invocar con new desde fuera, porque el conjunto de instancias es fijo. Y los campos deberían ser final, ya que las constantes son compartidas por toda la aplicación y modificar su estado provocaría efectos difíciles de rastrear.

Un ejemplo clásico que muestra el potencial de esta técnica es un enum de planetas que calcula la gravedad en su superficie a partir de su masa y su radio:

public enum Planeta {
    MERCURIO(3.303e+23, 2.4397e6),
    TIERRA(5.976e+24, 6.37814e6),
    JUPITER(1.9e+27, 7.1492e7);

    private static final double G = 6.67300E-11;
    private final double masa;
    private final double radio;

    Planeta(double masa, double radio) {
        this.masa = masa;
        this.radio = radio;
    }

    public double gravedad() {
        return G * masa / (radio * radio);
    }
}
MERCURIO: 3.70 m/s²
TIERRA: 9.80 m/s²
JUPITER: 24.81 m/s²

Buscar constantes por un atributo

Un caso muy común es tener que convertir un código externo, por ejemplo el que llega en una API o se guarda en una base de datos, en la constante correspondiente. En lugar de recorrer values() en cada búsqueda, lo eficiente es construir un mapa estático una sola vez:

import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

public enum Pais {
    ESPANA("ES"), MEXICO("MX"), ARGENTINA("AR"), COLOMBIA("CO");

    private static final Map<String, Pais> POR_CODIGO =
        Arrays.stream(values()).collect(Collectors.toUnmodifiableMap(Pais::codigo, Function.identity()));

    private final String codigo;

    Pais(String codigo) {
        this.codigo = codigo;
    }

    public String codigo() {
        return codigo;
    }

    public static Optional<Pais> desdeCodigo(String codigo) {
        return Optional.ofNullable(POR_CODIGO.get(codigo.toUpperCase()));
    }
}
System.out.println(Pais.desdeCodigo("mx"));
System.out.println(Pais.desdeCodigo("FR"));
Optional[MEXICO]
Optional.empty

Devolver un Optional en lugar de lanzar una excepción permite que quien llama decida cómo tratar los códigos desconocidos, algo muy útil al procesar datos de fuentes externas.

Comportamiento distinto en cada constante

Los enums permiten ir todavía más lejos: cada constante puede tener su propio cuerpo de clase y sobrescribir métodos. Combinado con interfaces, esto permite implementar el patrón estrategia de forma muy compacta:

import java.util.function.DoubleBinaryOperator;

public enum Operacion implements DoubleBinaryOperator {
    SUMA("+") {
        public double applyAsDouble(double a, double b) { return a + b; }
    },
    RESTA("-") {
        public double applyAsDouble(double a, double b) { return a - b; }
    },
    MULTIPLICACION("*") {
        public double applyAsDouble(double a, double b) { return a * b; }
    },
    DIVISION("/") {
        public double applyAsDouble(double a, double b) { return a / b; }
    };

    private final String simbolo;

    Operacion(String simbolo) {
        this.simbolo = simbolo;
    }

    public String simbolo() {
        return simbolo;
    }
}
for (Operacion op : Operacion.values()) {
    System.out.printf("8 %s 2 = %.1f%n", op.simbolo(), op.applyAsDouble(8, 2));
}
8 + 2 = 10.0
8 - 2 = 6.0
8 * 2 = 16.0
8 / 2 = 4.0

Como el enum implementa DoubleBinaryOperator, sus constantes pueden pasarse directamente a cualquier código que espere esa interfaz funcional. Otra forma igualmente válida, a menudo más concisa, es pasar una lambda al constructor y almacenarla en un campo:

public enum Descuento {
    NINGUNO(precio -> precio),
    ESTUDIANTE(precio -> precio * 0.85),
    BLACK_FRIDAY(precio -> precio * 0.6);

    private final java.util.function.DoubleUnaryOperator regla;

    Descuento(java.util.function.DoubleUnaryOperator regla) {
        this.regla = regla;
    }

    public double aplicar(double precio) {
        return regla.applyAsDouble(precio);
    }
}

Máquinas de estados con enums

Los estados de un pedido no solo son una lista: también definen qué transiciones son válidas. Un pedido entregado no puede volver a pendiente, y uno cancelado no puede enviarse. Los enums permiten modelar esas reglas dentro del propio tipo:

import java.util.EnumSet;
import java.util.Set;

public enum EstadoPedido {
    PENDIENTE, PAGADO, ENVIADO, ENTREGADO, CANCELADO;

    public Set<EstadoPedido> siguientesPermitidos() {
        return switch (this) {
            case PENDIENTE -> EnumSet.of(PAGADO, CANCELADO);
            case PAGADO -> EnumSet.of(ENVIADO, CANCELADO);
            case ENVIADO -> EnumSet.of(ENTREGADO);
            case ENTREGADO, CANCELADO -> EnumSet.noneOf(EstadoPedido.class);
        };
    }

    public EstadoPedido transicionarA(EstadoPedido destino) {
        if (!siguientesPermitidos().contains(destino)) {
            throw new IllegalStateException("No se puede pasar de " + this + " a " + destino);
        }
        return destino;
    }
}
EstadoPedido estado = EstadoPedido.PENDIENTE;
estado = estado.transicionarA(EstadoPedido.PAGADO);
estado = estado.transicionarA(EstadoPedido.PENDIENTE);
Exception in thread "main" java.lang.IllegalStateException: No se puede pasar de PAGADO a PENDIENTE

Concentrar las reglas en el enum evita que la lógica de transiciones se disperse por servicios y controladores, y gracias al switch exhaustivo, añadir un estado nuevo obliga a definir sus transiciones.

EnumSet y EnumMap: colecciones optimizadas

El JDK incluye dos colecciones diseñadas específicamente para enums, mucho más eficientes que sus equivalentes generales. Conocer EnumSet y EnumMap es una de esas mejoras sencillas que elevan la calidad del código.

EnumSet representa un conjunto de constantes de un mismo enum. Internamente usa un vector de bits, de modo que operaciones como añadir, eliminar o comprobar pertenencia son extremadamente rápidas y ocupan muy poca memoria. Ofrece métodos de fábrica muy expresivos:

import java.util.EnumSet;

enum Dia { LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO }

EnumSet<Dia> laborables = EnumSet.range(Dia.LUNES, Dia.VIERNES);
EnumSet<Dia> finDeSemana = EnumSet.complementOf(laborables);
EnumSet<Dia> reuniones = EnumSet.of(Dia.LUNES, Dia.JUEVES);

System.out.println(laborables);
System.out.println(finDeSemana);
System.out.println(reuniones.contains(Dia.MARTES));
[LUNES, MARTES, MIERCOLES, JUEVES, VIERNES]
[SABADO, DOMINGO]
false

EnumSet sustituye con ventaja a los antiguos campos de bits basados en enteros, como los permisos combinados con operadores |, ofreciendo la misma eficiencia con legibilidad y seguridad de tipos.

EnumMap es un mapa cuyas claves son constantes de un enum. Internamente es un arreglo indexado por el ordinal, así que es más rápido que un HashMap y mantiene las claves en el orden de declaración:

import java.util.EnumMap;
import java.util.List;
import java.util.Map;

List<EstadoPedido> estados = List.of(
    EstadoPedido.PAGADO, EstadoPedido.ENVIADO, EstadoPedido.PAGADO, EstadoPedido.PENDIENTE);

Map<EstadoPedido, Integer> conteo = new EnumMap<>(EstadoPedido.class);
for (EstadoPedido e : estados) {
    conteo.merge(e, 1, Integer::sum);
}
System.out.println(conteo);
{PENDIENTE=1, PAGADO=2, ENVIADO=1}

Con streams, el mismo resultado se obtiene indicando EnumMap como implementación del mapa en el colector:

Map<EstadoPedido, Long> resumen = estados.stream()
    .collect(Collectors.groupingBy(e -> e, () -> new EnumMap<>(EstadoPedido.class), Collectors.counting()));

El patrón singleton con un enum

Un enum con una única constante es, según muchos expertos, la mejor forma de implementar un singleton en Java. El patrón singleton con enum garantiza una sola instancia incluso frente a la serialización y a los ataques por reflexión, dos puntos débiles de las implementaciones tradicionales:

public enum Configuracion {
    INSTANCIA;

    private final java.util.Properties propiedades = new java.util.Properties();

    Configuracion() {
        try (var in = getClass().getResourceAsStream("/app.properties")) {
            if (in != null) propiedades.load(in);
        } catch (java.io.IOException e) {
            throw new java.io.UncheckedIOException(e);
        }
    }

    public String obtener(String clave, String porDefecto) {
        return propiedades.getProperty(clave, porDefecto);
    }
}
String url = Configuracion.INSTANCIA.obtener("api.url", "http://localhost:8080");

La JVM crea la instancia de forma perezosa y segura entre hilos la primera vez que se accede al enum. Aun así, en aplicaciones que usan frameworks de inyección de dependencias como Spring, suele ser preferible dejar que el contenedor gestione los objetos únicos, porque facilita las pruebas y la configuración por entorno.

Persistencia y serialización: el peligro del ordinal

Cuando un enum sale de la memoria de la aplicación, hacia una base de datos, un archivo JSON o un mensaje en una cola, aparece un riesgo importante: depender del ordinal.

El ordinal cambia en cuanto alguien reordena las constantes o inserta una nueva en medio. Si una base de datos almacena estados como números y un desarrollador añade EN_PREPARACION entre PAGADO y ENVIADO, todos los pedidos enviados pasarán a interpretarse como en preparación. Por eso la regla es clara: nunca persistas ni transmitas el ordinal.

En JPA, la anotación @Enumerated usa el ordinal por defecto, de modo que conviene indicar siempre que se guarde el nombre:

import jakarta.persistence.*;

@Entity
public class PedidoEntidad {

    @Id
    @GeneratedValue
    private Long id;

    @Enumerated(EnumType.STRING)
    @Column(length = 20, nullable = false)
    private EstadoPedido estado;
}

Si necesitas un código estable distinto del nombre, por ejemplo porque el nombre de la constante podría cambiar, define un campo propio y un AttributeConverter que traduzca entre ese código y la constante.

Con JSON ocurre algo similar. Librerías como Jackson serializan los enums por su nombre de forma predeterminada. Si el formato externo usa otros valores, la anotación @JsonValue en un método del enum indica qué valor emitir:

import com.fasterxml.jackson.annotation.JsonValue;

public enum Prioridad {
    BAJA("low"), MEDIA("medium"), ALTA("high");

    private final String valorApi;

    Prioridad(String valorApi) {
        this.valorApi = valorApi;
    }

    @JsonValue
    public String valorApi() {
        return valorApi;
    }
}

Para la serialización nativa de Java, los enums son seguros por diseño: se serializan por nombre y, al deserializarlos, la JVM devuelve siempre la instancia existente en lugar de crear una nueva.

Enums frente a interfaces selladas y records

Las versiones modernas de Java han incorporado herramientas que a veces compiten con los enums, y conviene saber cuándo elegir cada una.

Los enums son ideales cuando el conjunto de valores es fijo y todos comparten la misma estructura de datos. Si cada estado de un pedido solo necesita un nombre, una descripción y unas reglas, un enum es la opción más simple.

Las interfaces selladas, combinadas con records, encajan mejor cuando cada caso necesita datos diferentes. Por ejemplo, un resultado de pago puede ser aprobado, con un identificador de transacción; rechazado, con un motivo; o pendiente, con una URL de verificación. Un enum no puede asociar datos distintos a cada instancia en tiempo de ejecución, pero una jerarquía sellada sí:

public sealed interface ResultadoPago permits Aprobado, Rechazado, Pendiente {}

public record Aprobado(String transaccionId) implements ResultadoPago {}
public record Rechazado(String motivo) implements ResultadoPago {}
public record Pendiente(String urlVerificacion) implements ResultadoPago {}
String texto = switch (resultado) {
    case Aprobado a -> "Pago aprobado: " + a.transaccionId();
    case Rechazado r -> "Rechazado: " + r.motivo();
    case Pendiente p -> "Verifica en " + p.urlVerificacion();
};

Igual que con los enums, el compilador comprueba que el switch cubra todos los casos permitidos. La regla práctica es sencilla: si los valores son constantes con datos fijos, usa un enum; si cada caso transporta datos propios en tiempo de ejecución, usa una interfaz sellada con records.

Errores comunes y cómo evitarlos

Hay una serie de errores que se repiten al trabajar con enums.

El primero es usar valueOf con datos externos sin protección. Cualquier texto inesperado, una minúscula o un espacio provoca una IllegalArgumentException. Normaliza la entrada y captura la excepción, o mejor aún, ofrece un método de búsqueda que devuelva un Optional, como vimos con los países.

El segundo es depender del ordinal para la lógica de negocio o la persistencia. Si necesitas un orden o un código, defínelo como un campo explícito.

El tercero es añadir una rama default en las expresiones switch exhaustivas. Parece una medida de seguridad, pero desactiva la comprobación del compilador que avisa cuando se añade una nueva constante.

El cuarto es crear campos mutables en un enum. Como las constantes son compartidas por toda la aplicación y por todos los hilos, modificar su estado introduce errores de concurrencia y comportamientos sorprendentes.

El quinto es llamar a values() en bucles muy frecuentes. Cada llamada crea un arreglo nuevo; si lo necesitas a menudo, guárdalo en una constante privada o usa EnumSet.allOf().

El sexto aparece al acceder a campos estáticos desde el constructor del enum. Las constantes se crean antes que el resto de campos estáticos, por lo que intentar usarlos desde el constructor produce un error de compilación o valores nulos:

error: illegal reference to static field from initializer

La solución es inicializar estructuras como los mapas de búsqueda en un campo estático declarado después de las constantes, como hicimos en el ejemplo de Pais, o en un bloque static.

Buenas prácticas para diseñar enums

Para cerrar, estas pautas ayudan a sacar el máximo partido a las enumeraciones:

  • Usa enums para cualquier conjunto cerrado de valores en lugar de constantes enteras o cadenas.
  • Compara siempre con == y aprovecha las expresiones switch exhaustivas sin rama default.
  • Coloca en el enum los datos y el comportamiento que pertenecen a cada valor, en lugar de dispersarlos en sentencias condicionales.
  • Mantén los campos final y el estado inmutable.
  • Persiste y transmite los enums por nombre o por un código explícito, nunca por ordinal.
  • Usa EnumSet y EnumMap cuando trabajes con conjuntos o mapas de constantes.
  • Valora una interfaz sellada con records cuando cada caso necesite datos propios.

Conclusiones

Los enums de Java son mucho más que una lista de nombres. Proporcionan seguridad de tipos, eliminando los errores asociados a las constantes numéricas o de texto, y convierten al compilador en un aliado que detecta valores inválidos antes de que el código llegue a producción.

Su potencia real aparece cuando se aprovechan como lo que son: clases con un número fijo de instancias. Pueden tener campos, constructores y métodos, implementar interfaces y definir comportamiento distinto en cada constante, lo que permite modelar estrategias, reglas de negocio y máquinas de estados de forma compacta y expresiva.

Las expresiones switch modernas, con su comprobación de exhaustividad, y las colecciones especializadas EnumSet y EnumMap completan un conjunto de herramientas que hace que trabajar con enums sea seguro y eficiente. A ello se suma la posibilidad de implementar el patrón singleton de la forma más robusta que ofrece el lenguaje.

Por último, conviene recordar sus límites: no dependas del ordinal al persistir o transmitir datos, mantén las constantes inmutables y recurre a las interfaces selladas con records cuando cada caso necesite datos propios. Con estas pautas, los enums se convierten en una de las herramientas más útiles y subestimadas del lenguaje.