Borrado de tipo de generics Java: ¿cuándo y qué sucede?

Leí sobre el borrado de tipos de Java en el sitio web de Oracle .

¿Cuándo ocurre el borrado de tipos? En tiempo de comstackción o tiempo de ejecución? Cuando la clase está cargada? Cuando la clase es instanciada?

Muchos sitios (incluido el tutorial oficial mencionado anteriormente) dicen que el borrado de tipos ocurre en tiempo de comstackción. Si la información de tipo se elimina por completo en el momento de la comstackción, ¿cómo comprueba el JDK la compatibilidad del tipo cuando se invoca un método que utiliza generics sin información de tipo o información de tipo incorrecto?

Considere el siguiente ejemplo: Say clase A tiene un método, empty(Box b) . A.java y obtenemos el archivo de clase A.class .

 public class A { public static void empty(Box b) {} } 
 public class Box {} 

Ahora creamos otra clase B que invoca el método empty con un argumento no parametrizado (tipo crudo): empty(new Box()) . Si B.java con A.class en classpath, javac es lo suficientemente inteligente como para generar una advertencia. Entonces A.class tiene algún tipo de información almacenada en él.

 public class B { public static void invoke() { // java: unchecked method invocation: // method empty in class A is applied to given types // required: Box // found: Box // java: unchecked conversion // required: Box // found: Box A.empty(new Box()); } } 

Creo que el borrado de tipos ocurre cuando se carga la clase, pero es solo una suposición. Entonces, ¿cuándo sucede?

La borradura de tipo se aplica al uso de generics. Definitivamente hay metadatos en el archivo de la clase para decir si un método / tipo es genérico, y cuáles son las restricciones, etc. Pero cuando se usan generics, se convierten en verificaciones en tiempo de comstackción y conversiones en tiempo de ejecución. Entonces este código:

 List list = new ArrayList(); list.add("Hi"); String x = list.get(0); 

está comstackdo en

 List list = new ArrayList(); list.add("Hi"); String x = (String) list.get(0); 

En el momento de la ejecución, no hay forma de descubrir que T=String para el objeto de la lista: esa información se ha ido.

… pero la interfaz List aún se anuncia como genérica.

EDITAR: solo para aclarar, el comstackdor conserva la información acerca de que la variable es una List – pero aún no puede descubrir que T=String para el objeto de la lista misma.

El comstackdor es responsable de comprender generics en tiempo de comstackción. El comstackdor también es responsable de desechar esta “comprensión” de las clases genéricas, en un proceso que llamamos borrado de tipos . Todo sucede en tiempo de comstackción.

Nota: Contrariamente a las creencias de la mayoría de los desarrolladores de Java, es posible mantener la información del tipo de tiempo de comstackción y recuperar esta información en tiempo de ejecución, a pesar de que es muy restringida. En otras palabras: Java proporciona generics reificados de una manera muy restringida .

En cuanto a la borradura de tipo

Tenga en cuenta que, en tiempo de comstackción, el comstackdor tiene información de tipo completo disponible, pero esta información se descarta intencionalmente en general cuando se genera el código de bytes, en un proceso conocido como borrado de tipo . Esto se hace de esta manera debido a problemas de compatibilidad: la intención de los diseñadores de idiomas era proporcionar compatibilidad completa con el código fuente y compatibilidad total con el código de bytes entre las versiones de la plataforma. Si se implementara de manera diferente, tendría que recomstackr sus aplicaciones heredadas al migrar a versiones más nuevas de la plataforma. De la forma en que se hizo, todas las firmas de métodos se conservan (compatibilidad con el código fuente) y no es necesario volver a comstackr nada (compatibilidad binaria).

En cuanto a los generics reificados en Java

Si necesita mantener la información del tipo de tiempo de comstackción, necesita emplear clases anónimas. El punto es: en el caso muy especial de las clases anónimas, es posible recuperar la información completa del tipo de tiempo de comstackción en tiempo de ejecución que, en otras palabras, significa: generics reificados. Esto significa que el comstackdor no descarta la información de tipo cuando se trata de clases anónimas; esta información se mantiene en el código binario generado y el sistema de tiempo de ejecución le permite recuperar esta información.

He escrito un artículo sobre este tema:

http://rgomes-info.blogspot.co.uk/2013/12/using-typetokens-to-retrieve-generic.html

Una nota sobre la técnica descrita en el artículo anterior es que la técnica es oscura para la mayoría de los desarrolladores. A pesar de que funciona y funciona bien, la mayoría de los desarrolladores se sienten confundidos o incómodos con la técnica. Si tiene una base de código compartido o plan para lanzar su código al público, no recomiendo la técnica anterior. Por otro lado, si usted es el único usuario de su código, puede aprovechar la potencia que esta técnica le brinda.

Código de muestra

El artículo anterior tiene enlaces al código de muestra.

Si tiene un campo que es un tipo genérico, sus parámetros de tipo se comstackn en la clase.

Si tiene un método que toma o devuelve un tipo genérico, esos parámetros de tipo se comstackn en la clase.

Esta información es lo que el comstackdor usa para decirle que no puede pasar un Box al método empty(Box) .

La API es complicada, pero puede inspeccionar esta información de tipo a través de la API de reflexión con métodos como getGenericParameterTypes , getGenericReturnType y, para los campos, getGenericType .

Si tiene un código que usa un tipo genérico, el comstackdor inserta moldes según sea necesario (en la persona que llama) para verificar los tipos. Los objetos generics en sí mismos son solo el tipo crudo; el tipo parametrizado es “borrado”. Entonces, cuando creas un new Box() , no hay información sobre la clase Integer en el objeto Box .

Las preguntas frecuentes de Angelika Langer son la mejor referencia que he visto para Java Generics.

Generics in Java Language es una muy buena guía sobre este tema.

Los generics son implementados por el comstackdor de Java como una conversión front-end llamada borrado. Usted puede (casi) pensar en esto como una traducción de fuente a fuente, mediante la cual la versión genérica de loophole() se convierte a la versión no genérica.

Por lo tanto, está en tiempo de comstackción. La JVM nunca sabrá qué ArrayList usó.

También recomendaría la respuesta del Sr. Skeet sobre ¿Cuál es el concepto de borrado en generics en Java?

La borradura de tipo se produce en el momento de la comstackción. El tipo de borrado significa que se olvidará del tipo genérico, no de todo tipo. Además, todavía habrá metadatos sobre los tipos que son generics. Por ejemplo

 Box b = new Box(); String x = b.getDefault(); 

se convierte a

 Box b = new Box(); String x = (String) b.getDefault(); 

en tiempo de comstackción Puede recibir advertencias no porque el comstackdor sepa de qué tipo es el genérico, sino por el contrario, porque no sabe lo suficiente, por lo que no puede garantizar la seguridad del tipo.

Además, el comstackdor conserva la información de tipo sobre los parámetros en una llamada a método, que puede recuperar a través de la reflexión.

Esta guía es la mejor que he encontrado sobre el tema.

Me he encontrado con borrado de tipo en Android. En producción utilizamos la opción gradle con minify. Después de la minificación, tengo una excepción fatal. He hecho una función simple para mostrar la cadena de herencia de mi objeto:

 public static void printSuperclasses(Class clazz) { Type superClass = clazz.getGenericSuperclass(); Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName())); Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString())); while (superClass != null && clazz != null) { clazz = clazz.getSuperclass(); superClass = clazz.getGenericSuperclass(); Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName())); Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString())); } } 

