Saltar a contenido

3. Selección: switch expression

UD3 — Control de Flujo y Depuración · RA3 — Escribe y depura código, analizando y utilizando las estructuras de control del lenguaje

IES Thiar · Curso 2026 - 2027

Aspecto Valor
Resultado de aprendizaje RA: 3 — Escribe y depura código, analizando y utilizando las estructuras de control del lenguaje.
Criterios de evaluación a los que contribuye esta sección CE 3.1 (escribe y prueba código que hace uso de estructuras de selección)
CE 3.5 (crea programas ejecutables combinando selección con lectura de datos)
CE 3.7 (comenta y documenta el código).
Marco normativo RD 405/2023 · RD 450/2010 · RD 659/2023
Tecnología base Java 25 LTS (OpenJDK Temurin 25) — soporte hasta 2030+
Horas estimadas ~5 h (de las 26 h de la UD3).

Requisitos previos (lo que ya sabes): condiciones booleanas, operadores relacionales y lógicos, paréntesis, De Morgan y variables booleanas (sección 1 de UD3); sintaxis completa de if / else if / else, early return, trampas del if y validación con Scanner (sección 2 de UD3); tipos primitivos, var, String (con .equals(), .equalsIgnoreCase(), .length() y .matches()), text blocks, final para constantes, operador ternario y Integer.parseInt / Double.parseDouble (de UD1); introducción básica al concepto de enum como conjunto de constantes nombradas (de UD1, sección 6).

Lo que NO verás aquí (se reserva para secciones posteriores): bucles while / do-while / for (secciones 4–6), break / continue como sentencias de salto de bucle (sección 7), try-catch (sección 8), excepciones propias (sección 9), depurador de IntelliJ (sección 10), assert (sección 11), pattern matching con instanceof completo (sección 12). Tampoco verás la definición completa de enum (propiedades, constructores, métodos) ni la definición de sealed classes: ambas se cubren en UD4; aquí las usaremos como "cajas negras" reconocibles. Tampoco aparece nada de POO (UD2) ni de colecciones (UD6): seguimos trabajando con main y variables locales.


3.1 ¿De qué va esta sección? — Cuando el if se queda corto

En la sección 2 aprendiste a escribir if / else if / else y a validar entradas. Esa es la herramienta de selección más universal y la que más vas a usar. Pero hay un patrón que, escrito con if, queda verboso y repetitivo: cuando comparas el mismo valor contra muchos literales.

Acuérdate del ejemplo de los días de la semana que vimos al final de la sección 2:

if (dia == 1) {
    IO.println("Lunes");
} else if (dia == 2) {
    IO.println("Martes");
} else if (dia == 3) {
    IO.println("Miércoles");
} else if (dia == 4) {
    IO.println("Jueves");
} else if (dia == 5) {
    IO.println("Viernes");
} else if (dia == 6) {
    IO.println("Sábado");
} else if (dia == 7) {
    IO.println("Domingo");
} else {
    IO.println("Día no válido.");
}

Funciona, sí. Pero hay siete else if comparando dia contra un número. Cada rama repite la palabra dia ==, las llaves y el IO.println. Si tuvieras 20 casos (por ejemplo, un menú con 20 opciones), el código sería un ladrillo ilegible.

Para este patrón existe una herramienta mucho más elegante: el switch. En Java 25, el switch moderno (la switch expression estabilizada en Java 14 y ampliada con pattern matching en Java 21) es la forma idiomática de elegir entre muchos valores. Es más conciso, más seguro (no hay fall-through accidental) y más expresivo (puede devolver un valor, como el operador ternario).

En esta sección vas a aprender a usarlo bien. Cubriremos, en este orden:

  1. El switch clásico con case / break / default (legado, pero lo vas a encontrar en código antiguo).
  2. El switch expression con flecha ->, la forma moderna y recomendada.
  3. El switch como expresión (no sentencia): var r = switch(...) { ... };.
  4. Múltiples etiquetas en un case: case 1, 2, 3 -> ....
  5. switch con String.
  6. switch con enum (introducción; la definición completa de enums se ve en UD4).
  7. switch con tipos (Java 21+): pattern matching en switch.
  8. Guards con when (Java 21+): case Integer i when i > 0 -> ....
  9. Exhaustividad y por qué el compilador te obliga a poner default o a cubrir todos los casos.
  10. Cuándo switch y cuándo if: criterios de decisión.
  11. Trampas clásicas del switch.
  12. Ejemplos resueltos paso a paso.

Analogía del ascensor vs el portero

Imagina que quieres saber a qué planta va cada botón de un ascensor. Con if, sería como si un portero te preguntara: "¿Es el botón 1? No. ¿Es el 2? No. ¿Es el 3? Sí, pues al tercero". Con switch, es como un panel de botones: pulsas el 3 y la lógica salta directamente a esa planta, sin pasar por las demás. Mucho más rápido y más claro.

Vamos a empezar por el switch clásico, porque lo vas a encontrar en muchísimo código heredado (en proyectos open source, en libros antiguos, en tu futuro trabajo). Después pasaremos al moderno, que es el que escribirás tú.


3.2 El switch clásico con case / break / default

3.2.1 Sintaxis del switch clásico

El switch clásico (también llamado switch statement) lleva en Java desde la versión 1.0 (1996). Su sintaxis:

switch (selector) {
    case valor1:
        // Bloque 1
        break;
    case valor2:
        // Bloque 2
        break;
    case valor3:
        // Bloque 3
        break;
    default:
        // Bloque por defecto (opcional pero recomendado)
        break;
}

Partes:

  • selector: la variable o expresión cuyo valor se compara. Tiene que ser de tipo byte, short, int, char, String o enum. No admite long, float, double ni boolean.
  • case valor:: cada etiqueta compara el selector contra valor. Si coincide, se ejecuta el bloque a partir de ahí.
  • break;: al final de cada bloque, sale del switch. Si te lo olvidas, se ejecuta también el siguiente case (lo que se llama fall-through).
  • default:: se ejecuta si ningún case coincide. Es opcional, pero casi siempre se pone.

3.2.2 Ejemplo: día de la semana (versión clásica)

// Switch clásico: día de la semana
int dia = 3;

switch (dia) {
    case 1:
        IO.println("Lunes");
        break;
    case 2:
        IO.println("Martes");
        break;
    case 3:
        IO.println("Miércoles");
        break;
    case 4:
        IO.println("Jueves");
        break;
    case 5:
        IO.println("Viernes");
        break;
    case 6:
        IO.println("Sábado");
        break;
    case 7:
        IO.println("Domingo");
        break;
    default:
        IO.println("Día no válido (debe ser 1-7).");
        break;
}

Salida con dia = 3:

Miércoles

3.2.3 La trampa del fall-through

Aquí está la trampa más famosa del switch clásico. Mira este código sin break:

int dia = 3;

switch (dia) {
    case 1:
        IO.println("Lunes");
    case 2:
        IO.println("Martes");
    case 3:
        IO.println("Miércoles");
    case 4:
        IO.println("Jueves");
    case 5:
        IO.println("Viernes");
    default:
        IO.println("Día no válido");
}

Si dia = 3, la salida es:

1
2
3
4
Miércoles
Jueves
Viernes
Día no válido

¿Qué ha pasado? Sin break, Java entra en el case 3 y sigue ejecutando todos los casos siguientes hasta encontrar un break o el final del switch. Esto se llama fall-through ("caer a través de").

