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


141

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

Réponses:


200

C'est fondamentalement la façon dont les génériques sont implémentés en Java via la supercherie du compilateur. Le code générique compilé n'utilise en fait que java.lang.Objectpartout où vous parlez T(ou un autre paramètre de type) - et il y a des métadonnées pour dire au compilateur qu'il s'agit vraiment d'un type générique.

Lorsque vous compilez du code par rapport à un type ou une méthode générique, le compilateur détermine ce que vous voulez vraiment dire (c'est-à-dire à quoi Tsert l' argument de type ) et vérifie au moment de la compilation que vous faites la bonne chose, mais le code émis à nouveau parle simplement en termes de java.lang.Object- le compilateur génère des casts supplémentaires si nécessaire. Au moment de l'exécution, a List<String> et a List<Date>sont exactement les mêmes; les informations de type supplémentaires ont été effacées par le compilateur.

Comparez cela avec, par exemple, C #, où les informations sont conservées 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 n'est pas valide. (Il y a 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 "étranges" lorsqu'il s'agit de génériques Java.

Autres ressources:


6
@Rogerio: Non, les objets n'auront pas différents types génériques. Les champs connaissent les types, mais pas les objets.
Jon Skeet

8
@Rogerio: Absolument - il est extrêmement facile de savoir au moment de l'exécution si quelque chose qui n'est fourni que Object(dans un scénario faiblement typé) est en fait a List<String>) par exemple. En Java, ce n'est tout simplement pas faisable - vous pouvez découvrir qu'il s'agit d'un ArrayListtype générique d'origine, mais pas de celui-ci. Ce genre de chose peut survenir dans des situations de sérialisation / désérialisation, par exemple. Un autre exemple est celui où un conteneur doit être capable de construire des instances de son type générique - vous devez transmettre ce type séparément en Java (as Class<T>).
Jon Skeet

6
Je n'ai jamais prétendu que c'était toujours ou presque toujours un problème - mais c'est au moins assez fréquemment un problème dans mon expérience. Il y a plusieurs endroits où je suis obligé d'ajouter un Class<T>paramètre à un constructeur (ou à une méthode générique) simplement parce que Java ne conserve pas ces informations. Regardez EnumSet.allOfpar exemple - l'argument de type générique de la méthode devrait suffire; pourquoi dois-je également spécifier un argument «normal»? Réponse: tapez effacement. Ce genre de chose pollue une API. Par intérêt, avez-vous beaucoup utilisé les génériques .NET? (suite)
Jon Skeet

5
Avant d'utiliser les génériques .NET, je trouvais les génériques Java gênants de diverses manières (et le joker est toujours un casse-tête, bien que la forme de variance "spécifiée par l'appelant" ait définitivement des avantages) - mais c'était seulement après avoir utilisé les génériques .NET pendant un moment, j'ai vu combien de modèles sont devenus gênants ou impossibles avec les génériques Java. C'est à nouveau le paradoxe Blub. Je ne dis pas que les génériques .NET n'ont pas non plus d'inconvénients, d'ailleurs - il existe différentes relations de type qui ne peuvent malheureusement pas être exprimées - mais je préfère de loin cela aux génériques Java.
Jon Skeet

5
@Rogerio: Il y a beaucoup de choses que vous pouvez faire avec la réflexion - mais je n'ai pas tendance à trouver que je veux faire ces choses presque aussi souvent que celles que je ne peux pas faire avec les génériques Java. Je ne veux pas trouver l'argument de type pour un champ presque aussi souvent que je veux trouver l'argument de type d'un objet réel.
Jon Skeet

41

En guise de remarque, c'est un exercice intéressant pour voir ce que fait le compilateur lorsqu'il effectue un effacement - ce qui rend l'ensemble du concept un peu plus facile à comprendre. Il existe un indicateur spécial que vous pouvez transmettre au compilateur pour afficher les 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

Le -printflatest le drapeau qui est transmis au compilateur qui génère les fichiers. (La -XDpartie est ce qui dit javacde le remettre au jar exécutable qui fait réellement la compilation plutôt que juste javac, mais je m'éloigne du sujet ...) C'est -d output_dirnécessaire parce que le compilateur a besoin d'un endroit pour mettre les nouveaux fichiers .java.

Ceci, bien sûr, fait plus que simplement effacer; toutes les tâches automatiques du compilateur sont effectuées ici. Par exemple, des constructeurs par défaut sont également insérés, les nouvelles forboucles de style foreach sont développées en forboucles régulières , etc. Il est agréable de voir les petites choses qui se produisent automatiquement.


29

Effacement, signifie littéralement que les informations de type présentes dans le code source sont effacées du bytecode compilé. Comprenons cela avec du 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 ça. Notez que le code décompilé ne contient aucune trace des informations de type présentes dans le code source d'origine.

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

    }
} 

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 contient toujours des informations de type. J'ai essayé jigawotdit, ça marche.
franc

