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

238

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

¿Cuándo ocurre el borrado de tipo? ¿En tiempo de compilación o tiempo de ejecución? Cuando se carga la clase? ¿Cuándo se instancia la clase?

Muchos sitios (incluido el tutorial oficial mencionado anteriormente) dicen que el borrado de tipo ocurre en el momento de la compilación. Si la información de tipo se elimina por completo en el momento de la compilación, ¿cómo verifica el JDK la compatibilidad de tipo cuando se invoca un método que usa genéricos sin información de tipo o información de tipo incorrecta?

Considere el siguiente ejemplo: class Say Atiene un método, empty(Box<? extends Number> b). Compilamos A.javay obtenemos el archivo de clase A.class.

public class A {
    public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}

Ahora vamos a crear otra clase Bque invoca el método emptycon un argumento que no es parametrizada (tipo cruda): empty(new Box()). Si se compila B.javacon A.classla ruta de clases, javac es lo suficientemente inteligente como para levantar 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<? extends java.lang.Number>
        //  found:    Box
        // java: unchecked conversion
        //  required: Box<? extends java.lang.Number>
        //  found:    Box
        A.empty(new Box());
    }
}

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

Radiodef
fuente
2
Una versión más "genérica" ​​de esta pregunta: stackoverflow.com/questions/313584/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@afryingpan: El artículo mencionado en mi respuesta explica en detalle cómo y cuándo ocurre la eliminación de tipo. También explica cuándo se guarda la información de tipo. En otras palabras: genéricos reificados están disponibles en Java, contrario a la creencia generalizada. Ver: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Richard Gomes

Respuestas:

240

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 comprobaciones en tiempo de compilación y conversiones en tiempo de ejecución. Entonces 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 eso T=Stringpara el objeto de la lista: esa información se ha ido.

... pero la List<T>interfaz en sí misma todavía se anuncia como genérica.

EDITAR: solo para aclarar, el compilador retiene la información sobre la variable que es un List<String>- pero aún no puede encontrar eso T=Stringpara el objeto de la lista en sí.

Jon Skeet
fuente
66
No, incluso en el uso de un tipo genérico puede haber metadatos disponibles en tiempo de ejecución. No se puede acceder a una variable local a través de Reflection, pero para un parámetro de método declarado como "Lista <String> l", habrá metadatos de tipo en tiempo de ejecución, disponibles a través de la API de Reflection. Sí, "Tipo de borrado" no es tan simple como muchos piensan ...
Rogério
44
@Rogerio: Como respondí a su comentario, creo que se está confundiendo entre poder obtener el tipo de una variable y poder obtener el tipo de un objeto . El objeto en sí no conoce su argumento de tipo, aunque el campo sí.
Jon Skeet
Por supuesto, solo mirando el objeto en sí no se puede saber que es una Lista <String>. Pero los objetos no solo aparecen de la nada. Se crean localmente, se pasan como un argumento de invocación de método, se devuelven como el valor de retorno de una llamada a método o se leen desde un campo de algún objeto ... En todos estos casos, PUEDES saber en tiempo de ejecución cuál es el tipo genérico, implícitamente o mediante el uso de la API de Java Reflection.
Rogério
13
@Rogerio: ¿Cómo sabes de dónde viene el objeto? Si tiene un parámetro de tipo, List<? extends InputStream>¿cómo puede saber qué tipo era cuando se creó? Incluso si puede averiguar el tipo de campo en el que se ha almacenado la referencia, ¿por qué debería hacerlo? ¿Por qué debería poder obtener todo el resto de la información sobre el objeto en el momento de la ejecución, pero no sus argumentos de tipo genérico? Parece que estás tratando de hacer que el borrado del tipo sea una cosa pequeña que realmente no afecta a los desarrolladores, mientras que en algunos casos creo que es un problema muy significativo .
Jon Skeet
¡Pero el borrado de texto ES una cosa pequeña que realmente no afecta a los desarrolladores! Por supuesto, no puedo hablar por los demás, pero en mi experiencia nunca fue un gran problema. De hecho, aprovecho la información del tipo de tiempo de ejecución en el diseño de mi API de burla de Java (JMockit); Irónicamente, las API de burla de .NET parecen aprovechar menos el sistema de tipos genéricos disponible en C #.
Rogério
99

El compilador es responsable de comprender los genéricos en tiempo de compilación. El compilador también es responsable de desechar esta "comprensión" de las clases genéricas, en un proceso que llamamos borrado de tipo . Todo sucede en tiempo de compilación.

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

En cuanto al tipo de borrado

Tenga en cuenta que, en tiempo de compilación, el compilador tiene información de tipo completo disponible, pero esta información se elimina 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 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 recientes de la plataforma. De la forma en que se hizo, se conservan todas las firmas de métodos (compatibilidad con el código fuente) y no necesita volver a compilar nada (compatibilidad binaria).