A veces el fall-through se usa intencionadamente para agrupar casos:

// Fall-through intencionado: días laborables vs fin de semana
switch (dia) {
    case 1:  // Lunes
    case 2:  // Martes
    case 3:  // Miércoles
    case 4:  // Jueves
    case 5:  // Viernes
        IO.println("Día laborable");
        break;
    case 6:  // Sábado
    case 7:  // Domingo
        IO.println("Fin de semana");
        break;
    default:
        IO.println("Día no válido");
}

Aquí los case 1 a case 5 están vacíos y "caen" hasta el bloque de "Día laborable". Funciona, pero es frágil: si un compañero añade un case 4 con su propio break sin darse cuenta de que forma parte de un grupo, se rompe la lógica.

Por eso, en Java moderno usamos la sintaxis con flecha (la verás en la sección 3.3), donde el fall-through no existe.

3.2.4 Restricciones del selector

El selector del switch clásico solo admite estos tipos:

Tipo Admite
byte, short, int
char
String Sí (desde Java 7)
enum Sí (desde Java 5)
long, float, double No
boolean No
Objetos arbitrarios Solo con pattern matching (Java 21+, lo verás en sección 3.8)

¿Por qué no boolean? Porque un boolean solo puede ser true o false, y para eso ya tienes if/else. Un switch para dos valores no aporta nada.

¿Por qué no long, float, double? Por decisiones históricas de diseño: el switch se diseñó para tipos "pequeños" (caben en un int), y los de coma flotante tienen problemas de precisión al comparar.

3.2.5 Diagrama de flujo del switch clásico

flowchart TD
    Inicio([Inicio]) --> Eval["Evalúa el selector"]
    Eval --> C1{"¿selector == valor1?"}
    C1 -->|Sí| B1["Bloque 1 + break"]
    C1 -->|No| C2{"¿selector == valor2?"}
    C2 -->|Sí| B2["Bloque 2 + break"]
    C2 -->|No| C3{"¿selector == valor3?"}
    C3 -->|Sí| B3["Bloque 3 + break"]
    C3 -->|No| Def["Bloque default"]
    B1 --> Fin([Fin])
    B2 --> Fin
    B3 --> Fin
    Def --> Fin

A diferencia del if/else if, en el switch clásico cada case es una etiqueta, no una condición. No hay que escribir selector == valor1, solo case valor1:. Java hace la comparación por ti.

3.2.6 Por qué lo seguimos viendo

Aunque en este curso escribiremos siempre la forma moderna (sección 3.3), el switch clásico sigue siendo código Java válido y lo vas a encontrar en:

  • Libros antiguos y tutoriales de antes de 2020.
  • Proyectos empresariales escritos en Java 7 u 8.
  • Código generado automáticamente por herramientas como Lombok o IDEs.
  • Stack Overflow y blogs de programación.

Por eso es importante que lo reconozcas al leerlo, aunque no lo escribas. Y, si un día lo heredas, sepas refactorizarlo a la forma moderna.


3.3 Switch expression con flecha (->)

3.3.1 La sintaxis moderna

Desde Java 14 (estabilizada), existe una nueva sintaxis de switch con flecha -> que soluciona todos los problemas del clásico:

switch (selector) {
    case valor1 -> {
        // Bloque 1 (varias sentencias, entre llaves)
    }
    case valor2 -> {
        // Bloque 2
    }
    case valor3 -> unaSolaSentencia;   // si es una sola sentencia, sin llaves
    default -> {
        // Bloque por defecto
    }
}

Diferencias clave con el clásico:

Característica Clásico (:) Moderno (->)
Separador case valor: case valor ->
Fall-through Sí, si olvidas break. No existe: cada rama es exclusiva.
break obligatorio Sí, para salir. No, no se usa.
Llaves { } Obligatorias siempre. Solo si el bloque tiene varias sentencias.
Puede producir un valor No (es sentencia). Sí (es expresión). Lo verás en 3.4.

3.3.2 Ejemplo: día de la semana (versión moderna)

Reescribamos el ejemplo de antes con la sintaxis moderna:

int dia = 3;

switch (dia) {
    case 1 -> IO.println("Lunes");
    case 2 -> IO.println("Martes");
    case 3 -> IO.println("Miércoles");
    case 4 -> IO.println("Jueves");
    case 5 -> IO.println("Viernes");
    case 6 -> IO.println("Sábado");
    case 7 -> IO.println("Domingo");
    default -> IO.println("Día no válido (debe ser 1-7).");
}

Fíjate:

  • No hay break. Cada rama es exclusiva: cuando se ejecuta case 3 -> IO.println("Miércoles");, se sale del switch automáticamente.
  • No hay llaves cuando el bloque es una sola sentencia. Si necesitas varias sentencias, las pones entre { } (lo verás en 3.3.4).
  • default -> sin break ni :.
  • El código es mucho más limpio que la versión clásica.

3.3.3 El fall-through ya no existe

En la sintaxis con flecha, no hay fall-through. Cada rama ejecuta una sola cosa y sale. Esto elimina de raíz el bug más común del switch clásico (olvidar el break).

Si quieres agrupar varios valores en una misma rama, usas múltiples etiquetas (sección 3.5), no fall-through:

1
2
3
4
5
6
// Agrupar con múltiples etiquetas (sintaxis moderna)
switch (dia) {
    case 1, 2, 3, 4, 5 -> IO.println("Día laborable");
    case 6, 7 -> IO.println("Fin de semana");
    default -> IO.println("Día no válido");
}

Esto es inequívoco: los cinco primeros valores van a "Día laborable", y es imposible que un compañero rompa la lógica añadiendo un case 4 con su propio bloque.

3.3.4 Bloques con varias sentencias

Si una rama necesita varias sentencias, usas llaves:

int dia = 3;

switch (dia) {
    case 1 -> {
        String msg = "Lunes";
        IO.println("Hoy es " + msg);
        IO.println("Ánimo con la semana.");
    }
    case 6 -> {
        String msg = "Sábado";
        IO.println("Hoy es " + msg);
        IO.println("Disfruta el fin de semana.");
    }
    default -> IO.println("Otro día.");
}

Dentro del bloque { } puedes declarar variables, llamar a métodos, hacer lo que necesites. Es un mini-método en sí mismo.

3.3.5 Convenciones de estilo

IntelliJ IDEA te formatea el switch automáticamente con Ctrl+Alt+L. Las convenciones que seguimos en este curso:

  1. Una rama por línea. No apiles case 1 -> ...; case 2 -> ...; en la misma línea.
  2. Indentación consistente (4 espacios).
  3. default siempre al final, aunque Java no lo exija (lo verás en 3.10).
  4. Si usas llaves, abre { al final de la línea del case y cierra } en una línea nueva, alineado con case.
1
2
3
4
5
6
7
8
9
// Estilo correcto
switch (opcion) {
    case 1 -> {
        IO.println("Uno");
        IO.println("Segunda línea");
    }
    case 2 -> IO.println("Dos");
    default -> IO.println("Otro");
}

3.4 Switch como expresión: devuelve un valor

3.4.1 La gran novedad: el switch que produce un valor

Hasta ahora, el switch era una sentencia: ejecutaba un bloque y ya. Pero en Java 14+, el switch también puede ser una expresión: produce un valor que puedes asignar a una variable, devolver con return o pasar como argumento. Esto cambia radicalmente cómo se escribe el código.

Sintaxis:

