Probar el operador mejorado instanceof en Java 14

No muy lejos está la nueva versión 14 de Java, lo que significa que es hora de ver qué nuevas características de sintaxis contendrá esta versión de Java. Una de estas posibilidades sintácticas es la coincidencia de patrones del tipo que se implementará utilizando la instanceof mejorada (extendida) del operador.

Hoy me gustaría jugar con este nuevo operador y considerar las características de su trabajo con más detalle. Como la coincidencia de patrones por tipo aún no ha entrado en el repositorio principal de JDK, tuve que descargar el repositorio del proyecto Amber , que está desarrollando nuevas construcciones de sintaxis Java, y compilar el JDK desde este repositorio.

Entonces, lo primero que haremos es verificar la versión de Java para asegurarnos de que realmente usamos JDK 14:

 > java -version openjdk version "14-internal" 2020-03-17 OpenJDK Runtime Environment (build 14-internal+0-adhoc.osboxes.amber-amber) OpenJDK 64-Bit Server VM (build 14-internal+0-adhoc.osboxes.amber-amber, mixed mode, sharing) 

Eso es correcto

Ahora escribiremos un pequeño fragmento de código con la instanceof "antigua" del operador y lo ejecutaremos:

 public class A { public static void main(String[] args) { new A().f("Hello, world!"); } public void f(Object obj) { if (obj instanceof String) { String str = (String) obj; System.out.println(str.toLowerCase()); } } } 

 > java A.java hello, world! 

Funciona Esta es una verificación de tipo estándar seguida de un reparto. Escribimos construcciones similares todos los días, sin importar qué versión de Java usemos, al menos 1.0, al menos 13.
Pero ahora tenemos Java 14 en nuestras manos y reescribamos el código usando el operador mejorado instanceof (omitiré líneas de código repetidas en el futuro):

 if (obj instanceof String str) { System.out.println(str.toLowerCase()); } 

 > java --enable-preview --source 14 A.java hello, world! 

Genial El código es más limpio, más corto, más seguro y más legible. Hubo tres repeticiones de la palabra Cadena, una se convirtió. Tenga en cuenta que no olvidamos especificar los argumentos --enable-preview --source 14 , como El nuevo operador es una función de vista previa . Además, un lector atento probablemente notó que ejecutamos el archivo fuente A.java directamente, sin compilación. Esta característica apareció en Java 11.

Intentemos escribir algo más sofisticado y agreguemos una segunda condición que use la variable recién declarada:

 if (obj instanceof String str && str.length() > 5) { System.out.println(str.toLowerCase()); } 

