¿Cuál es el concepto de borrado en genéricos en Java?

Respuestas:

200

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.Objectdonde 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:

Jon Skeet
fuente
66
@Rogerio: No, los objetos no tendrán diferentes tipos genéricos. Los campos conocen los tipos, pero los objetos no.
Jon Skeet
8
@Rogerio: Absolutamente: es extremadamente fácil descubrir en el momento de la ejecución si algo que solo se proporciona como 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 ArrayListtipo 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>).
Jon Skeet
66
Nunca afirmé que siempre fue o casi siempre un problema, pero en mi experiencia es, al menos, razonablemente frecuente. Hay varios lugares donde me veo obligado a agregar un Class<T>parámetro a un constructor (o método genérico) simplemente porque Java no retiene esa información. Mire, EnumSet.allOfpor 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)
Jon Skeet
55
Antes de usar genéricos .NET, los genéricos de Java me resultaban incómodos de varias maneras (y el uso de comodines sigue siendo un dolor de cabeza, aunque la forma de variación "especificada por la persona que llama" definitivamente tiene ventajas), pero fue solo después de haber usado genéricos .NET Durante un tiempo, vi cuántos patrones se volvieron incómodos o imposibles con los genéricos de Java. Es la paradoja de Blub de nuevo. No estoy diciendo que los genéricos .NET tampoco tengan inconvenientes, por cierto, hay varias relaciones de tipo que, desafortunadamente, no se pueden expresar, pero lo prefiero a los genéricos de Java.
Jon Skeet
55
@Rogerio: Hay mucho que puedes hacer con la reflexión, pero no tiendo a encontrar que quiero hacer esas cosas con tanta frecuencia como las que no puedo hacer con los genéricos de Java. No quiero encontrar el argumento de tipo para un campo casi tan a menudo como quiero encontrar el argumento de tipo de un objeto real.
Jon Skeet
41

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 -printflates la bandera que se entrega al compilador que genera los archivos. (La -XDparte es lo que le dice javacque lo entregue al jar ejecutable que realmente realiza la compilación en lugar de simplemente javac, pero estoy divagando ...) -d output_dirEs 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 forbucles de estilo foreach se expanden a forbucles regulares , etc. Es agradable ver las pequeñas cosas que están sucediendo automáticamente.

jigawot
fuente
29

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();

    }
} 
Parag
fuente
Traté de usar el descompilador de Java para ver el código después de borrar el tipo del archivo .class, pero el archivo .class todavía tiene información de tipo. Intenté jigawotdecir, funciona.
Frank
25

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:

  • Compatibilidad de fuente (es bueno tener ...)
  • Compatibilidad binaria (debe tener!)
  • Compatibilidad de migración
    • Los programas existentes deben continuar funcionando
    • Las bibliotecas existentes deben poder usar tipos genéricos
    • ¡Debe tener!

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 checkCollectionmé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:

  • cuando una colección se pasa a una biblioteca de terceros y es imprescindible que el código de la biblioteca no corrompa la colección insertando un elemento del tipo incorrecto.
  • un programa falla con a 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.Iteratorclase 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();
}
VonC
fuente
2
Tenga en cuenta que la compatibilidad con versiones anteriores podría haberse logrado sin el borrado de tipos, pero no sin que los programadores de Java aprendieran un nuevo conjunto de colecciones. Esa es exactamente la ruta que siguió .NET. En otras palabras, es esta tercera bala la que es importante. (Continúa.)
Jon Skeet
15
Personalmente, creo que esto fue un error miope: dio una ventaja a corto plazo y una desventaja a largo plazo.
Jon Skeet
8

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.

Daniel Spiewak
fuente
66
El inconveniente no es cuando quieres hacer cosas "traviesas" en el momento de la ejecución. Es cuando quieres hacer cosas perfectamente razonables en el momento de la ejecución. De hecho, el borrado de tipos le permite hacer cosas mucho más traviesas, como lanzar una Lista <String> a la Lista y luego a la Lista <Fecha> con solo advertencias.
Jon Skeet
5

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.

Andrew Kennan
fuente
3
Los genéricos de Java no pueden representar tipos de valor de todos modos: no existe una Lista <int>. Sin embargo, no hay ningún paso por referencia en Java, es estrictamente pasar por valor (donde ese valor puede ser una referencia)
Jon Skeet
2

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);


   }
}
snr
fuente
2

¿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:

  • Verificaciones de tipo más fuertes en tiempo de compilación.
  • Eliminación de moldes.
  • Permitir a los programadores implementar algoritmos genéricos.

¿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:

  • Reemplace todos los parámetros de tipo en tipos genéricos con sus límites u Objeto si los parámetros de tipo son ilimitados. El bytecode producido, por lo tanto, contiene solo clases, interfaces y métodos ordinarios.
  • Inserte moldes tipo si es necesario para preservar la seguridad tipo.
  • Genere métodos puente para preservar el polimorfismo en tipos genéricos extendidos.

[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:

  • El borrado de List<Integer>, List<String>y List<List<String>>es List.
  • El borrado de List<Integer>[]es List[].
  • El borrado de Listsí mismo es similar para cualquier tipo sin procesar.
  • La eliminación de int es en sí misma, de manera similar para cualquier tipo primitivo.
  • El borrado de Integersí mismo es similar para cualquier tipo sin parámetros de tipo.
  • El borrado de Ten la definición de asListes Object, porque T no tiene límite.
  • El borrado de Ten la definición de maxes Comparable, porque T ha obligado Comparable<? super T>.
  • El borrado de Ten la definición final de maxes Object, porque Tha 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á :)

Atif
fuente