1
2
3
4
5
6
Tipo resultado = switch (selector) {
    case valor1 -> resultado1;
    case valor2 -> resultado2;
    case valor3 -> resultado3;
    default -> resultadoPorDefecto;
};

Detalles clave:

  1. La flecha -> ahora va seguida de una expresión (no de una sentencia), que es el valor que esa rama produce.
  2. No hace falta return dentro del switch: la expresión a la derecha de -> es el valor.
  3. El ; final es obligatorio: cierra la sentencia de asignación completa.
  4. Si una rama necesita varias sentencias y devolver un valor, se usa yield (lo verás en 3.4.4).

3.4.2 Ejemplo: nombre del día

// Switch expression: devuelve el nombre del día
int dia = 3;

String nombre = switch (dia) {
    case 1 -> "Lunes";
    case 2 -> "Martes";
    case 3 -> "Miércoles";
    case 4 -> "Jueves";
    case 5 -> "Viernes";
    case 6 -> "Sábado";
    case 7 -> "Domingo";
    default -> "Día no válido";
};

IO.println("Hoy es " + nombre);

Salida con dia = 3:

Hoy es Miércoles

Esto es radicalmente más limpio que la versión con if/else if o con el switch clásico. Lo que antes eran 16 líneas (7 else if con sus llaves) ahora son 9.

3.4.3 Switch expression como argumento

Como el switch expression produce un valor, puedes usarlo dentro de otras expresiones:

1
2
3
4
5
6
7
8
9
// Directamente en un IO.println
int dia = 5;
IO.println(
    switch (dia) {
        case 1, 2, 3, 4, 5 -> "Día laborable";
        case 6, 7 -> "Fin de semana";
        default -> "Día no válido";
    }
);
// En una concatenación
int mes = 4;
String estacion = "Estación: " + switch (mes) {
    case 12, 1, 2 -> "Invierno";
    case 3, 4, 5 -> "Primavera";
    case 6, 7, 8 -> "Verano";
    case 9, 10, 11 -> "Otoño";
    default -> "Mes no válido";
};
IO.println(estacion);

El switch expression se evalúa, produce su valor, y ese valor se inserta en la expresión mayor. Igual que harías con el operador ternario, pero con muchos más casos.

3.4.4 yield para bloques multilínea

Si una rama necesita varias sentencias antes de devolver el valor, usas un bloque con llaves y la palabra reservada yield para indicar qué devuelve:

int dia = 3;

String nombre = switch (dia) {
    case 1 -> "Lunes";
    case 2 -> "Martes";
    case 3 -> {
        String msg = "Miércoles";
        IO.println("Estamos a mitad de semana.");
        yield msg;   // 'yield' devuelve el valor de esta rama
    }
    case 4 -> "Jueves";
    case 5 -> "Viernes";
    case 6 -> "Sábado";
    case 7 -> "Domingo";
    default -> "Día no válido";
};

yield es el return del switch expression. Devuelve el valor de la rama actual y sale del switch.

Cuándo usar yield:

  • Cuando una rama necesita calcular el valor en varios pasos.
  • Cuando necesitas imprimir algo (un log) antes de devolver.
  • Cuando el valor depende de comprobaciones adicionales dentro de la rama.

En la mayoría de los casos no necesitas yield: una sola expresión a la derecha de -> es suficiente. Úsalo solo cuando de verdad lo necesites.

3.4.5 Switch expression vs sentencia: ¿cuándo usar cuál?

Caso Recomendación
Quieres asignar/devolver un valor según una variable. Switch expression (con =).
Quieres ejecutar acciones (imprimir, modificar estado) según una variable. Switch statement moderno (con flecha, sin asignar).
La lógica de cada rama es compleja, varias sentencias. Switch statement con bloques { }.
Necesitas un valor pero cada rama es muy simple. Switch expression sin yield.

Regla del profesor:

Si el switch produce algo (un valor), usa switch expression. Si el switch hace algo (efectos secundarios como imprimir), usa switch statement. En la práctica, el 80 % de los switches pueden ser expresiones.

3.4.6 La regla de la exhaustividad en switch expression

Una diferencia crítica: en un switch expression, TODOS los casos posibles deben estar cubiertos. No puedes olvidarte de default. Mira esto:

// NO COMPILA: falta el default
String nombre = switch (dia) {
    case 1 -> "Lunes";
    case 2 -> "Martes";
    case 3 -> "Miércoles";
    case 4 -> "Jueves";
    case 5 -> "Viernes";
    case 6 -> "Sábado";
    case 7 -> "Domingo";
};
// Error: el switch expression debe ser exhaustivo

El compilador te obliga a cubrir todos los casos posibles. ¿Por qué? Porque el switch expression tiene que producir un valor siempre, así que si el selector toma un valor no contemplado, el programa no sabría qué devolver. Java se defiende exigiéndote un default:

// SÍ COMPILA: con default
String nombre = switch (dia) {
    case 1 -> "Lunes";
    case 2 -> "Martes";
    case 3 -> "Miércoles";
    case 4 -> "Jueves";
    case 5 -> "Viernes";
    case 6 -> "Sábado";
    case 7 -> "Domingo";
    default -> "Día no válido";
};

Esta es una gran ventaja del switch expression: el compilador te obliga a pensar "qué pasa si el valor no es ninguno de los esperados". Con if/else if, es fácil olvidarse del else final y que el programa se calle en casos imprevistos.

Volveremos sobre esto en la sección 3.10.


3.5 Múltiples etiquetas en un case

3.5.1 Sintaxis de múltiples etiquetas

Una de las mejoras más útiles de la sintaxis moderna: puedes agrupar varios valores en un mismo case separándolos por comas:

case valor1, valor2, valor3 -> ...;

Esto sustituye al fall-through del switch clásico y es mucho más legible.

3.5.2 Ejemplo: días laborables vs fin de semana

1
2
3
4
5
6
7
8
9
int dia = 6;

String tipo = switch (dia) {
    case 1, 2, 3, 4, 5 -> "Día laborable";
    case 6, 7 -> "Fin de semana";
    default -> "Día no válido (1-7)";
};

IO.println(tipo);

Salida con dia = 6:

Fin de semana

Comparemos con la versión clásica con fall-through:

// Versión clásica (frágil)
switch (dia) {
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
        IO.println("Día laborable");
        break;
    case 6:
    case 7:
        IO.println("Fin de semana");
        break;
    default:
        IO.println("Día no válido");
}

La versión moderna es claramente superior: una sola línea por grupo, sin break, sin riesgo de fall-through accidental.

3.5.3 Ejemplo: estación del año

int mes = 4;

String estacion = switch (mes) {
    case 12, 1, 2 -> "Invierno";
    case 3, 4, 5 -> "Primavera";
    case 6, 7, 8 -> "Verano";
    case 9, 10, 11 -> "Otoño";
    default -> "Mes no válido (1-12)";
};

IO.println("Estación: " + estacion);

Esto reemplaza con elegancia el if/else if que escribimos en la sección 2 para el mismo problema. Compara:

// Con if/else if (sección 2)
String estacion;
if (mes == 12 || mes == 1 || mes == 2) {
    estacion = "Invierno";
} else if (mes == 3 || mes == 4 || mes == 5) {
    estacion = "Primavera";
} else if (mes == 6 || mes == 7 || mes == 8) {
    estacion = "Verano";
} else {
    estacion = "Otoño";
}
1
2
3
4
5
6
7
8
// Con switch expression (esta sección)
String estacion = switch (mes) {
    case 12, 1, 2 -> "Invierno";
    case 3, 4, 5 -> "Primavera";
    case 6, 7, 8 -> "Verano";
    case 9, 10, 11 -> "Otoño";
    default -> "Mes no válido (1-12)";
};