Y hay dos resultados de esta función:

Código no minificado:

 D/Reflection: this class: com.example.App.UsersList D/Reflection: superClass: com.example.App.SortedListWrapper D/Reflection: this class: com.example.App.SortedListWrapper D/Reflection: superClass: android.support.v7.util.SortedList$Callback D/Reflection: this class: android.support.v7.util.SortedList$Callback D/Reflection: superClass: class java.lang.Object D/Reflection: this class: java.lang.Object D/Reflection: superClass: null 

Código Minificado:

 D/Reflection: this class: com.example.App.UsersList D/Reflection: superClass: class com.example.App.SortedListWrapper D/Reflection: this class: com.example.App.SortedListWrapper D/Reflection: superClass: class android.support.v7.ge D/Reflection: this class: android.support.v7.ge D/Reflection: superClass: class java.lang.Object D/Reflection: this class: java.lang.Object D/Reflection: superClass: null 

Por lo tanto, en el código minificado, las clases parametrizadas se reemplazan por tipos de clases sin ningún tipo de información. Como solución para mi proyecto, eliminé todas las llamadas de reflexión y las repliqué con tipos de params explícitos pasados ​​en argumentos de función.

El término “borrado de tipo” no es realmente la descripción correcta del problema de Java con los generics. La borradura de tipo no es per se una mala cosa, de hecho es muy necesaria para el rendimiento y se usa a menudo en varios lenguajes como C ++, Haskell, D.

