El viejo switch
bueno switch
estado en Java desde el primer día. Todos lo usamos y estamos acostumbrados, especialmente sus peculiaridades. (¿Alguien más se molesta por el break
?) Pero ahora todo comienza a cambiar: en Java 12, el interruptor en lugar de un operador se ha convertido en una expresión:
boolean result = switch(ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException());
Switch ahora tiene la capacidad de devolver el resultado de su trabajo, que puede asignarse a una variable; También puede usar la sintaxis de estilo lambda, que le permite deshacerse de la transferencia para todos los case
en los que no hay una declaración de break
.
En esta guía, le contaré todo lo que necesita saber sobre las expresiones de cambio en Java 12.
Vista previa
De acuerdo con la especificación preliminar del lenguaje , las expresiones de cambio recién comienzan a implementarse en Java 12.
Esto significa que esta construcción de control se puede cambiar en futuras versiones de la especificación del lenguaje.
Para comenzar a usar la nueva versión de switch
debe usar la opción de línea de comando --enable-preview
tanto durante la compilación como durante el inicio del programa (también debe usar la --release 12
al compilar - nota del traductor).
Tenga en cuenta que ese interruptor , como expresión, actualmente no tiene la sintaxis final en Java 12.
Si desea jugar con todo esto usted mismo, puede visitar mi proyecto de demostración Java X en un github .
Problema con declaraciones en el interruptor
Antes de pasar a una descripción general de las innovaciones en switch , evalúe rápidamente una situación. Supongamos que nos enfrentamos a un boulean ternary "terrible" y queremos convertirlo en un boulean regular. Aquí hay una forma de hacer esto:
boolean result; switch(ternaryBool) { case TRUE: result = true;
De acuerdo en que esto es muy inconveniente. Al igual que muchas otras opciones de cambio que se encuentran en "naturaleza", el ejemplo anterior simplemente calcula el valor de una variable y lo asigna, pero la implementación se omite (declara el result
del identificador y lo usa más tarde), se repite (mi break
'y siempre el resultado de copiar-pasta) y propenso a errores (¿olvidó otra rama? ¡Oh!). Claramente hay algo que mejorar.
Intentemos resolver estos problemas colocando el interruptor en un método separado:
private static boolean toBoolean(Bool ternaryBool) { switch(ternaryBool) { case TRUE: return true; case FALSE: return false; case FILE_NOT_FOUND: throw new UncheckedIOException("This is ridiculous!", new FileNotFoundException());
Esto es mucho mejor: no hay una variable ficticia, no hay break
saturan el código y los mensajes del compilador sobre la ausencia de default
(incluso si esto no es necesario, como en este caso).
Pero, si lo piensa, no estamos obligados a crear métodos solo para eludir la característica de lenguaje incómodo. Y esto es incluso sin considerar que dicha refactorización no siempre es posible. ¡No, necesitamos una mejor solución!
¡Introduciendo expresiones de cambio!
Como mostré al comienzo del artículo, comenzando con Java 12 y superior, puede resolver el problema anterior de la siguiente manera:
boolean result = switch(ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException());
Creo que esto es bastante obvio: si ternartBool
es TRUE
, entonces el result
'se establecerá en true
(en otras palabras, TRUE
convierte en true
). FALSE
convierte en false
.
Dos pensamientos surgen inmediatamente:
switch
puede tener un resultado;- ¿Qué pasa con las flechas?
Antes de profundizar en los detalles de las nuevas características del interruptor , al principio hablaré sobre estos dos aspectos principales.
Expresión o declaración
Puede que se sorprenda de que cambiar ahora sea una expresión. ¿Pero qué era él antes?
Antes de Java 12, un interruptor era un operador, una construcción imperativa que regula el flujo de control.
Piense en las diferencias entre las versiones antigua y nueva de switch como la diferencia entre if
y el operador ternario. Ambos verifican la condición lógica y realizan ramificaciones dependiendo de su resultado.
La diferencia es que if
solo ejecuta el bloque correspondiente, mientras que el operador ternario devuelve algún resultado:
if(condition) { result = doThis(); } else { result = doThat(); } result = condition ? doThis() : doThat();
Lo mismo es para el interruptor : antes de Java 12, si desea calcular el valor y guardar el resultado, puede asignarlo a una variable (y luego break
), o devolverlo desde un método creado específicamente para la switch
.
Ahora, se evalúa la expresión completa de la instrucción switch (se selecciona la rama correspondiente para la ejecución) y el resultado de los cálculos se puede asignar a una variable.
Otra diferencia entre la expresión y la declaración es que la declaración de cambio , debido a que es parte de la declaración, debe terminar con un punto y coma, a diferencia de la declaración de cambio clásica.
Flecha o dos puntos
El ejemplo introductorio utilizó la nueva sintaxis de estilo lambda con una flecha entre la etiqueta y la parte en ejecución. Es importante comprender que para esto no es necesario usar switch
como expresión. De hecho, el siguiente ejemplo es equivalente al código dado al comienzo del artículo:
boolean result = switch(ternaryBool) { case TRUE: break true; case FALSE: break false; case FILE_NOT_FOUND: throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); default: throw new IllegalArgumentException("Seriously?!!?"); };
¡Tenga en cuenta que ahora puede usar break
con un valor! Esto encaja perfectamente con las switch
estilo antiguo que usan break
sin ningún significado. Entonces, en qué caso una flecha significa una expresión en lugar de un operador, ¿por qué está aquí? ¿Solo sintaxis hipster?
Históricamente, las marcas de dos puntos simplemente marcan el punto de entrada al bloque de instrucciones. A partir de este punto, comienza la ejecución de todo el código siguiente, incluso cuando se encuentra otra etiqueta. En switch
sabemos que esto pasa al siguiente case
(fall-through): la etiqueta del case
determina hacia dónde salta el flujo de control. Para completarlo, necesita un break
o return
.
A su vez, usar la flecha significa que solo se ejecutará el bloque a la derecha. Y no "fallar".
Más sobre la evolución del interruptor
Múltiples etiquetas en caso
Hasta ahora, cada case
una sola etiqueta. Pero ahora todo ha cambiado: un case
puede corresponder a varias etiquetas:
String result = switch(ternaryBool) { case TRUE, FALSE -> "sane";
El comportamiento debe ser obvio: TRUE
y FALSE
producen el mismo resultado: se evalúa la expresión "cuerda".
Esta es una innovación bastante agradable que reemplazó el uso múltiple de case
cuando se requería implementar una transición de paso al siguiente case
.
Tipos fuera de Enum
Todos los ejemplos de switch
en este artículo usan enum
. ¿Qué hay de otros tipos? Las expresiones y las switch
también pueden funcionar con String
, int
, (consulte la documentación ) short
, byte
, char
y sus contenedores. Hasta ahora, nada ha cambiado aquí, aunque la idea de usar tipos de datos como float
y long
todavía es válida (del segundo al último párrafo).
Más sobre la flecha
Veamos dos propiedades específicas de la forma de flecha de un registro separador:
- falta de una transición a través del siguiente
case
; - bloques de operadores.
No pasa al siguiente caso
Esto es lo que dice JEP 325 sobre esto:
El diseño actual de la switch
en Java está estrechamente relacionado con lenguajes como C y C ++ y admite la semántica de extremo a extremo de forma predeterminada. Aunque esta forma tradicional de control a menudo es útil para escribir código de bajo nivel (como analizadores para codificación binaria), dado que el switch
usa en código de nivel superior, los errores de este enfoque comienzan a superar su flexibilidad.
Estoy totalmente de acuerdo y agradezco la oportunidad de usar el interruptor sin un comportamiento predeterminado:
switch(ternaryBool) { case TRUE, FALSE -> System.out.println("Bool was sane");
Es importante saber que esto no tiene nada que ver con el uso de switch como expresión o declaración. El factor decisivo aquí es la flecha contra el colon.
Bloques de operador
Como en el caso de lambdas, la flecha puede apuntar a un operador (como arriba) o a un bloque resaltado con llaves:
boolean result = switch(Bool.random()) { case TRUE -> { System.out.println("Bool true");
Los bloques que deben crearse para operadores de varias líneas tienen una ventaja adicional (que no se requiere cuando se usan dos puntos), lo que significa que para usar los mismos nombres de variables en diferentes ramas, el switch
no requiere un procesamiento especial.
Si le parecía inusual salir de los bloques utilizando el break
lugar del return
, entonces no se preocupe, esto también me desconcertó y me pareció extraño. Pero luego lo pensé y llegué a la conclusión de que tiene sentido, ya que conserva el viejo estilo de la construcción del switch
, que usa break
sin valores.
Obtenga más información sobre las declaraciones de cambio
Y por último, pero no menos importante, los detalles del uso de switch
como expresión:
- múltiples expresiones;
- retorno temprano (
return
temprano); - cobertura de todos los valores.
¡Tenga en cuenta que no importa qué forma se use!
Expresiones múltiples
Las expresiones de interruptor son expresiones múltiples. Esto significa que no tienen su propio tipo, pero pueden ser uno de varios tipos. Muy a menudo, las expresiones lambda se usan como tales expresiones: s -> s + " "
, puede ser Function<String, String>
, pero también puede ser Function<Serializable, Object>
o UnaryOperator<String>
.
Usando expresiones de cambio, un tipo está determinado por la interacción entre el lugar donde se usa el cambio y los tipos de sus ramas. Si se asigna una expresión de cambio a una variable con tipo, se pasa como argumento o se usa de otro modo en un contexto donde se conoce el tipo exacto (esto se llama el tipo de destino), entonces todas sus ramas deben coincidir con ese tipo. Esto es lo que hemos hecho hasta ahora:
String result = switch (ternaryBool) { case TRUE, FALSE -> "sane"; default -> "insane"; };
Como resultado, el switch
asigna a la variable de result
de tipo String
. Por lo tanto, String
es el tipo de destino, y todas las ramas deben devolver un resultado de tipo String
.
Lo mismo sucede aquí:
Serializable serializableMessage = switch (bool) { case TRUE, FALSE -> "sane";
¿Qué pasará ahora?
(Para usar el tipo var, lea en nuestro último artículo 26 las recomendaciones para usar el tipo var en Java - nota del traductor)
Si el tipo de destino es desconocido, debido al hecho de que usamos var, el tipo se calcula al encontrar el supertipo más específico de los tipos creados por las ramas.
Regreso temprano
La consecuencia de la diferencia entre la expresión y la switch
es que puede usar return
para salir de la switch
:
public String sanity(Bool ternaryBool) { switch (ternaryBool) {
... no puedes usar return
dentro de una expresión ...
public String sanity(Bool ternaryBool) { String result = switch (ternaryBool) {
Esto tiene sentido si usa una flecha o dos puntos.
Cubriendo todas las opciones
Si utiliza switch
como operador, no importa si todas las opciones están cubiertas o no. Por supuesto, puede omitir accidentalmente el case
, y el código no funcionará correctamente, pero al compilador no le importa: usted, su IDE y sus herramientas de análisis de código se quedarán solo con esto.
Las expresiones de cambio exacerban este problema. ¿A dónde debería ir si falta la etiqueta deseada? La única respuesta que Java puede dar es devolver null
para los tipos de referencia y un valor predeterminado para las primitivas. Esto causaría muchos errores en el código principal.
Para evitar tal resultado, el compilador puede ayudarlo. Para las declaraciones de cambio, el compilador insistirá en que todas las opciones posibles estén cubiertas. Veamos un ejemplo que podría conducir a un error de compilación:
La siguiente solución es interesante: agregar la rama default
ciertamente solucionará el error, pero esta no es la única solución, aún puede agregar un case
para FALSE
.
Sí, el compilador finalmente podrá determinar si todos los valores de enumeración están cubiertos (si todas las opciones están agotadas) y no establecer valores predeterminados inútiles. Sentémonos un momento en silenciosa gratitud.
Aunque, esto todavía plantea una pregunta. ¿Qué pasa si alguien toma y convierte un Bool loco en un Booleano cuaternario agregando un cuarto valor? Si vuelve a compilar la expresión de cambio para el Bool extendido, obtendrá un error de compilación (la expresión ya no es exhaustiva). Sin recompilación, esto se convertirá en un problema de tiempo de ejecución. Para detectar este problema, el compilador va a la rama default
, que se comporta igual que la que usamos hasta ahora, arrojando una excepción.
En Java 12, abarcar todos los valores sin la rama default
solo funciona para enum
, pero cuando el switch
vuelve más poderoso en futuras versiones de Java, también puede funcionar con tipos arbitrarios. Si las etiquetas de los case
no solo pueden verificar la igualdad, sino también hacer comparaciones (por ejemplo, _ <5 -> ...), esto cubrirá todas las opciones para los tipos numéricos.
Pensando
Aprendimos del artículo que Java 12 convierte un switch
en una expresión, dándole nuevas características:
- ahora un
case
puede corresponder a varias etiquetas; - El nuevo
case … -> …
forma de flecha case … -> …
sigue la sintaxis de las expresiones lambda:
- se permiten operadores o bloques de una sola línea;
case
impide pasar al siguiente case
;
- ahora toda la expresión se evalúa como un valor, que luego se puede asignar a una variable o pasar como parte de una declaración más grande;
- expresión múltiple: si se conoce el tipo de destino, todas las ramas deben corresponderle. De lo contrario, se define un tipo específico que coincide con todas las ramas;
break
puede devolver un valor de un bloque;- para una expresión de
switch
usando enum
, el compilador verifica el alcance de todos sus valores. Si el default
ausente, se agrega una rama que genera una excepción.
¿A dónde nos llevará? Primero, dado que esta no es la versión final de switch
, todavía tiene tiempo para dejar comentarios en la lista de correo de Amber si no está de acuerdo con algo.
Luego, suponiendo que el interruptor permanezca como está en este momento, creo que la forma de la flecha se convertirá en la nueva opción predeterminada. Sin un pasaje directo al siguiente case
y con expresiones concisas lambda (es muy natural tener un caso y una declaración en una línea), el switch
ve mucho más compacto y no afecta la legibilidad del código. Estoy seguro de que solo usaré dos puntos si necesito pasar por el pasaje.
Que piensas ¿Satisfecho con cómo resultaron las cosas?