El switch es claramente más limpio: menos repetición de mes ==, menos llaves, menos líneas. Aquí es donde el switch brilla y el if se queda corto.

3.5.4 Múltiples etiquetas con bloques

Si una rama con múltiples etiquetas necesita varias sentencias:

int dia = 3;

switch (dia) {
    case 1, 2, 3, 4, 5 -> {
        IO.println("Día laborable.");
        IO.println("Levántate temprano.");
    }
    case 6, 7 -> {
        IO.println("Fin de semana.");
        IO.println("Puedes dormir más.");
    }
    default -> IO.println("Día no válido.");
}

Sin sorpresas: el bloque { } se ejecuta completo si el selector coincide con cualquiera de las etiquetas.

3.5.5 Restricciones con múltiples etiquetas

Qué Admitido
Etiquetas separadas por comas en un case. Sí, con la flecha ->.
Etiquetas separadas por comas en el switch clásico (:). Sí, desde Java 14 (pero se desaconseja el clásico).
Rangos (case 1..5). No en Java (sí en Kotlin, Groovy, etc.). Usa múltiples etiquetas.
Expresiones en etiquetas (case x+1). No. Solo literales y constantes final.

La limitación de "no rangos" es importante: en Java no puedes escribir case 1..5 -> .... Tienes que escribir case 1, 2, 3, 4, 5 -> .... Es un poco más verboso, pero claro.


3.6 Switch con String

3.6.1 Sintaxis

Desde Java 7, el switch admite String como selector. Es perfectamente equivalente a comparar con .equals(), pero más conciso.

1
2
3
4
5
6
7
8
String rol = "admin";

switch (rol) {
    case "admin" -> IO.println("Acceso total.");
    case "editor" -> IO.println("Acceso de edición.");
    case "lector" -> IO.println("Acceso de solo lectura.");
    default -> IO.println("Rol no reconocido.");
}

3.6.2 Switch con String es case-sensitive

El switch con String distingue mayúsculas y minúsculas, igual que .equals(). Si el usuario escribe "Admin" o "ADMIN", no entra en el case "admin".

1
2
3
4
5
6
7
String rol = "Admin";   // con mayúscula

switch (rol) {
    case "admin" -> IO.println("Acceso total.");   // NO entra
    case "editor" -> IO.println("Acceso de edición.");
    default -> IO.println("Rol no reconocido.");   // entra aquí
}

Para evitar este problema, normaliza el String antes del switch con .toLowerCase() o .toUpperCase():

1
2
3
4
5
6
7
8
9
String rol = "Admin";
String rolNormalizado = rol.toLowerCase();   // "admin"

switch (rolNormalizado) {
    case "admin" -> IO.println("Acceso total.");
    case "editor" -> IO.println("Acceso de edición.");
    case "lector" -> IO.println("Acceso de solo lectura.");
    default -> IO.println("Rol no reconocido.");
}

Ahora "Admin", "ADMIN", "admin" y "aDmIn" entran todos en el case "admin". Mucho más robusto.

3.6.3 Switch con String vs if/else if con .equals()

Comparemos las dos formas para el mismo problema:

// Con if/else if (sección 2)
if (rol.equals("admin")) {
    IO.println("Acceso total.");
} else if (rol.equals("editor")) {
    IO.println("Acceso de edición.");
} else if (rol.equals("lector")) {
    IO.println("Acceso de solo lectura.");
} else {
    IO.println("Rol no reconocido.");
}
1
2
3
4
5
6
7
// Con switch (esta sección)
switch (rol) {
    case "admin" -> IO.println("Acceso total.");
    case "editor" -> IO.println("Acceso de edición.");
    case "lector" -> IO.println("Acceso de solo lectura.");
    default -> IO.println("Rol no reconocido.");
}

El switch es más limpio y, además, el compilador sabe que estás comparando siempre contra el mismo rol (cosa que con if/else if no es obvia). En algunos casos, la JVM puede optimizar el switch con String usando un hashCode() interno, haciéndolo más eficiente que una cadena de .equals().

3.6.4 Switch con String y null

Aquí hay una sutileza importante: en el switch clásico, si el String es null, el programa lanza NullPointerException. No hay case null.

1
2
3
4
5
6
String rol = null;

switch (rol) {                              // ¡Lanza NullPointerException!
    case "admin" -> IO.println("Acceso total.");
    // ...
}

Para evitarlo, comprueba null antes del switch:

String rol = null;

if (rol == null) {
    IO.println("No has indicado ningún rol.");
} else {
    switch (rol) {
        case "admin" -> IO.println("Acceso total.");
        case "editor" -> IO.println("Acceso de edición.");
        default -> IO.println("Rol no reconocido.");
    }
}

En Java 21+ existe case null en switch con pattern matching (sección 3.8), pero para switch con String "puro" hay que hacer la comprobación aparte.

3.6.5 Ejemplo: menú de opciones

import java.util.Scanner;

public class MenuString {

    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);

        IO.println("--- Menú ---");
        IO.println("sumar | restar | multiplicar | dividir | salir");
        IO.print("¿Qué quieres hacer? ");
        String opcion = sc.nextLine().trim().toLowerCase();

        switch (opcion) {
            case "sumar" -> {
                IO.print("A: ");
                double a = Double.parseDouble(sc.nextLine().replace(',', '.'));
                IO.print("B: ");
                double b = Double.parseDouble(sc.nextLine().replace(',', '.'));
                IO.println("A + B = " + (a + b));
            }
            case "restar" -> {
                IO.print("A: ");
                double a = Double.parseDouble(sc.nextLine().replace(',', '.'));
                IO.print("B: ");
                double b = Double.parseDouble(sc.nextLine().replace(',', '.'));
                IO.println("A - B = " + (a - b));
            }
            case "multiplicar" -> {
                IO.print("A: ");
                double a = Double.parseDouble(sc.nextLine().replace(',', '.'));
                IO.print("B: ");
                double b = Double.parseDouble(sc.nextLine().replace(',', '.'));
                IO.println("A * B = " + (a * b));
            }
            case "dividir" -> {
                IO.print("A: ");
                double a = Double.parseDouble(sc.nextLine().replace(',', '.'));
                IO.print("B: ");
                double b = Double.parseDouble(sc.nextLine().replace(',', '.'));
                if (b == 0) {
                    IO.println("Error: división por cero.");
                } else {
                    IO.println("A / B = " + (a / b));
                }
            }
            case "salir" -> IO.println("Adiós.");
            default -> IO.println("Opción no reconocida. Escribe una de las del menú.");
        }

        sc.close();
    }
}

Fíjate:

  • Normalizamos la entrada con .trim().toLowerCase(): el usuario puede escribir " Sumar ", "SUMAR" o "sumar" y todas funcionan.
  • Cada case es un bloque con sus propias variables a y b. Esto es legal porque cada bloque tiene su propio ámbito.
  • default para cualquier entrada no reconocida.

3.7 Switch con enum

3.7.1 Recordatorio: qué es un enum

