Reemplazar objeto con var: ¿qué podría salir mal?

Recientemente me encontré con una situación en la que reemplazar Object por var en un programa Java 10 arroja una excepción en tiempo de ejecución. Me preguntaba cuántas formas diferentes de lograr este efecto, y abordé este problema a la comunidad:



Resultó que puedes lograr el efecto de diferentes maneras. Aunque todos son un poco complicados, es interesante recordar las diversas sutilezas del lenguaje con el ejemplo de tal tarea. Veamos qué métodos se encontraron.


Miembros


Entre los encuestados había muchas personas famosas y no muy personas. Este es Sergey bsideup Egorov, empleado fundamental , orador, uno de los creadores de Testcontainers. Este es Victor Polishchuk , famoso por los informes sobre la sangrienta empresa. También señaló Nikita Artyushov de Google; Dmitry Mikhailov y Maccimo . Pero estuve especialmente encantado con la llegada de Wouter Coekaerts . Es conocido por su artículo el año pasado , donde caminó a través del sistema de tipos Java y contó cuán irremediablemente estaba roto. Parte de este artículo, jbaruch y yo incluso lo utilizamos en la cuarta versión de Java Puzzlers .


Tarea y soluciones


Entonces, la esencia de nuestra tarea es esta: hay un programa Java en el que hay una declaración de una variable de la forma Object x = ... (honesto estándar java.lang.Object , sin sustituciones de tipo). El programa compila, ejecuta e imprime algo como "Ok". Reemplazamos Object con var , lo que requiere inferencia de tipo automática, después de lo cual el programa continúa compilando, pero se bloquea al iniciarse con una excepción.


Las soluciones se pueden dividir aproximadamente en dos grupos. En el primero, después de reemplazar con var, la variable se vuelve primitiva (es decir, originalmente era autoboxing). El segundo tipo sigue siendo objeto, pero más específico que Object . Aquí puede resaltar un subgrupo interesante que usa genéricos.


Boxeo


¿Cómo distinguir un objeto de un primitivo? Hay muchas formas diferentes. Lo más fácil es verificar la identidad. Esta solución fue propuesta por Nikita:


 Object x = 1000; if (x == new Integer(1000)) throw new Error(); System.out.println("Ok"); 

Cuando x es un objeto, ciertamente no puede ser igual en referencia al nuevo objeto new Integer(1000) . Y si es un primitivo, de acuerdo con las reglas del lenguaje, el new Integer(1000) se despliega inmediatamente en un primitivo también, y los números se comparan como primitivos.


Otra forma son los métodos sobrecargados. Puede escribir el suyo, pero Sergey ideó una opción más elegante: usar la biblioteca estándar. El método List.remove es List.remove , que está sobrecargado y puede eliminar un elemento por índice si se pasa una primitiva o un elemento por valor si se pasa un objeto. Esto ha llevado repetidamente a errores en programas reales si usa List<Integer> . Para nuestra tarea, la solución puede verse así:


 Object x = 1000; List<?> list = new ArrayList<>(); list.remove(x); System.out.println("Ok"); 

Ahora estamos tratando de eliminar el elemento inexistente 1000 de la lista vacía, esto es solo una acción inútil. Pero si reemplazamos Object por var , llamaremos a otro método que elimine el elemento con el índice 1000. Y esto ya conduce a IndexOutOfBoundsException .


El tercer método es el operador de conversión de tipo. Podemos convertir con éxito otro primitivo en un tipo primitivo, pero un objeto se convierte solo si hay un contenedor sobre el mismo tipo al que convertiremos (entonces ocurrirá un encajonamiento). En realidad, necesitamos el efecto contrario: una excepción es en el caso de un primitivo, y no en el caso de un objeto, pero usando try-catch esto es fácil de lograr, lo que Viktor usó:


 Object x = 40; try { throw new Error("Oops :" + (char)x); } catch (ClassCastException e) { System.out.println("Ok"); } 

Aquí, ClassCastException es el comportamiento esperado, luego el programa sale normalmente. Pero después de usar var esta excepción desaparece y arrojamos algo más. Me pregunto si esto está inspirado en el código real de la empresa sangrienta? ..


Wouter propuso otra opción de conversión de tipo. Puedes usar la extraña lógica del operador ?: . Es cierto que su código solo da resultados diferentes, por lo que debe modificarlo de alguna manera para que haya una excepción. Entonces, me parece, con bastante elegancia:


 Object x = 1.0; System.out.println(String.valueOf(false ? x : 100000000000L).substring(12) + "Ok"); 

La diferencia entre este método es que no usamos el valor de x directamente, pero ¿el tipo x afecta el tipo de expresión false ? x : 100000000000L false ? x : 100000000000L . Si x es un Object , entonces el tipo de la expresión completa es Object , y luego solo tenemos el boxeo, String.valueOf() cadena de 100000000000 , para la cual la substring(12) es una cadena vacía. Si usa var , entonces el tipo x convierte en double y, por lo tanto, el tipo false ? x : 100000000000L false ? x : 100000000000L también false ? x : 100000000000L double , es decir, 100000000000L se convertirá en 1.0E11 , donde tiene mucho menos de 12 caracteres, por lo que llamar a la substring conduce a una StringIndexOutOfBoundsException .