25

Pour compléter la réponse déjà très complète de Jon Skeet, il faut se rendre compte que le concept d' effacement de type découle d'un besoin de compatibilité avec les versions précédentes de Java .

Initialement présentée à EclipseCon 2007 (plus disponible), la compatibilité comprenait ces points:

  • Compatibilité source (bien d'avoir ...)
  • Compatibilité binaire (doit avoir!)
  • Compatibilité de migration
    • Les programmes existants doivent continuer à fonctionner
    • Les bibliothèques existantes doivent pouvoir utiliser des types génériques
    • Doit avoir!

Réponse originale:

Par conséquent:

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

Il y a des propositions pour une plus grande réification . Reify étant "Considérez un concept abstrait comme réel", où les constructions de langage devraient être des concepts, pas seulement du sucre syntaxique.

Je devrais également mentionner la checkCollectionméthode de Java 6, qui renvoie une vue dynamiquement sécurisée de la collection spécifiée. Toute tentative d'insérer un élément du mauvais type entraînera une erreur immédiate ClassCastException.

Le mécanisme générique du langage fournit une vérification de type à la compilation (statique), mais il est possible de contourner ce mécanisme avec des transtypages non vérifiés .

Ce n'est généralement pas un problème, car le compilateur émet des avertissements sur toutes ces opérations non vérifiées.

Il y a cependant des moments où la vérification de type statique seule n'est pas suffisante, comme:

  • lorsqu'une collection est passée à 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 type incorrect.
  • un programme échoue avec un ClassCastException, indiquant qu'un élément mal typé a été placé dans une collection paramétrée. Malheureusement, l'exception peut se produire à 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 de juillet 2012, près de quatre ans plus tard:

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

Le langage de programmation Java implémente les génériques à l'aide de l'effacement, ce qui garantit que les versions héritées 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 type. Un tel type est appelé un type brut ( spécification du langage Java 3 / 4.8 ). Autoriser le type brut garantit également la rétrocompatibilité du code source.

Selon cela, les versions suivantes de la java.util.Iteratorclasse sont à la fois rétrocompatibles avec le code source et binaire:

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

2
Notez que la rétrocompatibilité aurait pu être obtenue sans effacement de type, mais pas sans que les programmeurs Java aient appris un nouvel ensemble de collections. C'est exactement la voie empruntée par .NET. En d'autres termes, c'est cette troisième puce qui est la plus importante. (Suite.)
Jon Skeet

15
Personnellement, je pense que c'était une erreur myope - cela a donné un avantage à court terme et un désavantage à long terme.
Jon Skeet

8

Complétant la réponse déjà complétée de Jon Skeet ...

Il a été mentionné que la mise en œuvre de génériques par effacement conduit à des limitations ennuyeuses (par exemple non new T[42]). Il a également été mentionné que la principale raison de faire les choses de cette façon était la rétrocompatibilité dans le bytecode. C'est aussi (en grande partie) vrai. Le bytecode généré -target 1.5 est quelque peu différent du moulage désucléé -target 1.4. Techniquement, il est même possible (par une immense ruse) d'accéder à des instanciations de type générique au moment de l'exécution , prouvant qu'il y a vraiment quelque chose dans le bytecode.

Le point le plus intéressant (qui n'a pas été soulevé) est que l'implémentation de génériques à l'aide de 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 vs CLR. Sur la JVM, il est possible d'implémenter directement des types supérieurs 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 a une connaissance d'exécution des instanciations de paramètres. Pour cette raison, le CLR lui-même doit avoir une certaine conception de la façon dont les génériques doivent être utilisés, annulant les tentatives d'étendre le système avec des règles imprévues. En conséquence, les types supérieurs de Scala sur le CLR sont implémentés en utilisant une forme étrange d'effacement émulée dans le compilateur lui-même,

L'effacement peut être gênant lorsque vous voulez faire des choses vilaines à l'exécution, mais il offre la plus grande flexibilité aux rédacteurs du compilateur. Je suppose que cela explique en partie pourquoi cela ne disparaîtra pas de si tôt.


6
L'inconvénient n'est pas lorsque vous voulez faire des choses "coquines" 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 lancer une List <String> en List puis en List <Date> avec seulement des avertissements.
Jon Skeet

5

Si je comprends bien (étant un gars .NET ), la JVM n'a pas de concept de génériques, donc le compilateur remplace les paramètres de type par Object et effectue tout le casting pour vous.

Cela signifie que les génériques Java ne sont rien d'autre que du sucre de syntaxe et n'offrent aucune amélioration des performances pour les types de valeur qui nécessitent un boxing / unboxing lorsqu'ils sont passés par référence.


3
De toute façon, les génériques Java ne peuvent pas représenter les types de valeur - il n'y a pas de List <int>. Cependant, il n'y a pas du tout de référence en Java - c'est strictement passe par valeur (où cette valeur peut être une référence.)
Jon Skeet

2

Il y a de bonnes explications. J'ajoute seulement un exemple pour montrer comment l'effacement de type fonctionne avec un décompilateur.

Classe originale,

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

Code décompilé à partir de son bytecode,

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


   }
}