Un enum (de enumeration) es un tipo especial de Java que sirve para definir un conjunto cerrado de constantes nombradas. Ya lo viste por encima en UD1 (sección 6): las constantes de un enum son valores fijos, escritos en MAYÚSCULAS, separados por comas.

1
2
3
4
// Definición mínima de un enum
enum DiaSemana {
    LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO
}

Los enums se ven en UD4

Aquí solo usamos enums como "cajas negras" reconocibles. La definición completa de enums (con propiedades, constructores, métodos, etc.) se cubre en UD4, sección 6. Por ahora te basta saber que un enum es un conjunto de constantes y que se pueden usar en un switch igual que un int o un String.

3.7.2 Switch con enum: la mejor opción para conjuntos cerrados

El switch con enum es uno de los usos más elegantes. Como el compilador conoce todos los valores posibles del enum, puede comprobar que has cubierto todos los casos.

// Definimos el enum (luego verás más en UD4)
enum DiaSemana {
    LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO
}

public class SwitchEnum {

    public static void main(String[] args) {

        DiaSemana hoy = DiaSemana.MIERCOLES;

        String tipo = switch (hoy) {
            case LUNES, MARTES, MIERCOLES, JUEVES, VIERNES -> "Día laborable";
            case SABADO, DOMINGO -> "Fin de semana";
        };

        IO.println("Hoy es " + tipo);
    }
}

Salida:

Hoy es Día laborable

3.7.3 Exhaustividad real con enum

Aquí viene lo potente: como el compilador sabe que DiaSemana tiene exactamente 7 valores, si te dejas uno, da error de compilación.

1
2
3
4
5
// NO COMPILA: falta SABADO y DOMINGO
String tipo = switch (hoy) {
    case LUNES, MARTES, MIERCOLES, JUEVES, VIERNES -> "Día laborable";
    // Falta SABADO y DOMINGO
};

Error del compilador:

error: the switch expression does not cover all possible input values

Esto es oro puro: el compilador te obliga a pensar en todos los casos. Si mañana añades un nuevo valor al enum (por ejemplo, FESTIVO), todos los switch que lo usan te marcan error hasta que los actualices. Es la fuerza del sistema de tipos trabajando a tu favor.

Si pones default en un switch con enum exhaustivo, IntelliJ te avisa de que es redundante. La mejor práctica es cubrir todos los casos explícitamente y no usar default:

1
2
3
4
5
// MEJOR: cubrir todos los casos, sin default
String tipo = switch (hoy) {
    case LUNES, MARTES, MIERCOLES, JUEVES, VIERNES -> "Día laborable";
    case SABADO, DOMINGO -> "Fin de semana";
};

3.7.4 No uses default con enums exhaustivos

Hay una regla no escrita en la comunidad Java:

Si usas un enum como selector y cubres todos sus valores, no pongas default. El default solo sirve para "tapar" casos no contemplados, y si el enum está cerrado, no hay más casos.

// Regular: default innecesario
String tipo = switch (hoy) {
    case LUNES, MARTES, MIERCOLES, JUEVES, VIERNES -> "Día laborable";
    case SABADO, DOMINGO -> "Fin de semana";
    default -> "Imposible";   // el compilador sabe que nunca se ejecuta
};

// Mejor: sin default
String tipo = switch (hoy) {
    case LUNES, MARTES, MIERCOLES, JUEVES, VIERNES -> "Día laborable";
    case SABADO, DOMINGO -> "Fin de semana";
};

Sin default, si mañana añades FESTIVO al enum, el compilador te obliga a actualizar el switch. Con default, el switch "se traga" el nuevo caso sin avisarte, y podrías tener un bug silencioso.

3.7.5 Ejemplo completo: turno de trabajo

enum Turno {
    MAÑANA, TARDE, NOCHE
}

public class SwitchTurno {

    public static void main(String[] args) {

        Turno miTurno = Turno.NOCHE;

        String descripcion = switch (miTurno) {
            case MAÑANA -> "De 06:00 a 14:00";
            case TARDE -> "De 14:00 a 22:00";
            case NOCHE -> "De 22:00 a 06:00";
        };

        double bonus = switch (miTurno) {
            case MAÑANA -> 0.0;
            case TARDE -> 0.05;   // 5 % extra
            case NOCHE -> 0.15;   // 15 % extra
        };

        IO.println("Tu turno: " + descripcion);
        IO.println("Bonus salarial: " + (bonus * 100) + " %");
    }
}

Aquí tenemos dos switch expression sobre el mismo enum: uno devuelve un String, otro un double. Los dos son exhaustivos (cubren los tres valores del enum) y no necesitan default.

3.7.6 Enum del API de Java: java.time.DayOfWeek

En Java viene predefinido el enum java.time.DayOfWeek (lo verás en profundidad en UD2 cuando estudiemos java.time). Aunque pertenece al API que se profundiza en UD2, ya podemos usarlo aquí porque es "una caja negra con 7 valores":

import java.time.DayOfWeek;

DayOfWeek hoy = DayOfWeek.WEDNESDAY;

String tipo = switch (hoy) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Laborable";
    case SATURDAY, SUNDAY -> "Fin de semana";
};

IO.println(tipo);

Aviso: en java.time.DayOfWeek los valores están en inglés (MONDAY, TUESDAY, etc.). Es una de las pequeñas contradicciones del API de Java: la internacionalización (i18n) de los nombres va por otro lado (DateTimeFormatter), y los identificadores del enum siempre son en inglés.


3.8 Exhaustividad y default

3.8.1 ¿Qué es la exhaustividad?

La exhaustividad es la propiedad de un switch que cubre todos los casos posibles del selector. Java la exige en los switch expression (los que devuelven un valor) y la recomienda en los switch statement (los que ejecutan acciones).

Tipo de switch ¿Exige exhaustividad?
Switch expression (var r = switch(...) { ... };) , siempre.
Switch statement con flecha (->) sobre int, char, String No obligatoria, pero recomendada con default.
Switch statement con enum Recomendada: cubre todos los valores o pon default.

3.8.2 ¿Cómo se logra la exhaustividad?

Hay dos formas:

  1. Cubriendo todos los valores del enum (en switch con enum).
  2. Poniendo default (la forma universal).

Ejemplo de cada una:

// Forma 1: cubrir todos los valores del enum
String r1 = switch (dia) {
    case LUNES, MARTES, MIERCOLES, JUEVES, VIERNES -> "Laborable";
    case SABADO, DOMINGO -> "Fin de semana";
    // No hace falta default: el enum tiene 7 valores y los cubrimos todos.
};

// Forma 2: default (la universal)
String r2 = switch (dia) {
    case 1, 2, 3, 4, 5 -> "Laborable";
    case 6, 7 -> "Fin de semana";
    default -> "Día no válido";   // cubre "todo lo demás"
};

Una tercera forma, reservada para UD7

Existe una tercera forma de lograr exhaustividad, basada en sealed classes y record (jerarquías cerradas de tipos). La verás en UD7 (POO Avanzada), cuando conozcas objetos, herencia y polimorfismo. Aquí no la tratamos porque necesita conceptos que aún no tienes.

3.8.3 default como "red de seguridad"

Para switch con int o String, el default es obligatorio en switch expression y recomendado en switch statement. Sirve para capturar valores que no has contemplado:

int dia = 8;

String nombre = switch (dia) {
    case 1 -> "Lunes";
    case 2 -> "Martes";
    case 3 -> "Miércoles";
    case 4 -> "Jueves";
    case 5 -> "Viernes";
    case 6 -> "Sábado";
    case 7 -> "Domingo";
    default -> "Día no válido (1-7): " + dia;
};