Se compila y funciona. ¿Pero qué pasa si cambias las condiciones?

 if (str.length() > 5 && obj instanceof String str) { System.out.println(str.toLowerCase()); } 

 A.java:7: error: cannot find symbol if (str.length() > 5 && obj instanceof String str) { ^ 

Error de compilación Lo que es de esperar: la variable str aún no se ha declarado, lo que significa que no se puede usar.

Por cierto, ¿qué pasa con la mutabilidad? ¿La variable es final o no? Intentamos:

 if (obj instanceof String str) { str = "World, hello!"; System.out.println(str.toLowerCase()); } 

 A.java:8: error: pattern binding str may not be assigned str = "World, hello!"; ^ 

Sí, la variable final. Esto significa que la palabra "variable" no es del todo correcta aquí. Y el compilador utiliza el término especial "enlace de patrón". Por lo tanto, propongo de ahora en adelante no decir "variable", sino "enlace de patrón" (desafortunadamente, la palabra "enlace" no está muy bien traducida al ruso).

Con mutabilidad y terminología resueltas. Vamos a experimentar más. ¿Qué pasa si logramos "romper" el compilador?

¿Qué sucede si nombra una variable y un enlace de patrón con el mismo nombre?

 if (obj instanceof String obj) { System.out.println(obj.toLowerCase()); } 

 A.java:7: error: variable obj is already defined in method f(Object) if (obj instanceof String obj) { ^ 

Es lógico. La superposición de una variable desde el ámbito externo no funciona. Esto es equivalente a como si obj la variable obj segunda vez en el mismo alcance.

Y si es así:

 if (obj instanceof String str && obj instanceof String str) { System.out.println(str.toLowerCase()); } 

 A.java:7: error: illegal attempt to redefine an existing match binding if (obj instanceof String str && obj instanceof String str) { ^ 

El compilador es tan sólido como el hormigón.

¿Qué más puedes probar? Juguemos con los ámbitos. Si el enlace se define en la rama if , ¿se definirá en la rama else si la condición se invierte?

 if (!(obj instanceof String str)) { System.out.println("not a string"); } else { System.out.println(str.toLowerCase()); } 

Funcionó. El compilador no solo es confiable, sino también inteligente.

Y si es asi?

 if (obj instanceof String str && true) { System.out.println(str.toLowerCase()); } 

Funcionó de nuevo. El compilador entiende correctamente que la condición se reduce a una simple obj instanceof String str .

¿Realmente no es posible "romper" el compilador?

Tal vez?

 if (obj instanceof String str || false) { System.out.println(str.toLowerCase()); } 

 A.java:8: error: cannot find symbol System.out.println(str.toLowerCase()); ^ 

Si! Esto ya parece un error. Después de todo, las tres condiciones son absolutamente equivalentes:

  • obj instanceof String str
  • obj instanceof String str && true
  • obj instanceof String str || false

Las reglas de alcance del flujo, por otro lado, son bastante no triviales , y tal vez tal caso realmente no debería funcionar. Pero si miras puramente desde un punto de vista humano, entonces creo que esto es un error.

Pero vamos, intentemos otra cosa. ¿Funcionará esto?

 if (!(obj instanceof String str)) { throw new RuntimeException(); } System.out.println(str.toLowerCase()); 

Compilado Esto es bueno, ya que este código es equivalente al siguiente:
 if (!(obj instanceof String str)) { throw new RuntimeException(); } else { System.out.println(str.toLowerCase()); } 

Y dado que ambas opciones son equivalentes, el programador espera que funcionen de la misma manera.

¿Qué pasa con los campos superpuestos?

 public class A { private String str; public void f(Object obj) { if (obj instanceof String str) { System.out.println(str.toLowerCase()); } else { System.out.println(str.toLowerCase()); } } } 

El compilador no juró. Esto es lógico, porque las variables locales siempre pueden superponerse a los campos. Aparentemente, también decidieron no hacer excepciones para los enlaces de patrones. Por otro lado, dicho código es bastante frágil. Un movimiento descuidado, y es posible que no note cómo se rompió su rama:

 private boolean isOK() { return false; } public void f(Object obj) { if (obj instanceof String str || isOK()) { System.out.println(str.toLowerCase()); } else { System.out.println(str.toLowerCase()); } } 

Ambas ramas ahora usan el campo str , que un programador desatento podría no esperar. Para detectar dichos errores lo antes posible, use las inspecciones en el IDE y diferentes resaltados de sintaxis para campos y variables. También recomiendo que siempre use el calificador this para los campos. Esto agregará aún más confiabilidad.

¿Qué más es interesante? Al igual que la "antigua" instanceof , la nueva nunca coincide con null . Esto significa que siempre puede confiar en el hecho de que las carpetas de patrones nunca pueden ser null :

 if (obj instanceof String str) { System.out.println(str.toLowerCase()); //    NullPointerException } 

Por cierto, usando esta propiedad, puede acortar tales cadenas:

 if (a != null) { B b = a.getB(); if (b != null) { C c = b.getC(); if (c != null) { System.out.println(c.getSize()); } } } 

Si usa instanceof , el código anterior se puede reescribir así:

 if (a != null && a.getB() instanceof B b && b.getC() instanceof C c) { System.out.println(c.getSize()); } 

Escribe en los comentarios lo que piensas sobre este estilo. ¿Usarías este idioma?

¿Qué pasa con los genéricos?

 import java.util.List; public class A { public static void main(String[] args) { new A().f(List.of(1, 2, 3)); } public void f(Object obj) { if (obj instanceof List<Integer> list) { System.out.println(list.size()); } } } 

 > java --enable-preview --source 14 A.java Note: A.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. 3 

Muy interesante Si la instanceof "antigua" solo admite la instanceof List o la instanceof List<?> , Entonces la nueva funciona con cualquier tipo en particular. Estamos esperando que la primera persona caiga en esa trampa:

 if (obj instanceof List<Integer> list) { System.out.println("Int list of size " + list.size()); } else if (obj instanceof List<String> list) { System.out.println("String list of size " + list.size()); } 

¿Por qué esto no funciona?
Respuesta: falta de genéricos reificados en Java.

En mi humilde opinión, este es un problema bastante grave. Por otro lado, no sé cómo solucionarlo. Parece que debe confiar en las inspecciones en el IDE nuevamente.

Conclusiones


En general, el nuevo tipo de coincidencia de patrones funciona muy bien. La instanceof mejorada del operador le permite hacer no solo una prueba de tipo, sino también declarar aglutinantes prefabricados de este tipo, eliminando la necesidad de fundición manual. Esto significa que habrá menos ruido en el código, y será mucho más fácil para el lector discernir la lógica útil. Por ejemplo, la mayoría de las implementaciones de equals() se pueden escribir en una línea:

 public class Point { private final int x, y; … @Override public int hashCode() { return Objects.hash(x, y); } @Override public boolean equals(Object obj) { return obj instanceof Point p && px == this.x && py == this.y; } } 

El código anterior se puede escribir incluso más corto. Como?
Usando entradas que también se incluirán en Java 14. Hablaremos de ellas la próxima vez.

Por otro lado, varios puntos controvertidos plantean pequeñas preguntas:

  • Reglas de alcance no completamente transparentes (ejemplo con instanceof || false ).
  • Campos superpuestos.
  • instanceof y genéricos.

Sin embargo, estos son más insignificantes que los reclamos serios. Con todo, los enormes beneficios de la nueva instanceof operador definitivamente valen la pena agregar su lenguaje. Y si aún deja el estado de vista previa y se convierte en una sintaxis estable, será una gran motivación dejar finalmente Java 8 a la nueva versión de Java.

PD: Tengo un canal en Telegram donde escribo sobre noticias de Java. Te insto a suscribirte.

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


All Articles