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 A
tiene un método, empty(Box<? extends Number> b)
. Compilamos 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 vamos a crear otra clase B
que invoca el método empty
con un argumento que no es parametrizada (tipo cruda): empty(new Box())
. Si se compila B.java
con A.class
la 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?
fuente
Respuestas:
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:
se compila en
En el momento de la ejecución no hay forma de descubrir eso
T=String
para 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 esoT=String
para el objeto de la lista en sí.fuente
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 .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.
fuente
new Box<String>() {};
no en caso de acceso indirectovoid foo(T) {...new Box<T>() {};...}
porque el compilador no guarda información de tipo para La declaración del método de cierre.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 laempty(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
,getGenericReturnType
y, 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 laInteger
clase en elBox
objeto.Las preguntas frecuentes de Angelika Langer son la mejor referencia que he visto para Java Generics.
fuente
Generics in Java Language es una muy buena guía sobre este tema.
Entonces, es en tiempo de compilación. La JVM nunca sabrá cuál
ArrayList
usó.También recomendaría la respuesta del Sr. Skeet sobre ¿Cuál es el concepto de borrado en genéricos en Java?
fuente
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
se convierte a
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.
fuente
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 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
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:
es una abstracción, en detalle, un constructor de tipos, que se "reifica" cuando se especializa con un tipo concreto, por ejemplo, Integer:
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:
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:
causa muchas ambigüedades como
refiérase a la misma función:
y, por lo tanto, la sobrecarga de métodos genéricos no se puede usar en Java.
fuente
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:
Y hay dos resultados de esta función:
Código no minimizado:
Código minificado:
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.
fuente