Sin default, este switch expression no compilaría porque el compilador no puede garantizar que dia esté entre 1 y 7 (es un int, puede valer cualquier cosa).

3.8.4 default con throw: el patrón "inesperado"

En código profesional, cuando el default no debería ejecutarse nunca (porque la lógica del programa garantiza que los demás casos cubren todo), se suele lanzar una excepción para avisar de que algo ha ido mal:

// Patrón "default inesperado": lanza excepción si entra aquí
String nombre = switch (dia) {
    case 1 -> "Lunes";
    case 2 -> "Martes";
    case 3 -> "Miércoles";
    case 4 -> "Jueves";
    case 5 -> "Viernes";
    case 6 -> "Sábado";
    case 7 -> "Domingo";
    default -> throw new IllegalArgumentException("Día inesperado: " + dia);
};

Si dia vale 8, en vez de imprimir "Día no válido", el programa lanza una excepción y se detiene con un mensaje claro. Las excepciones se ven en la sección 8; por ahora te basta saber que throw new IllegalArgumentException("mensaje") es la forma de decir "esto no debería haber pasado, aborta".

3.8.5 Diagrama de decisión: ¿necesito default?

flowchart TD
    A["¿Es switch expression?"] -->|Sí| B["¿Selector es enum y cubres todos los valores?"]
    A -->|No, es statement| C["Recomendado poner default"]
    B -->|Sí| D["No hace falta default"]
    B -->|No| E["Pon default o throw"]
    C --> F["Fin"]
    D --> F
    E --> F

3.9 Cuándo usar switch y cuándo if

Ya tenemos dos herramientas de selección: if/else if/else (sección 2) y switch (esta sección). ¿Cuándo usar cada una? Esta tabla te lo decide en segundos:

Criterio Usa if Usa switch
Comparas el mismo valor contra muchos literales. Funciona, pero verboso. Mejor switch.
Comparas rangos (edad >= 18 && edad <= 65). Mejor if. Switch no admite rangos.
Comparas condiciones booleanas complejas. Mejor if. Switch no admite booleanos.
Quieres asignar/devolver un valor según una variable. Funciona, pero verboso. Mejor switch expression.
Selector es boolean, long, float o double. Solo if. Switch no los admite. No se puede.
Selector es int, char, String o enum. Funciona. Mejor switch.
Casos mutuamente excluyentes y pocos. Cualquiera. Cualquiera.
Casos con condiciones compuestas (||, &&). Mejor if. No aplica (todavía).
Validación de entrada (< 0 || > 120). Mejor if. No aplica.

3.9.1 Reglas mnemotécnicas

"Switch para valores, if para condiciones."

Si la pregunta es "¿qué valor tiene esta variable?", usa switch. Si la pregunta es "¿se cumple esta condición?", usa if.

"Switch cuando hay 3+ casos sobre el mismo selector."

Si tienes 3 o más else if comparando x == valor, cámbialo a switch. Es más limpio y el compilador te ayuda con la exhaustividad.

"Switch expression cuando el switch produce un valor."

Si el switch devuelve algo (un String, un int, un cálculo), usa switch expression. Si solo hace algo (imprimir, modificar estado), usa switch statement.

3.9.2 Ejemplo: refactorización de if a switch

Vamos a refactorizar un código con if a switch para ver la diferencia:

Antes (con if/else if):

String nombreDia;
if (dia == 1) {
    nombreDia = "Lunes";
} else if (dia == 2) {
    nombreDia = "Martes";
} else if (dia == 3) {
    nombreDia = "Miércoles";
} else if (dia == 4) {
    nombreDia = "Jueves";
} else if (dia == 5) {
    nombreDia = "Viernes";
} else if (dia == 6) {
    nombreDia = "Sábado";
} else if (dia == 7) {
    nombreDia = "Domingo";
} else {
    nombreDia = "Día no válido";
}

Después (con switch expression):

String nombreDia = switch (dia) {
    case 1 -> "Lunes";
    case 2 -> "Martes";
    case 3 -> "Miércoles";
    case 4 -> "Jueves";
    case 5 -> "Viernes";
    case 6 -> "Sábado";
    case 7 -> "Domingo";
    default -> "Día no válido";
};

De 16 líneas a 9. Y, sobre todo, más legible: el switch deja claro que estamos clasificando dia, mientras que el if/else if repite dia == una y otra vez.

3.9.3 Cuándo if sigue siendo mejor

Hay casos en los que el if no tiene rival:

// Validación de rango: if es la única opción razonable
if (edad < 0 || edad > 120) {
    IO.println("Edad no válida.");
} else if (edad < 18) {
    IO.println("Menor de edad.");
} else if (edad < 65) {
    IO.println("Adulto.");
} else {
    IO.println("Jubilado.");
}

Aquí no puedes usar switch porque:

  • Los casos no son literales (son rangos como edad < 18).
  • El selector no es un valor único, son condiciones booleanas.

Por eso ambas herramientas coexisten: if para condiciones, switch para valores.


3.10 Errores habituales al usar switch

Antes de pasar a los ejemplos, repasemos los errores más comunes para que no caigas en ellos.

3.10.1 Error 1: olvidar break en el switch clásico

Ya lo vimos en 3.2.3: el fall-through accidental es el bug más típico del switch clásico.

Solución: usa siempre la sintaxis moderna con flecha ->. Si te ves obligado a mantener código clásico, pon break al final de cada case y revisa dos veces.

3.10.2 Error 2: olvidar default en switch expression

1
2
3
4
5
6
7
8
// NO COMPILA: falta default o cobertura completa
String r = switch (dia) {
    case 1 -> "Lunes";
    case 2 -> "Martes";
    // ...
    case 7 -> "Domingo";
    // falta default
};

Solución: en switch expression, siempre cubre todos los casos o pon default. IntelliJ lo marca en rojo.

3.10.3 Error 3: null en switch con String

1
2
3
4
5
6
String rol = null;

switch (rol) {                          // NullPointerException
    case "admin" -> IO.println("OK");
    // ...
}

Solución: comprueba null antes del switch con un if. (Existe case null con pattern matching, pero se ve en UD7.)

3.10.4 Error 4: no normalizar String antes del switch

1
2
3
4
5
6
String rol = "Admin";   // con mayúscula

switch (rol) {
    case "admin" -> IO.println("Acceso total.");   // NO entra
    // ...
}

Solución: .toLowerCase() o .toUpperCase() antes del switch, o usa equalsIgnoreCase con if/else if.

3.10.5 Error 5: rangos en switch

1
2
3
4
5
6
// NO COMPILA: los rangos no existen en switch
switch (edad) {
    case 0..17 -> IO.println("Menor");
    case 18..65 -> IO.println("Adulto");
    default -> IO.println("Jubilado");
}

Solución: para rangos, usa if. Si insistes en switch, tienes que enumerar todos los valores (case 0, 1, 2, 3, ..., 17 -> ...), lo cual es absurdo.

3.10.6 Error 6: default en switch con enum exhaustivo

1
2
3
4
5
6
// default innecesario: el enum está cerrado
String r = switch (dia) {
    case LUNES, MARTES, MIERCOLES, JUEVES, VIERNES -> "Laborable";
    case SABADO, DOMINGO -> "Fin de semana";
    default -> "Imposible";   // IntelliJ avisa: caso imposible
};

