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());
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.