Quel est le concept d'effacement des génériques en Java?
C'est essentiellement la façon dont les génériques sont implémentés dans Java via la supercherie du compilateur. Le code générique compilé en fait utilise simplement Java.lang.Object
Partout où vous parlez de T
(ou d'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 par rapport à un type ou une méthode générique, le compilateur détermine ce que vous voulez vraiment dire (c'est-à-dire quel est l'argument de type pour T
) et vérifie à compiler fois que vous faites la bonne chose, mais le code émis parle à nouveau en termes de Java.lang.Object
- le compilateur génère des transtypages supplémentaires si nécessaire. Au moment de l'exécution, un List<String>
Et un List<Date>
Sont exactement les mêmes; les informations de type supplémentaires ont été effacées par le compilateur.
Comparez cela avec, disons, C #, où les informations sont conservées au moment de l'exécution, permettant au code de contenir des expressions telles que typeof(T)
qui est l'équivalent de T.class
- sauf que ce dernier n'est pas valide. (Il existe d'autres différences entre les génériques .NET et Java, pensez-vous.) L'effacement de type est à l'origine de nombreux messages d'avertissement/d'erreur "étranges" lors du traitement de Java.
Autres ressources:
Tout comme une note, c'est un exercice intéressant pour voir réellement ce que fait le compilateur lorsqu'il effectue l'effacement - rend le concept un peu plus facile à saisir. Il existe un indicateur spécial que vous pouvez transmettre au compilateur pour générer des fichiers Java dont les génériques ont été effacés et les transtypages insérés. Un exemple:
javac -XD-printflat -d output_dir SomeFile.Java
Le -printflat
est l'indicateur qui est transmis au compilateur qui génère les fichiers. (Le -XD
partie est ce qui dit à javac
de le remettre au pot exécutable qui fait la compilation plutôt que simplement javac
, mais je m'écarte ...) Le -d output_dir
est nécessaire car le compilateur a besoin d'un emplacement pour placer les nouveaux fichiers .Java.
Bien sûr, cela fait plus que simplement effacer; toutes les tâches automatiques effectuées par le compilateur sont effectuées ici. Par exemple, des constructeurs par défaut sont également insérés, les nouvelles boucles de style foreach for
sont développées en boucles for
régulières, etc. Il est agréable de voir les petites choses qui se produisent automatiquement.
L'effacement signifie littéralement que les informations de type présentes dans le code source sont effacées du bytecode compilé. Laissez-nous comprendre 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 Java, vous obtiendrez quelque chose comme ceci. Notez que le code décompilé ne contient aucune trace des informations de type présentes dans l'original code source.
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();
}
}
Pour compléter la réponse déjà très complète de Jon Skeet, vous devez réaliser le concept de type effacement dérive d'un besoin de compatibilité avec les versions précédentes de Java .
Initialement présentée à EclipseCon 2007 (plus disponible), la compatibilité incluait ces points:
Réponse originale:
Par conséquent:
new ArrayList<String>() => new ArrayList()
Il existe des propositions pour une plus grande réification. Reify étant "considérer 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 méthode checkCollection
de Java 6, qui renvoie une vue dynamiquement sécurisée de la collection spécifiée. Toute tentative d'insertion d'un élément de type incorrect entraînera un ClassCastException
immédiat.
Le mécanisme générique du langage fournit une vérification de type (statique) à la compilation, mais il est possible de déjouer ce mécanisme avec des transtypages non contrôlés .
Habituellement, ce n'est pas un problème, car le compilateur émet des avertissements sur toutes ces opérations non contrôlées.
Il y a cependant des moments où la vérification de type statique seule n'est pas suffisante, comme:
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 véritable source du problème.Mise à jour de juillet 2012, près de quatre ans plus tard:
Il est maintenant (2012) détaillé dans " API Migration Compatibility Rules (Signature Test) "
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é avec un fichier de classe générique sans modifier ni recompiler aucun 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é type brut ( Java Language Specification 3/4.8 ). Autoriser le type brut garantit également une compatibilité descendante pour le code source.
Selon cela, les versions suivantes de
Java.util.Iterator
La classe est à la fois binaire et code source rétrocompatible:
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();
}
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 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é descendante dans le bytecode. C'est aussi (surtout) vrai. Le bytecode généré -target 1.5 est quelque peu différent du casting juste dé-sucré -target 1.4. Techniquement, il est même possible (grâce à une immense ruse) d'accéder à des instanciations de type générique à 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 la mise en œuvre 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 des types supérieurs directement du fait que la JVM elle-même n'impose aucune restriction sur les types génériques (car 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 idée de la façon dont les génériques doivent être utilisés, annulant les tentatives d'extension du 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 d'effacement étrange émulée dans le compilateur lui-même, ce qui les rend non entièrement compatibles avec les génériques .NET anciens.
L'effacement peut être gênant lorsque vous voulez faire des choses coquines au moment de l'exécution, mais il offre la plus grande flexibilité aux rédacteurs du compilateur. Je suppose que cela fait partie des raisons pour lesquelles cela ne disparaîtra pas de si tôt.
Si je comprends bien (étant un type . NET ) le JVM n'a pas de concept de génériques, donc le compilateur remplace les paramètres de type par Objet et effectue tout le casting pour vous.
Cela signifie que 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 la mise en boîte/la décompression lorsqu'ils sont transmis par référence.
Il y a de bonnes explications. J'ajoute seulement un exemple pour montrer comment l'effacement de type fonctionne avec un décompilateur.
Classe d'origine,
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é 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);
}
}