Solución: si cubres todos los valores del enum, quita el default. Así, si mañana añades un valor nuevo al enum, el compilador te obliga a actualizar el switch.

3.10.7 Error 7: mezclar : y -> en el mismo switch

1
2
3
4
5
// NO COMPILA: no se pueden mezclar las dos sintaxis
switch (dia) {
    case 1: IO.println("Lunes");      // sintaxis clásica
    case 2 -> IO.println("Martes");   // sintaxis moderna
}

Solución: usa una sola sintaxis por switch. Y preferiblemente, siempre la moderna (->).


3.11 Ejemplos resueltos paso a paso

Cuatro ejemplos completos que integran todo lo aprendido. Te recomiendo escribirlos en IntelliJ y jugar con los valores.

3.11.1 Ejemplo 1: día de la semana con switch expression

public class DiaSemanaSwitch {

    public static void main(String[] args) {

        IO.print("Día de la semana (1-7): ");
        String entrada = IO.readln();

        int dia = Integer.parseInt(entrada);

        String nombre = switch (dia) {
            case 1 -> "Lunes";
            case 2 -> "Martes";
            case 3 -> "Miércoles";
            case 4 -> "Jueves";
            case 5 -> "Viernes";
            case 6 -> "Sábado";
            case 7 -> "Domingo";
            default -> "Día no válido";   // obligatorio en switch expression
        };

        String tipo = switch (dia) {
            case 1, 2, 3, 4, 5 -> "laborable";
            case 6, 7 -> "fin de semana";
            default -> "desconocido";
        };

        IO.println("Hoy es " + nombre + ", un día " + tipo + ".");
    }
}

Salidas posibles:

Entrada Salida
1 Hoy es Lunes, un día laborable.
6 Hoy es Sábado, un día fin de semana.
9 Hoy es Día no válido, un día desconocido.

Puntos clave:

  1. Dos switch expression sobre el mismo selector: una para el nombre, otra para el tipo.
  2. Múltiples etiquetas para agrupar laborables y fin de semana.
  3. default obligatorio porque dia es un int y el compilador no sabe que está entre 1 y 7.
  4. Sin import: java.lang.IO está disponible por defecto.

3.11.2 Ejemplo 2: calculadora con switch expression

Reescribimos la calculadora de la sección 2 usando switch expression.

public class CalculadoraSwitch {

    public static void main(String[] args) {

        IO.println("=== Calculadora ===");
        IO.println("1. Sumar");
        IO.println("2. Restar");
        IO.println("3. Multiplicar");
        IO.println("4. Dividir");
        IO.print("Opción (1-4): ");
        String entrada = IO.readln();

        int opcion = Integer.parseInt(entrada);

        IO.print("Operando A: ");
        double a = Double.parseDouble(IO.readln().trim().replace(',', '.'));
        IO.print("Operando B: ");
        double b = Double.parseDouble(IO.readln().trim().replace(',', '.'));

        // Switch expression con bloques (yield) para la división
        String resultado = switch (opcion) {
            case 1 -> {
                IO.println("Operación: suma");
                yield "A + B = " + (a + b);
            }
            case 2 -> {
                IO.println("Operación: resta");
                yield "A - B = " + (a - b);
            }
            case 3 -> {
                IO.println("Operación: multiplicación");
                yield "A * B = " + (a * b);
            }
            case 4 -> {
                IO.println("Operación: división");
                if (b == 0) {
                    yield "Error: no se puede dividir por cero.";
                } else {
                    yield "A / B = " + (a / b);
                }
            }
            default -> "Opción no reconocida.";
        };

        IO.println(resultado);
    }
}

Puntos clave:

  1. Switch expression con bloques { ... yield ...; } porque cada rama imprime un mensaje y además devuelve un valor.
  2. Validación de división por cero dentro del case 4 con un if interno. Como estamos dentro de un bloque, podemos escribir varias sentencias.
  3. yield devuelve el valor de cada rama.
  4. El switch produce un String que se asigna a resultado y se imprime al final.

3.11.3 Ejemplo 3: tarifa según tipo de vehículo (con enum)

// Enum: se profundiza en UD4. Aquí lo usamos como "caja negra".
enum Vehiculo {
    COCHE, MOTO, AUTOBUS, CAMION, BICICLETA
}

public class TarifaPeaje {

    public static void main(String[] args) {

        Vehiculo v = Vehiculo.COCHE;

        double tarifa = switch (v) {
            case COCHE     -> 3.50;
            case MOTO      -> 2.00;
            case AUTOBUS   -> 8.00;
            case CAMION    -> 12.00;
            case BICICLETA -> 0.00;
        };

        IO.println("Vehículo: " + v);
        IO.println(String.format("Tarifa: %.2f €", tarifa));
    }
}

Puntos clave:

  1. Enum propio definido fuera de la clase (sintaxis mínima, sin profundizar — UD4).
  2. Switch expression sin default: como el enum está cerrado y cubrimos los 5 valores, el compilador garantiza exhaustividad.
  3. Cada rama produce un double: el switch expression devuelve el valor de la tarifa.
  4. Si mañana añadiéramos FURGONETA al enum, el compilador daría error hasta que añadamos un case FURGONETA -> .... Eso es la fuerza de la exhaustividad con enums trabajando a tu favor.

3.11.4 Ejemplo 4: menú interactivo con switch statement

Aquí un switch statement (no expression) que ejecuta acciones, no produce un valor:

public class MenuSwitch {

    public static void main(String[] args) {

        IO.println("=== Menú ===");
        IO.println("1. Saludar");
        IO.println("2. Despedirse");
        IO.println("3. Mostrar hora");
        IO.println("4. Salir");
        IO.print("Opción: ");
        String entrada = IO.readln();

        int opcion = Integer.parseInt(entrada);

        // Switch statement (no expression): ejecuta acciones, no devuelve valor
        switch (opcion) {
            case 1 -> {
                IO.print("¿Cómo te llamas? ");
                String nombre = IO.readln().trim();
                IO.println("Hola, " + nombre + ".");
            }
            case 2 -> {
                IO.print("¿Cómo te llamas? ");
                String nombre = IO.readln().trim();
                IO.println("Adiós, " + nombre + ".");
            }
            case 3 -> {
                // Mostrar hora del sistema (java.time se ve en UD2; aquí usamos una versión simple)
                long millis = System.currentTimeMillis();
                IO.println("Han pasado " + millis + " ms desde el 1 de enero de 1970.");
            }
            case 4 -> IO.println("Saliendo del programa...");
            default -> IO.println("Opción incorrecta");
        }
    }
}

Puntos clave:

  1. Switch statement con flecha (no expression): no se asigna a ninguna variable.
  2. Bloques { } porque cada rama tiene varias sentencias.
  3. Cada bloque puede declarar sus propias variables locales (nombre), independientes entre sí.

3.12 Buenas prácticas del switch

3.12.1 Las 8 reglas del switch profesional

  1. Usa la sintaxis moderna con flecha ->. No escribas switch clásico a menos que mantengas código heredado.
  2. Switch expression cuando produces un valor. Switch statement cuando solo ejecutas acciones.
  3. Múltiples etiquetas (case 1, 2, 3 ->) en vez de fall-through.
  4. Cubre todos los casos en switch expression: o todos los valores del enum, o default.
  5. No pongas default con enum exhaustivo. Deja que el compilador te avise si falta un caso.
  6. Normaliza String antes del switch con .toLowerCase() o .toUpperCase().
  7. Comprueba null antes del switch con String (con un if previo).
  8. throw en default inesperado para detectar bugs: default -> throw new IllegalArgumentException(...).