Sobre 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 del 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 arroja información de tipo cuando hay clases anónimas involucradas; 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:

https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/

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 le brinda esta técnica.

Código de muestra

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

Richard Gomes
fuente
1
@ will824: he mejorado enormemente la respuesta y he agregado un enlace a algunos casos de prueba. Saludos :)
Richard Gomes
1
En realidad, no mantuvieron la compatibilidad binaria y de origen: oracle.com/technetwork/java/javase/compatibility-137462.html ¿Dónde puedo leer más sobre su intención? Los doctores dicen que usa el tipo de borrado, pero no dice por qué.
Dzmitry Lazerka
@ Richard De hecho, ¡excelente artículo! Podría agregar que las clases locales también funcionan y que, en ambos casos (clases anónimas y locales), la información sobre el argumento de tipo deseado se mantiene solo en caso de acceso directo, new Box<String>() {};no en caso de acceso indirecto void foo(T) {...new Box<T>() {};...}porque el compilador no guarda información de tipo para La declaración del método de cierre.
Yann-Gaël Guéhéneuc
He arreglado un enlace roto a mi artículo. Lentamente estoy buscando en Google mi vida y reclamando mis datos. :-)
Richard Gomes
33

Si tiene un campo que es de 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 la que utiliza el compilador para decirle que no se puede pasar de una Box<String>a la empty(Box<T extends Number>)método.

La API es complicado, pero se puede inspeccionar este tipo de información a través de la API de reflexión con métodos como getGenericParameterTypes, getGenericReturnTypey, para los campos, getGenericType.

Si tiene código que usa un tipo genérico, el compilador inserta conversiones según sea necesario (en la persona que llama) para verificar los tipos. Los objetos genéricos en sí mismos son solo el tipo sin formato; el tipo parametrizado se "borra". Entonces, cuando crea un new Box<Integer>(), no hay información sobre la Integerclase en el Boxobjeto.

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

erickson
fuente
2
En realidad, es el tipo genérico formal de los campos y métodos que se compilan en la clase, es decir, típicamente "T". Para obtener el tipo real del tipo genérico, debe utilizar el "truco de clase anónima" .
Yann-Gaël Guéhéneuc
13

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

El compilador Java implementa los genéricos como una conversión de front-end llamada borrado. Puede (casi) pensar en ella 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.

Entonces, es en tiempo de compilación. La JVM nunca sabrá cuál ArrayListusó.

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

Eugene Yokota
fuente
6

El borrado de tipo ocurre en tiempo de compilación. Lo que significa la eliminación de tipo es que olvidará el tipo genérico, no todos los tipos. 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 al método, que puede recuperar mediante reflexión.

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

Vinko Vrsalovic
fuente
6

El término "borrado de tipo" no es realmente la descripción correcta del problema de Java con los genéricos. El borrado de tipo no es per se algo malo, de hecho es muy necesario para el rendimiento y a menudo se usa en varios lenguajes como C ++, Haskell, D.

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

¿Qué es el tipo de borrado?

el borrado de tipo se refiere al proceso de tiempo de carga mediante 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 tirar las etiquetas de tipo creadas en tiempo de diseño o las etiquetas de tipo inferidas en tiempo de compilación de modo que el programa compilado en código binario no contenga ninguna etiqueta de tipo. Y este es el caso de cada lenguaje de programación que compila código binario, excepto en algunos casos donde necesita etiquetas 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 para el borrado de tipos es que los programas se transforman en un lenguaje que es de algún tipo sin tipo (el lenguaje binario solo permite bits) ya que los tipos son solo abstracciones y afirman una estructura para sus valores y la semántica adecuada para manejarlos.

Esto es a cambio, una cosa natural normal.

El problema de Java es diferente y se debe a cómo se reifica.

Las declaraciones a menudo hechas sobre Java no tienen genéricos reificados también están mal.

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:

Una 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, por ejemplo, Integer:

ArrayList<Integer>
{
    Integer[] elems;
}

donde ArrayList<Integer>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 vinculado implícito ( ArrayList<T extends Object>== ArrayList<T>).

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

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){}

refiérase a la misma función:

void function(ArrayList list)

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

iconfly
fuente
2

Me he encontrado con el borrado de tipo en Android. En producción utilizamos gradle con 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 minimizado:

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 minimizado, las clases parametrizadas reales se reemplazan por tipos de clases sin formato sin ninguna información de tipo. Como solución para mi proyecto, eliminé todas las llamadas de reflexión y las respondí con tipos de parámetros explícitos pasados ​​en argumentos de función.

porfirion
fuente