2

Pourquoi utiliser Generices

En un mot, les génériques permettent aux types (classes et interfaces) d'être des paramètres lors de la définition des classes, des interfaces et des méthodes. Tout comme les paramètres formels plus familiers utilisés dans les déclarations de méthodes, les paramètres de type vous permettent de réutiliser le même code avec différentes entrées. La différence est que les entrées des paramètres formels sont des valeurs, tandis que les entrées des paramètres de type sont des types. ode qui utilise des génériques présente de nombreux avantages par rapport au code non générique:

  • Vérifications de type plus solides au moment de la compilation.
  • Élimination des moulages.
  • Permettre aux programmeurs d'implémenter des algorithmes génériques.

Qu'est-ce que l'effacement de type

Les génériques ont été introduits dans le langage Java pour fournir des vérifications de type plus strictes au moment de la compilation et pour prendre en charge la programmation générique. Pour implémenter des génériques, le compilateur Java applique l'effacement de type à:

  • Remplacez tous les paramètres de type dans les types génériques par leurs limites ou Object si les paramètres de type sont illimités. Le bytecode produit ne contient donc que des classes, interfaces et méthodes ordinaires.
  • Insérez des moulages de type si nécessaire pour préserver la sécurité du type.
  • Générez des méthodes de pont pour préserver le polymorphisme dans les types génériques étendus.

[NB] -Qu'est-ce que la méthode bridge? En bref, dans le cas d'une interface paramétrée telle que Comparable<T>, cela peut entraîner l'insertion de méthodes supplémentaires par le compilateur; ces méthodes supplémentaires sont appelées ponts.

Comment fonctionne l'effacement

L'effacement d'un type est défini comme suit: supprimer tous les paramètres de type des types paramétrés, et remplacer toute variable de type par l'effacement de sa borne, ou par Object si elle n'a pas de borne, ou par l'effacement de la borne la plus à gauche si elle a multiples limites. Voici quelques exemples:

  • L'effacement List<Integer>, List<String>et List<List<String>>est List.
  • L'effacement de List<Integer>[]est List[].
  • L'effacement de Listest lui-même, de même pour tout type brut.
  • L'effacement de int est lui-même, de même pour tout type primitif.
  • L'effacement de Integerest lui-même, de même pour tout type sans paramètres de type.
  • L'effacement de Tdans la définition de asListest Object, car T n'a pas de limite.
  • L'effacement de Tdans la définition de maxest Comparable, car T a lié Comparable<? super T>.
  • L'effacement de Tdans la définition finale de maxest Object, car Ta lié Object& Comparable<T>et nous prenons l'effacement de la borne la plus à gauche.

Il faut être prudent lors de l'utilisation de génériques

En Java, deux méthodes distinctes ne peuvent pas avoir la même signature. Puisque les génériques sont implémentés par effacement, il s'ensuit également que deux méthodes distinctes ne peuvent pas avoir de signatures avec le même effacement. Une classe ne peut pas surcharger deux méthodes dont les signatures ont le même effacement, et une classe ne peut pas implémenter deux interfaces qui ont le même effacement.

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

Nous avons l'intention que ce code fonctionne comme suit:

assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));

Cependant, dans ce cas, les effacements des signatures des deux méthodes sont identiques:

boolean allZero(List)

Par conséquent, un conflit de nom est signalé au moment de la compilation. Il n'est pas possible de donner aux deux méthodes le même nom et d'essayer de les distinguer par surcharge, car après l'effacement, il est impossible de distinguer un appel de méthode de l'autre.

J'espère que Reader appréciera :)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.