156 votes

Quel est le concept d'effacement dans les génériques en Java ?

Quel est le concept d'effacement dans les génériques en Java ?

220voto

Jon Skeet Points 692016

Il s'agit essentiellement de la manière dont les génériques sont mis en œuvre en Java par le biais d'une astuce du compilateur. Le code générique compilé en fait utilise juste java.lang.Object où que vous parliez de T (ou un autre paramètre de type) - et il y a des métadonnées pour indiquer au compilateur qu'il s'agit vraiment d'un type générique.

Lorsque vous compilez du code contre un type ou une méthode générique, le compilateur détermine ce que vous voulez vraiment dire (c'est-à-dire ce que l'argument de type de la fonction T est) et vérifie à compiler temps que vous faites la bonne chose, mais le code émis parle encore en termes de java.lang.Object - le compilateur génère des casts supplémentaires si nécessaire. Au moment de l'exécution, un List<String> et un List<Date> sont exactement les mêmes ; l'information supplémentaire sur le type a été ajoutée. effacé par le compilateur.

Comparez cela avec, par exemple, C#, où l'information est conservée au moment de l'exécution, ce qui permet au code de contenir des expressions telles que typeof(T) qui est l'équivalent de T.class - sauf que cette dernière est invalide. (Il existe d'autres différences entre les génériques .NET et les génériques Java, remarquez). L'effacement de type est la source de nombreux messages d'avertissement/d'erreur "bizarres" lorsqu'on utilise les génériques Java.

Autres ressources :

0 votes

Cette réponse est factuellement incorrecte : par exemple, si j'ai une classe avec deux champs de types List<String> et List<Date>, alors au moment de l'exécution ils auront des types génériques différents. Pour plus de détails, voir java.sun.com/javase/6/docs/api/java/lang/reflect/

6 votes

@Rogerio : Non, le objets n'auront pas de types génériques différents. Le site champs connaissent les types, mais pas les objets.

3 votes

Oui, mais vous ne l'avez pas précisé. Une déclaration comme "Au moment de l'exécution, une Liste<String> et une Liste<Date> sont exactement les mêmes" peut facilement être interprétée comme étant plus générale qu'elle ne l'est en réalité. Pourquoi n'avez-vous jamais mentionné que beaucoup d'informations sur les types sont en fait disponibles au moment de l'exécution ? Cela semble être un parti pris contre Java pour moi...

42voto

jigawot Points 121

En passant, il est intéressant de voir ce que fait le compilateur lorsqu'il effectue un effacement - le concept est un peu plus facile à comprendre. Il existe un drapeau spécial que vous pouvez passer au compilateur pour produire des fichiers java dont les génériques ont été effacés et les casts insérés. Un exemple :

javac -XD-printflat -d output_dir SomeFile.java

El -printflat est l'indicateur qui est transmis au compilateur qui génère les fichiers. (L'indicateur -XD est ce qui indique javac pour le remettre à l'exécutable jar qui effectue la compilation plutôt que de simplement javac mais je m'égare...) Le site -d output_dir est nécessaire car le compilateur a besoin d'un endroit où placer les nouveaux fichiers .java.

Bien entendu, cela ne se limite pas à l'effacement ; toutes les opérations automatiques du compilateur sont effectuées ici. Par exemple, les constructeurs par défaut sont également insérés, le nouveau style de foreach for les boucles sont étendues aux for boucles, etc. Il est agréable de voir les petites choses qui se passent automatiquement.

32voto

Parag Points 2447

L'effacement, littéralement, signifie que les informations de type qui sont présentes dans le code source sont effacées du bytecode compilé. Comprenons cela avec un peu de code.

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 vous compilez ce code, puis le décompilez avec un décompilateur Java, vous obtiendrez quelque chose comme ceci. Remarquez que le code décompilé ne contient aucune trace des informations de type présentes dans le code source 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();

    }
}

0 votes

J'ai essayé d'utiliser le décompilateur java pour voir le code après l'effacement du type du fichier .class, mais le fichier .class a toujours des informations de type. J'ai essayé jigawot dit, ça marche.

26voto

VonC Points 414372

Pour compléter la réponse déjà très complète de Jon Skeet, vous devez réaliser le concept de effacement de type découle d'un besoin de compatibilité avec les versions précédentes de Java .

Initialement présentée à l'EclipseCon 2007 (qui n'est plus disponible), la compatibilité incluait ces points :

  • Compatibilité des sources (souhaitable...)
  • Compatibilité binaire (indispensable !)
  • Compatibilité avec la migration
    • Les programmes existants doivent continuer à fonctionner
    • Les bibliothèques existantes doivent pouvoir utiliser des types génériques
    • A avoir absolument !

Réponse originale :

D'où :

new ArrayList<String>() => new ArrayList()

Il existe des propositions pour une plus grande réification . Réifier étant "Considérer un concept abstrait comme réel", où les constructions linguistiques devraient être des concepts, et pas seulement du sucre syntaxique.

