Respuestas:
Básicamente es la forma en que los genéricos se implementan en Java a través del truco del compilador. El código genérico compilado en realidad solo se usa java.lang.Object
donde sea que hable T
(o algún otro parámetro de tipo), y hay algunos metadatos para decirle al compilador que realmente es un tipo genérico.
Cuando compila algún código contra un tipo o método genérico, el compilador determina lo que realmente quiere decir (es decir, para qué es el argumento de tipo T
) y verifica en el momento de la compilación que está haciendo lo correcto, pero el código emitido nuevamente solo habla en términos de java.lang.Object
: el compilador genera conversiones adicionales cuando es necesario. En el momento de la ejecución, a List<String>
y a List<Date>
son exactamente iguales; El compilador ha borrado la información de tipo adicional .
Compare esto con, digamos, C #, donde la información se retiene en el momento de la ejecución, permitiendo que el código contenga expresiones tales como typeof(T)
cuál es el equivalente a T.class
, excepto que este último no es válido. (Hay más diferencias entre los genéricos de .NET y los genéricos de Java, tenga en cuenta). El borrado de tipos es la fuente de muchos de los mensajes de advertencia / error "extraños" cuando se trata de genéricos de Java.
Otros recursos:
Object
(en un escenario de tipo débil) es realmente a List<String>
) por ejemplo. En Java, eso no es factible: puede descubrir que es un ArrayList
tipo genérico original, pero no el que era. Este tipo de cosas pueden surgir en situaciones de serialización / deserialización, por ejemplo. Otro ejemplo es cuando un contenedor tiene que poder construir instancias de su tipo genérico; debe pasar ese tipo por separado en Java (as Class<T>
).
Class<T>
parámetro a un constructor (o método genérico) simplemente porque Java no retiene esa información. Mire, EnumSet.allOf
por ejemplo, el argumento de tipo genérico para el método debería ser suficiente; ¿Por qué necesito especificar un argumento "normal" también? Respuesta: borrar tipo. Este tipo de cosas contamina una API. Por interés, ¿ha usado mucho los genéricos .NET? (continuación)
Como una nota al margen, es un ejercicio interesante ver lo que hace el compilador cuando realiza el borrado: hace que todo el concepto sea un poco más fácil de entender. Hay un indicador especial que puede pasar al compilador para generar archivos java a los que se hayan borrado los genéricos y se hayan insertado los moldes. Un ejemplo:
javac -XD-printflat -d output_dir SomeFile.java
La -printflat
es la bandera que se entrega al compilador que genera los archivos. (La -XD
parte es lo que le dice javac
que lo entregue al jar ejecutable que realmente realiza la compilación en lugar de simplemente javac
, pero estoy divagando ...) -d output_dir
Es necesario porque el compilador necesita un lugar para colocar los nuevos archivos .java.
Esto, por supuesto, hace más que solo borrar; Todas las cosas automáticas que hace el compilador se hacen aquí. Por ejemplo, los constructores predeterminados también se insertan, los nuevos for
bucles de estilo foreach se expanden a for
bucles regulares , etc. Es agradable ver las pequeñas cosas que están sucediendo automáticamente.
Borrar, literalmente significa que la información de tipo que está presente en el código fuente se borra del código de bytes compilado. Vamos a entender esto con algún código.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class GenericsErasure {
public static void main(String args[]) {
List<String> list = new ArrayList<String>();
list.add("Hello");
Iterator<String> iter = list.iterator();
while(iter.hasNext()) {
String s = iter.next();
System.out.println(s);
}
}
}
Si compila este código y luego lo descompila con un descompilador de Java, obtendrá algo como esto. Observe que el código descompilado no contiene ningún rastro de la información de tipo presente en el código fuente original.
import java.io.PrintStream;
import java.util.*;
public class GenericsErasure
{
public GenericsErasure()
{
}
public static void main(String args[])
{
List list = new ArrayList();
list.add("Hello");
String s;
for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
s = (String)iter.next();
}
}
jigawot
decir, funciona.
Para completar la respuesta de Jon Skeet, que ya es muy completa, debe tener en cuenta que el concepto de borrado de tipo deriva de una necesidad de compatibilidad con versiones anteriores de Java .
Presentado inicialmente en EclipseCon 2007 (ya no está disponible), la compatibilidad incluía esos puntos:
Respuesta original:
Por lo tanto:
new ArrayList<String>() => new ArrayList()
Hay propuestas para una mayor reificación . Reify es "Considerar un concepto abstracto como real", donde las construcciones del lenguaje deben ser conceptos, no solo azúcar sintáctico.
También debería mencionar el checkCollection
método de Java 6, que devuelve una vista dinámica de tipo seguro de la colección especificada. Cualquier intento de insertar un elemento del tipo incorrecto dará como resultado un inmediato ClassCastException
.
El mecanismo genérico en el lenguaje proporciona una verificación de tipo en tiempo de compilación (estática), pero es posible vencer este mecanismo con conversiones no controladas .
Por lo general, esto no es un problema, ya que el compilador emite advertencias en todas esas operaciones no verificadas.
Sin embargo, hay ocasiones en que la verificación de tipo estático por sí sola no es suficiente, como:
ClassCastException
, lo que indica que un elemento incorrectamente escrito se colocó en una colección parametrizada. Desafortunadamente, la excepción puede ocurrir en cualquier momento después de que se inserte el elemento erróneo, por lo que generalmente proporciona poca o ninguna información sobre la fuente real del problema.Actualización de julio de 2012, casi cuatro años después:
Ahora (2012) se detalla en " Reglas de compatibilidad de migración de API (Prueba de firma) "
El lenguaje de programación Java implementa genéricos mediante el borrado, lo que garantiza que las versiones heredadas y genéricas usualmente generen archivos de clase idénticos, excepto cierta información auxiliar sobre los tipos. La compatibilidad binaria no se interrumpe porque es posible reemplazar un archivo de clase heredado con un archivo de clase genérico sin cambiar ni recompilar ningún código de cliente.
Para facilitar la interacción con el código heredado no genérico, también es posible usar la eliminación de un tipo parametrizado como tipo. Dicho tipo se denomina tipo sin formato ( Java Language Specification 3 / 4.8 ). Permitir el tipo sin formato también garantiza la compatibilidad con versiones anteriores para el código fuente.
De acuerdo con esto, las siguientes versiones de la
java.util.Iterator
clase son binarias y compatibles con el código fuente:
Class java.util.Iterator as it is defined in Java SE version 1.4:
public interface Iterator {
boolean hasNext();
Object next();
void remove();
}
Class java.util.Iterator as it is defined in Java SE version 5.0:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
Complementando la respuesta de Jon Skeet ya complementada ...
Se ha mencionado que la implementación de medicamentos genéricos a través del borrado conlleva algunas limitaciones molestas (por ejemplo, no new T[42]
). También se ha mencionado que la razón principal para hacer las cosas de esta manera era la compatibilidad con versiones anteriores en el código de bytes. Esto también es (principalmente) cierto. El bytecode generado -target 1.5 es algo diferente de la conversión de -gaget -target 1.4. Técnicamente, incluso es posible (a través de inmensos trucos) obtener acceso a instancias de tipo genérico en tiempo de ejecución , lo que demuestra que realmente hay algo en el código de bytes.
El punto más interesante (que no se ha planteado) es que la implementación de genéricos utilizando borrado ofrece bastante más flexibilidad en lo que puede lograr el sistema de tipos de alto nivel. Un buen ejemplo de esto sería la implementación JVM de Scala frente a CLR. En la JVM, es posible implementar tipos superiores directamente debido al hecho de que la JVM en sí misma no impone restricciones a los tipos genéricos (ya que estos "tipos" están efectivamente ausentes). Esto contrasta con el CLR, que tiene conocimiento de tiempo de ejecución de las instancias de parámetros. Debido a esto, el CLR en sí mismo debe tener algún concepto de cómo se deben usar los genéricos, anulando los intentos de extender el sistema con reglas no anticipadas. Como resultado, los tipos superiores de Scala en el CLR se implementan utilizando una forma extraña de borrado emulada dentro del compilador,
El borrado puede ser inconveniente cuando quieres hacer cosas malas en tiempo de ejecución, pero ofrece la mayor flexibilidad a los escritores del compilador. Supongo que eso es parte de por qué no va a desaparecer en el corto plazo.
Según tengo entendido (siendo un tipo .NET ), la JVM no tiene concepto de genéricos, por lo que el compilador reemplaza los parámetros de tipo con Object y realiza todo el casting por usted.
Esto significa que los genéricos de Java no son más que azúcar de sintaxis y no ofrecen ninguna mejora de rendimiento para los tipos de valor que requieren boxing / unboxing cuando se pasan por referencia.
Hay buenas explicaciones. Solo agrego un ejemplo para mostrar cómo funciona el borrado de tipo con un descompilador.
Clase original,
import java.util.ArrayList;
import java.util.List;
public class S<T> {
T obj;
S(T o) {
obj = o;
}
T getob() {
return obj;
}
public static void main(String args[]) {
List<String> list = new ArrayList<>();
list.add("Hello");
// for-each
for(String s : list) {
String temp = s;
System.out.println(temp);
}
// stream
list.forEach(System.out::println);
}
}
Código descompilado de su código de bytes,
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;
public class S {
Object obj;
S(Object var1) {
this.obj = var1;
}
Object getob() {
return this.obj;
}
public static void main(String[] var0) {
ArrayList var1 = new ArrayList();
var1.add("Hello");
// for-each
Iterator iterator = var1.iterator();
while (iterator.hasNext()) {
String string;
String string2 = string = (String)iterator.next();
System.out.println(string2);
}
// stream
PrintStream printStream = System.out;
Objects.requireNonNull(printStream);
var1.forEach(printStream::println);
}
}
¿Por qué usar Generices?
En pocas palabras, los genéricos permiten que los tipos (clases e interfaces) sean parámetros al definir clases, interfaces y métodos. Al igual que los parámetros formales más familiares utilizados en las declaraciones de métodos, los parámetros de tipo proporcionan una forma de reutilizar el mismo código con diferentes entradas. La diferencia es que las entradas a los parámetros formales son valores, mientras que las entradas a los parámetros de tipo son tipos. La oda que usa genéricos tiene muchos beneficios sobre el código no genérico:
¿Qué es el borrado de tipo?
Se introdujeron genéricos en el lenguaje Java para proporcionar verificaciones de tipo más estrictas en el momento de la compilación y para soportar la programación genérica. Para implementar genéricos, el compilador de Java aplica el borrado de tipo a:
[NB] -¿Qué es el método de puente? En breve, en el caso de una interfaz parametrizada como Comparable<T>
, esto puede hacer que el compilador inserte métodos adicionales; Estos métodos adicionales se denominan puentes.
Cómo funciona el borrado
La eliminación de un tipo se define de la siguiente manera: elimine todos los parámetros de tipo de los tipos parametrizados y reemplace cualquier variable de tipo con la eliminación de su límite, o con Object si no tiene un límite, o con la eliminación del límite más a la izquierda si tiene Múltiples límites. Aquí hay unos ejemplos:
List<Integer>
, List<String>
y List<List<String>>
es List
.List<Integer>[]
es List[]
.List
sí mismo es similar para cualquier tipo sin procesar.Integer
sí mismo es similar para cualquier tipo sin parámetros de tipo.T
en la definición de asList
es Object
, porque T
no tiene límite.T
en la definición de max
es Comparable
, porque T
ha obligado Comparable<? super T>
.T
en la definición final de max
es Object
, porque
T
ha unido Object
& Comparable<T>
y tomamos el borrado del límite más a la izquierda.Debe tener cuidado cuando use genéricos
En Java, dos métodos distintos no pueden tener la misma firma. Como los genéricos se implementan por borrado, también se deduce que dos métodos distintos no pueden tener firmas con el mismo borrado. Una clase no puede sobrecargar dos métodos cuyas firmas tienen el mismo borrado, y una clase no puede implementar dos interfaces que tengan el mismo borrado.
class Overloaded2 {
// compile-time error, cannot overload two methods with same erasure
public static boolean allZero(List<Integer> ints) {
for (int i : ints) if (i != 0) return false;
return true;
}
public static boolean allZero(List<String> strings) {
for (String s : strings) if (s.length() != 0) return false;
return true;
}
}
Tenemos la intención de que este código funcione de la siguiente manera:
assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));
Sin embargo, en este caso las eliminaciones de las firmas de ambos métodos son idénticas:
boolean allZero(List)
Por lo tanto, se informa un choque de nombres en tiempo de compilación. No es posible dar a ambos métodos el mismo nombre e intentar distinguirlos sobrecargándolos, porque después de la eliminación es imposible distinguir una llamada de método de la otra.
Con suerte, Reader disfrutará :)