Réponses:
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.Object
partout 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 T
sert 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:
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 ArrayList
type 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>
).
Class<T>
paramètre à un constructeur (ou à une méthode générique) simplement parce que Java ne conserve pas ces informations. Regardez EnumSet.allOf
par 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)
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 -printflat
est le drapeau qui est transmis au compilateur qui génère les fichiers. (La -XD
partie est ce qui dit javac
de 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_dir
né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 for
boucles de style foreach sont développées en for
boucles régulières , etc. Il est agréable de voir les petites choses qui se produisent automatiquement.
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();
}
}
jigawot
dit, ça marche.
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:
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 checkCollection
mé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:
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.Iterator
classe 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();
}
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.
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.
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);
}
}
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:
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 à:
[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:
List<Integer>
, List<String>
et List<List<String>>
est List
.List<Integer>[]
est List[]
.List
est lui-même, de même pour tout type brut.Integer
est lui-même, de même pour tout type sans paramètres de type.T
dans la définition de asList
est Object
, car T
n'a pas de limite.T
dans la définition de max
est Comparable
, car T
a lié Comparable<? super T>
.T
dans la définition finale de max
est Object
, car
T
a 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 :)