objeto - Borrado de tipos genéricos de Java: ¿cuándo y qué pasa?
tipo comodin java (7)
El borrado de tipo se aplica al uso de genéricos. Definitivamente hay metadatos en el archivo de clase para decir si un método / tipo es genérico y cuáles son las restricciones, etc. Pero cuando se usan los genéricos, se convierten en controles de tiempo de compilación y conversiones de tiempo de ejecución. Así que este código:
List<String> list = new ArrayList<String>();
list.add("Hi");
String x = list.get(0);
se compila 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 propia interfaz List<T>
todavía se anuncia como genérica.
EDITAR: Solo para aclarar, el compilador retiene la información acerca de que la variable es una List<String>
, pero aún no puede descubrir que T=String
para el objeto de la lista en sí.
Leí sobre el borrado de tipos de Java en el sitio web de Oracle .
¿Cuándo se produce el borrado de tipo? ¿En tiempo de compilación o tiempo de ejecución? ¿Cuándo se carga la clase? Cuando la clase es instanciada?
Muchos sitios (incluido el tutorial oficial mencionado anteriormente) dicen que el borrado de tipos se produce en tiempo de compilación. Si la información de tipo se elimina por completo en el momento de la compilación, ¿cómo comprueba el JDK la compatibilidad de tipo cuando se invoca un método que utiliza genéricos sin información de tipo o información de tipo incorrecto?
Considere el siguiente ejemplo: Supongamos que la clase A
tiene un método, empty(Box<? extends Number> b)
. A.java
y obtenemos el archivo de clase A.class
.
public class A {
public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}
Ahora creamos otra clase B
que invoca el método empty
con un argumento no parametrizado (tipo sin formato): empty(new Box())
. Si B.java
con A.class
en la ruta de A.class
, javac es lo suficientemente inteligente como para generar una advertencia. Así que A.class
tiene algún tipo de información almacenada en ella.
public class B {
public static void invoke() {
// java: unchecked method invocation:
// method empty in class A is applied to given types
// required: Box<? extends java.lang.Number>
// found: Box
// java: unchecked conversion
// required: Box<? extends java.lang.Number>
// found: Box
A.empty(new Box());
}
}
Mi conjetura sería que el borrado de tipo ocurre cuando se carga la clase, pero es solo una conjetura. Entonces, ¿cuándo sucede?
El borrado de tipo se produce en tiempo de compilación. Lo que significa el borrado de tipo es que se olvidará del tipo genérico, no de todo tipo. Además, todavía habrá metadatos sobre los tipos que son genéricos. Por ejemplo
Box<String> b = new Box<String>();
String x = b.getDefault();
se convierte a
Box b = new Box();
String x = (String) b.getDefault();
en tiempo de compilación. Puede recibir advertencias no porque el compilador 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 compilador retiene la información de tipo sobre los parámetros en una llamada de método, que puede recuperar a través de la reflexión.
Esta guía es la mejor que he encontrado sobre el tema.
El compilador es responsable de comprender los genéricos en tiempo de compilación. El compilador también es responsable de deshacerse de esta "comprensión" de las clases genéricas, en un proceso que llamamos eliminación de tipos . Todo sucede en tiempo de compilación.
Nota: Contrariamente a las creencias de la mayoría de los desarrolladores de Java, es posible mantener la información de tipo de tiempo de compilación y recuperar esta información en tiempo de ejecución, a pesar de una forma muy restringida. En otras palabras: Java proporciona genéricos reificados de una manera muy restringida .
Respecto al borrado de tipos.
Tenga en cuenta que, en tiempo de compilación, el compilador tiene información completa de tipo disponible, pero esta información se elimina intencionalmente en general cuando se genera el código de byte, 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 total 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 volver a compilar sus aplicaciones heredadas al migrar a versiones más nuevas de la plataforma. La forma en que se hizo, todas las firmas de los métodos se conservan (compatibilidad con el código fuente) y no es necesario recompilar nada (compatibilidad binaria).
Respecto a los genéricos reificados en Java.
Si necesita mantener información de tipo de tiempo de compilación, debe emplear clases anónimas. El punto es: en el caso muy especial de las clases anónimas, es posible recuperar información completa de tipo de tiempo de compilación en tiempo de ejecución que, en otras palabras, significa: genéricos reificados. Esto significa que el compilador no desecha información de tipo cuando se trata de clases anónimas; esta información se guarda 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 compartida o planea 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 el poder que esta técnica le brinda.
Código de muestra
El artículo anterior tiene enlaces a código de ejemplo.
El término "borrado de tipo" no es realmente la descripción correcta del problema de Java con los genéricos. El borrado de tipos no es per se una cosa mala, de hecho es muy necesario para el rendimiento y se usa a menudo en varios idiomas como C ++, Haskell, D.
Antes de disgustarse, recuerde la definición correcta de borrado de tipo de Wiki
¿Qué es el borrado de tipo?
el borrado de tipo se refiere al proceso de tiempo de carga por el cual las anotaciones de tipo explícitas se eliminan de un programa, antes de que se ejecute en tiempo de ejecución
El borrado de tipo significa eliminar las etiquetas de tipo creadas en el momento del diseño o las etiquetas de tipo inferidas en el momento de la compilación, de modo que el programa compilado en código binario no contenga ninguna etiqueta de tipo. Y este es el caso de todos los lenguajes de programación que compilan en código binario, excepto en algunos casos en los que necesita etiquetas de tiempo de ejecución. Estas excepciones incluyen, por ejemplo, todos los tipos existenciales (tipos de referencia de Java que son subtipobles, cualquier tipo en muchos idiomas, tipos de unión). La razón para el borrado de tipo es que los programas se transforman a un lenguaje que en cierto modo es uni-typed (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 cómo se reifica.
Las declaraciones hechas a menudo sobre Java no tienen genéricos reificados también son incorrectas.
Java se reifica, pero de manera 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 programa de computadora se convierte en un modelo de datos explícito u otro objeto creado en un lenguaje de programación.
Reificación significa convertir algo abstracto (tipo paramétrico) en algo concreto (tipo concreto) por especialización.
Ilustramos esto con un simple ejemplo:
Un ArrayList con definición:
ArrayList<T>
{
T[] elems;
...//methods
}
es una abstracción, en detalle un constructor de tipos, que se "reifica" cuando se especializa con un tipo concreto, digamos Integer:
ArrayList<Integer>
{
Integer[] elems;
}
donde ArrayList<Integer>
es realmente un tipo.
Pero esto es exactamente lo que Java no hace! En su lugar, reifican tipos abstractos constantemente con sus límites, es decir, producen el mismo tipo concreto independiente de los parámetros pasados para la especialización:
ArrayList
{
Object[] elems;
}
que aquí se reifica con el Objeto enlazado implícito ( ArrayList<T extends Object>
== ArrayList<T>
).
A pesar de eso, hace que las matrices genéricas sean inutilizables y causen algunos errores extraños para los tipos en bruto:
List<String> l= List.<String>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<Integer> list){}
void function(ArrayList<Float> list){}
void function(ArrayList<String> list){}
referirse a la misma función:
void function(ArrayList list)
y por lo tanto, la sobrecarga de métodos genéricos no se puede utilizar en Java.
Me he encontrado con el borrado de tipos en Android. En producción utilizamos gradle con la opción 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<com.example.App.Models.User>
D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: android.support.v7.util.SortedList$Callback<T>
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.g.e
D/Reflection: this class: android.support.v7.g.e
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 reales se reemplazan con tipos de clases en bruto sin ningún tipo de información. Como solución para mi proyecto, eliminé todas las llamadas de reflexión y las realicé con tipos de parámetros explícitos pasados en argumentos de función.
Si tiene un campo que es un tipo genérico, sus parámetros de tipo se compilan en la clase.
Si tiene un método que toma o devuelve un tipo genérico, esos parámetros de tipo se compilan en la clase.
Esta información es lo que el compilador usa para decirle que no puede pasar un Box<String>
al método empty(Box<T extends Number>)
.
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 utiliza un tipo genérico, el compilador inserta las conversiones según sea necesario (en la persona que llama) para verificar los tipos. Los objetos genéricos en sí mismos son sólo el tipo en bruto; El tipo parametrizado se "borra". Por lo tanto, cuando crea un new Box<Integer>()
, no hay información sobre la clase Integer
en el objeto Box
.
Las preguntas frecuentes de Angelika Langer es la mejor referencia que he visto para Java Generics.
Los genéricos en el lenguaje Java es una muy buena guía sobre este tema.
Los genéricos son implementados por el compilador de Java como una conversión de front-end llamada borrado. Puede (casi) pensar en ello como una traducción de fuente a fuente, por lo que la versión genérica de
loophole()
se convierte a la versión no genérica.
Por lo tanto, es en tiempo de compilación. La JVM nunca sabrá qué ArrayList
utilizó.
También recomendaría la respuesta del Sr. Skeet sobre ¿Cuál es el concepto de borrado en genéricos en Java?