3.12.2 Cuándo refactorizar

Síntoma Refactoriza a
5+ else if comparando el mismo valor. switch expression.
Switch clásico con muchos break. Switch moderno con flecha.
Switch con case null que no compila (clásico). if previo para null.
default con enum exhaustivo. Quitar default y cubrir todos los casos.

3.12.3 Comentar el switch

Los comentarios buenos en un switch explican decisiones de diseño, no lo obvio:

// MAL: comenta lo evidente
switch (opcion) {
    case 1 -> IO.println("Sumar");   // si opcion es 1, suma
}

// BIEN: explica una decisión de negocio
switch (tipoCliente) {
    case VIP -> descuento = 0.20;   // Política 2025: VIP siempre 20 %, sin mínimos
    case NORMAL -> descuento = 0.05;
    case NUEVO -> descuento = 0.10; // Promoción de captación: 10 % primera compra
    default -> descuento = 0.0;
}

3.13 Actividades propuestas

Cinco actividades graduadas. Hazlas en orden.

3.13.1 Actividad 1 — Refactoriza de if a switch

Objetivo: practicar la refactorización de if/else if a switch expression.

Enunciado: Toma el siguiente código (que usa if/else if) y reescríbelo con switch expression:

String nombreMes;
if (mes == 1) nombreMes = "Enero";
else if (mes == 2) nombreMes = "Febrero";
else if (mes == 3) nombreMes = "Marzo";
else if (mes == 4) nombreMes = "Abril";
else if (mes == 5) nombreMes = "Mayo";
else if (mes == 6) nombreMes = "Junio";
else if (mes == 7) nombreMes = "Julio";
else if (mes == 8) nombreMes = "Agosto";
else if (mes == 9) nombreMes = "Septiembre";
else if (mes == 10) nombreMes = "Octubre";
else if (mes == 11) nombreMes = "Noviembre";
else if (mes == 12) nombreMes = "Diciembre";
else nombreMes = "Mes no válido";

Requisitos:

  • Usa switch expression con flecha ->.
  • Incluye default.
  • Asigna el resultado a String nombreMes.

Entrega: archivo RefactorMeses.java en el paquete jams.programacion.ud3.

Criterios evaluados: CE 3.1, CE 3.5, CE 3.7.

3.13.2 Actividad 2 — Calculadora de tarifa de aparcamiento

Objetivo: practicar switch expression con bloques y yield.

Enunciado: Una app de aparcamiento cobra:

Tipo de vehículo Tarifa (€/h)
COCHE 2.50
MOTO 1.20
AUTOBUS 6.00
CAMION 8.50
BICICLETA 0.00 (gratis)

Requisitos:

  • Define un enum Vehiculo con los cinco valores (fuera de la clase main).
  • Pregunta al usuario el tipo de vehículo (1 = COCHE, 2 = MOTO, ... 5 = BICICLETA).
  • Pregunta el número de horas (admite decimales).
  • Usa un switch statement sobre int para convertir la opción numérica en un valor del enum (con default -> throw para opción inválida).
  • Usa un switch expression sobre el enum para obtener la tarifa por hora.
  • Calcula el total y muéstralo con String.format("%.2f", total).
  • Lee todo con IO.readln().

Entrega: archivo TarifaAparcamiento.java en el paquete jams.programacion.ud3. Incluye en comentarios al final tres ejemplos de ejecución.

Criterios evaluados: CE 3.1, CE 3.5, CE 3.7.

3.13.3 Actividad 3 — Conversor de puntuación a letra

Objetivo: practicar switch con rangos simulados (usando if interno) y comparativa con if/else if.

Enunciado: Escribe un programa ConversorNota que pida al usuario una nota numérica (0.0 a 10.0) y devuelva su letra (A, B, C, D, F) según la tabla del ejemplo 2.8.4 de la sección 2.

Tareas:

  1. Versión A (con if/else if): copia la solución del ejemplo 2.8.4.
  2. Versión B (con switch): como el switch no admite rangos, usa un truco: trunca la nota a un int con (int) nota y haz switch sobre ese int, agrupando los casos. Por ejemplo: case 9, 10 -> "A", case 7, 8 -> "B", etc. (Cuidado con los decimales: nota = 7.9 truncada es 7, que va a "B". ¿Es correcto?)
  3. Compara ambas versiones: ¿cuál es más legible? ¿Cuál maneja mejor los decimales? Escribe tus conclusiones en un comentario al final.

Requisitos:

  • Lee la nota con IO.readln().trim().replace(',', '.').

Entrega: archivo ConversorNota.java con ambas versiones (la B activa, la A en un comentario de bloque).

Criterios evaluados: CE 3.1, CE 3.5, CE 3.7.

3.13.4 Actividad 4 — Cuestionario de repaso

Responde por escrito:

  1. ¿Cuál es la diferencia principal entre switch clásico y switch con flecha ->?
  2. ¿Qué es el fall-through y por qué es peligroso?
  3. ¿Cuándo se usa yield en un switch expression?
  4. ¿Por qué el switch expression obliga a ser exhaustivo?
  5. ¿Por qué no es buena idea poner default en un switch con enum si ya cubres todos los valores?
  6. Escribe con múltiples etiquetas: "los días 6 y 7 son fin de semana".
  7. ¿Qué pasa si haces switch sobre un String que es null?
  8. ¿Por qué switch (edad) { case 0..17 -> ...; } no compila en Java?
  9. ¿Qué hace default -> throw new IllegalArgumentException(...)?
  10. ¿Cuándo eliges if y cuándo switch? Pon dos ejemplos de cada uno.
  11. ¿Por qué conviene normalizar un String con toLowerCase() antes de un switch?
  12. ¿Por qué el switch con boolean, long, float y double no está permitido?

Entrega: archivo cuestionario_seccion3.txt con tus respuestas.

Criterios evaluados: CE 3.1, CE 3.7.


3.15 Lo que NO hemos visto (y dónde verlo)

Esta sección te ha enseñado el switch moderno en su versión básica: switch clásico, switch con flecha, switch expression, múltiples etiquetas, switch con String y switch con enum. Con eso ya puedes escribir código profesional para clasificar por valor.

Hay tres cosas relacionadas con el switch que no hemos visto y que se reservan para unidades posteriores, porque necesitan conceptos que todavía no tienes:

Concepto ¿Por qué no aquí? ¿Dónde se ve?
Switch con pattern matching (case Integer i -> ...) Necesita entender Object, autoboxing, jerarquía de tipos y casting. UD7 (POO Avanzada).
Guards con when (case Integer i when i > 0 -> ...) Extensión del pattern matching en switch. UD7 (POO Avanzada).
Exhaustividad con sealed classes y record Necesita conocer herencia, interfaces, sealed y records. UD4 (intro) y UD7 (desarrollo).
case null Forma parte del pattern matching en switch. UD7 (POO Avanzada).

Cuando llegues a UD7, después de haber visto UD2 (POO con la API de Java) y UD4 (Clases y Objetos Propios), tendrás todas las herramientas para entender el pattern matching en switch de una sola vez y en profundidad. Esa es la razón por la que lo hemos dejado aparcado: aprenderlo ahora, sin saber qué es un objeto, sería copiar código sin entenderlo.