Guía completa para cambiar expresiones en Java 12


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()); // as we'll see in "Exhaustiveness", `default` is not necessary default -> throw new IllegalArgumentException("Seriously?!"); }; 

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; // don't forget to `break` or you're screwed! break; case FALSE: result = false; break; case FILE_NOT_FOUND: // intermediate variable for demo purposes; // wait for it... var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; default: // ... here we go: // can't declare another variable with the same name var ex2 = new IllegalArgumentException("Seriously?!"); throw ex2; } 

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()); // without default branch, the method wouldn't compile default: throw new IllegalArgumentException("Seriously?!"); } } 

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()); // as we'll see in "Exhaustiveness", `default` is not necessary default -> throw new IllegalArgumentException("Seriously?!"); }; 

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"; // `default, case FILE_NOT_FOUND -> ...` does not work // (neither does other way around), but that makes // sense because using only `default` suffices default -> "insane"; }; 

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"); // in colon-form, if `ternaryBool` is `TRUE` or `FALSE`, // we would see both messages; in arrow-form, only one // branch is executed default -> System.out.println("Bool was insane"); } 

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"); // return with `break`, not `return` break true; } case FALSE -> { System.out.println("Bool false"); break false; } case FILE_NOT_FOUND -> { var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; } default -> { var ex = new IllegalArgumentException("Seriously?!"); throw ex; } }; 

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"; // note that we don't throw the exception! // but it's `Serializable`, so it matches the target type default -> new IllegalArgumentException("insane"); }; 

¿Qué pasará ahora?


 // compiler infers super type of `String` and // `IllegalArgumentException` ~> `Serializable` var serializableMessage = switch (bool) { case TRUE, FALSE -> "sane"; // note that we don't throw the exception! default -> new IllegalArgumentException("insane"); }; 

(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) { // `return` is only possible from block case TRUE, FALSE -> { return "sane"; } default -> { return "This is ridiculous!"; } }; } 

... no puedes usar return dentro de una expresión ...


 public String sanity(Bool ternaryBool) { String result = switch (ternaryBool) { // this does not compile - error: // "return outside of enclosing switch expression" case TRUE, FALSE -> { return "sane"; } default -> { return "This is ridiculous!"; } }; } 

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:


 // compile error: // "the switch expression does not cover all possible input values" boolean result = switch (ternaryBool) { case TRUE -> true; // no case for `FALSE` case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); }; 

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 .


 // compiles without `default` branch because // all cases for `ternaryBool` are covered boolean result = switch (ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); }; 

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?

Source: https://habr.com/ru/post/443464/


All Articles