Je dois également mentionner le checkCollection de Java 6, qui renvoie une vue dynamiquement sûre de la collection spécifiée. Toute tentative d'insertion d'un élément de type erroné entraînera un échec immédiat de la méthode ClassCastException .

Le mécanisme des génériques dans le langage fournit une vérification des types à la compilation (statique), mais il est possible de contourner ce mécanisme avec des casts non vérifiés. .

En général, ce n'est pas un problème, car le compilateur émet des avertissements pour toutes ces opérations non vérifiées.

Il arrive cependant que la vérification statique des types ne suffise pas, par exemple :

  • lorsqu'une collection est transmise à une bibliothèque tierce et qu'il est impératif que le code de la bibliothèque ne corrompe pas la collection en insérant un élément du mauvais type.
  • un programme échoue avec un ClassCastException indiquant qu'un élément incorrectement typé a été placé dans une collection paramétrée. Malheureusement, l'exception peut survenir à tout moment après l'insertion de l'élément erroné, de sorte qu'elle ne fournit généralement que peu ou pas d'informations sur la source réelle du problème.

Mise à jour en juillet 2012, presque quatre ans plus tard :

Il est maintenant (2012) détaillé dans " Règles de compatibilité de la migration des API (test de signature) "

Le langage de programmation Java met en œuvre les génériques en utilisant l'effacement, ce qui garantit que les versions patrimoniales et génériques génèrent généralement des fichiers de classe identiques, à l'exception de certaines informations auxiliaires sur les types. La compatibilité binaire n'est pas rompue, car il est possible de remplacer un fichier de classe hérité par un fichier de classe générique sans modifier ni recompiler le code client.

Pour faciliter l'interfaçage avec du code hérité non générique, il est également possible d'utiliser l'effacement d'un type paramétré comme un type. Un tel type est appelé un type brut ( Spécification du langage Java 3/4.8 ). Le fait d'autoriser le type brut assure également une compatibilité ascendante pour le code source.

Selon cette dernière, les versions suivantes de l java.util.Iterator sont rétrocompatibles tant au niveau du code binaire que du code source :

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

3 votes

Notez que la rétrocompatibilité aurait pu être obtenue sans effacement des types, mais pas sans que les programmeurs Java apprennent un nouvel ensemble de collections. C'est exactement la voie choisie par .NET. En d'autres termes, c'est ce troisième point qui est le plus important. (Suite.)

17 votes

Personnellement, je pense qu'il s'agissait d'une erreur de vision - cela a donné un avantage à court terme et un désavantage à long terme.

9voto

Daniel Spiewak Points 30706

Complétant la réponse déjà complète de Jon Skeet...

Il a été mentionné que la mise en œuvre de génériques par effacement entraîne certaines limitations gênantes (par exemple, pas de new T[42] ). Il a également été mentionné que la principale raison de procéder de cette manière était la compatibilité ascendante dans le bytecode. C'est également (en grande partie) vrai. Le bytecode généré -target 1.5 est quelque peu différent du casting désucré -target 1.4. Techniquement, il est même possible (par une immense ruse) d'avoir accès aux instanciations de types génériques. au moment de l'exécution ce qui prouve qu'il y a vraiment quelque chose dans le bytecode.

Le point le plus intéressant (qui n'a pas été soulevé) est que la mise en œuvre de génériques utilisant l'effacement offre un peu plus de flexibilité dans ce que le système de type de haut niveau peut accomplir. Un bon exemple de ceci serait l'implémentation JVM de Scala par rapport au CLR. Sur la JVM, il est possible d'implémenter des types supérieurs directement en raison du fait que la JVM elle-même n'impose aucune restriction sur les types génériques (puisque ces "types" sont effectivement absents). Cela contraste avec le CLR, qui connaît les instanciations de paramètres au moment de l'exécution. Pour cette raison, le CLR lui-même doit avoir un certain concept de la façon dont les génériques devraient être utilisés, annulant ainsi les tentatives d'étendre le système avec des règles non anticipées. En conséquence, les types supérieurs de Scala sur le CLR sont mis en œuvre en utilisant une forme bizarre d'effacement émulée dans le compilateur lui-même, ce qui les rend non entièrement compatibles avec les génériques classiques de .NET.

L'effacement peut s'avérer peu pratique lorsque l'on veut faire des choses vilaines à l'exécution, mais il offre le plus de flexibilité aux auteurs de compilateurs. Je suppose que c'est en partie pour cela qu'il n'est pas prêt de disparaître.

8 votes

L'inconvénient n'est pas lorsque vous voulez faire des choses "vilaines" au moment de l'exécution. C'est lorsque vous voulez faire des choses parfaitement raisonnables au moment de l'exécution. En fait, l'effacement de type vous permet de faire des choses bien plus vilaines - comme le casting d'une List<String> en List puis en List<Date> avec seulement des avertissements.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X