Finalmente, aprovechamos el hecho de que una variable se puede cambiar después de la creación. Y en la variable de objeto, a diferencia de la primitiva, puede poner null . Poner null en una variable es fácil; hay muchas formas. Pero aquí, Wouter también adoptó un enfoque creativo utilizando el ridículo método Integer.getInteger :


 Object x = 1; x = Integer.getInteger("moo"); System.out.println("Ok"); 

No todos saben que este método lee una propiedad del sistema llamada moo y, si hay una, intenta convertirla en un número, de lo contrario, devuelve null . Si no hay ninguna propiedad, asignamos silenciosamente null al objeto, pero caemos de NullPointerException cuando intentamos asignarlo a una primitiva (el anboxing automático ocurre allí). Podría haber sido más fácil, por supuesto. Versión aproximada x = null; no se arrastra, no se compila, pero el compilador lo tragará ahora:


 Object x = 1; x = (Integer)null; System.out.println("Ok"); 

Tipo de objeto


Supongamos que ya no puedes jugar con primitivos. ¿Qué más se te ocurre?


Bueno, en primer lugar, el método de sobrecarga de método más simple propuesto por Dmitry:


 public static void main(String[] args) { Object x = "Ok"; sayWhat(x); } static void sayWhat(Object x) { System.out.println(x); } static void sayWhat(String x) { throw new Error(); } 

La vinculación de métodos sobrecargados en Java ocurre estáticamente, en la etapa de compilación. El método sayWhat sayWhat(Object) se sayWhat(Object) , pero si inferimos el tipo x automáticamente, String sayWhat(String) la String y, por lo tanto, se vinculará el método sayWhat(String) más específico.


Otra forma de hacer una llamada ambigua en Java es utilizando argumentos variables (varargs). Wouter recordó esto nuevamente:


 Object x = new Object[] {}; Arrays.asList(x).get(0); System.out.println("Ok"); 

Cuando el tipo de variable es Object , el compilador piensa que es un argumento variable y envuelve la matriz en otra matriz de un elemento, por lo que get() cumple con éxito. Si usa var , Object[] muestra el tipo Object[] y no habrá ajuste adicional. De esta manera obtenemos una lista vacía, y la llamada get() fallará.


Maccimo eligió hardcore: decidió llamar a println través de la API MethodHandle:


 Object x = "Ok"; MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual( PrintStream.class, "println", MethodType.methodType(void.class, Object.class)); mh.invokeExact(System.out, x); 

El método invokeExact y varios otros métodos del java.lang.invoke tienen la llamada "firma polimórfica". Aunque se declara como el método habitual vararg invokeExact(Object... args) , no ocurre en el empaquetado de matriz estándar. En cambio, se genera una firma en el código de bytes que coincide con los tipos de argumentos realmente pasados. El método invokeExact diseñado para la invocación súper rápida de identificadores de métodos, por lo que no realiza transformaciones de argumentos estándar como la conversión o el boxeo. Se espera que el tipo de método de manejo coincida exactamente con la firma de la llamada. Esto se verifica en tiempo de ejecución y, como en el caso de var coincidencia se rompe, obtenemos una WrongMethodTypeException .


Genéricos


Por supuesto, la parametrización de tipos puede agregar un brillo a cualquier tarea en Java. Dmitry trajo una solución similar al código que encontré inicialmente. La decisión de Dmitry es detallada, por lo que mostraré mi:


 public static void main(String[] args) { Object x = foo(new StringBuilder()); System.out.println(x); } static <T> T foo(T x) { return (T)"Ok"; } 

El tipo T se emite como StringBuilder , pero en este código no se requiere que el compilador inserte una verificación de tipo en el dial-peer en el código de bytes. Es suficiente para él que StringBuilder se pueda asignar a Object , lo que significa que todo está bien. Nadie está en contra del hecho de que el método con el valor de retorno StringBuilder realidad devolvió la cadena si de todos modos asignó el resultado a una variable de tipo Object . El compilador honestamente advierte que tienes un elenco sin control, lo que significa que se lava las manos. Sin embargo, cuando se reemplaza x con var tipo x también se muestra como StringBuilder , y ya no es posible sin la verificación de tipo, porque asignar algo más a la variable de tipo StringBuilder tiene valor. Como resultado, después de cambiar a var programa se bloquea de forma segura con una ClassCastException .


Wouter sugirió una variante de esta solución utilizando métodos estándar:


 Object o = ((List<String>)(List)List.of(1)).get(0); System.out.println("Ok"); 

Finalmente, otra opción de Wouter:


 Object x = ""; TreeSet<?> set = Stream.of(x) .collect(toCollection(() -> new TreeSet<>((a, b) -> 0))); if (set.contains(1)) { System.out.println("Ok"); } 

Aquí, dependiendo del uso de var u Object el tipo de flujo se muestra como Stream<Object> o como Stream<String> . En consecuencia, se TreeSet tipo TreeSet y el tipo lambda comparador. En el caso de var , las cadenas deben llegar al lambda, por lo que al generar una representación de tiempo de ejecución lambda, se inserta automáticamente una conversión de tipo, lo que da una ClassCastException al intentar convertir una unidad en una cadena.


En general, el resultado fue muy aburrido. Si puede encontrar métodos fundamentalmente diferentes para romper var , entonces escriba los comentarios.

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


All Articles