Antes de disgustar, recuerde la definición correcta de borrado de tipo de Wiki

¿Qué es borrado de tipo?

tipo borrado se refiere al proceso de tiempo de carga mediante el cual se eliminan las anotaciones de tipo explícito de un progtwig, antes de que se ejecute en tiempo de ejecución

La borradura de tipo significa tirar las tags de tipo creadas en el tiempo de diseño o las tags de tipo inferido en el momento de la comstackción de modo que el progtwig comstackdo en el código binario no contenga ninguna etiqueta de tipo. Y este es el caso de cada lenguaje de progtwigción que comstack código binario, excepto en algunos casos donde necesita tags de tiempo de ejecución. Estas excepciones incluyen, por ejemplo, todos los tipos existenciales (tipos de referencia de Java que son subtipos, cualquier tipo en muchos idiomas, tipos de unión). La razón del borrado de tipos es que los progtwigs se transforman a un lenguaje que está en cierto modo uni-tipeado (el lenguaje binario solo permite bits) ya que los tipos son solo abstracciones y afirman una estructura para sus valores y la semántica apropiada para manejarlos.

Así que esto es a cambio, una cosa natural normal.

El problema de Java es diferente y se debe a la forma en que se corrige.

Las declaraciones a menudo hechas sobre Java no tienen generics reificados también es incorrecto.

Java se reifica, pero de forma incorrecta debido a la compatibilidad con versiones anteriores.

¿Qué es la reificación?

De nuestra Wiki

La reificación es el proceso mediante el cual una idea abstracta sobre un progtwig de computadora se convierte en un modelo de datos explícito u otro objeto creado en un lenguaje de progtwigción.

Reificación significa convertir algo abstracto (tipo paramétrico) en algo concreto (tipo concreto) por especialización.

Ilustramos esto con un simple ejemplo:

Una ArrayList con definición:

 ArrayList { T[] elems; ...//methods } 

es una abstracción, en detalle un constructor de tipos, que se “reifica” cuando se especializa con un tipo concreto, por ejemplo entero:

 ArrayList { Integer[] elems; } 

donde ArrayList es realmente un tipo.

¡Pero esto es exactamente lo que Java no hace! , en cambio, reifican constantemente los tipos abstractos con sus límites, es decir, producen el mismo tipo concreto independientemente de los parámetros pasados ​​para la especialización:

 ArrayList { Object[] elems; } 

que aquí se reifica con el Objeto enlazado implícito ( ArrayList == ArrayList ).

A pesar de eso, hace que las matrices genéricas sean inutilizables y causan algunos errores extraños para los tipos sin formato:

 List l= List.of("h","s"); List lRaw=l l.add(new Object()) String s=l.get(2) //Cast Exception 

causa muchas ambigüedades como

 void function(ArrayList list){} void function(ArrayList list){} void function(ArrayList list){} 

se refieren a la misma función:

 void function(ArrayList list) 

y, por lo tanto, la sobrecarga de métodos generics no se puede usar en Java.