Quel est le concept d'effacement dans les génériques en Java?
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 aList<String>
) par exemple. En Java, ce n'est tout simplement pas faisable - vous pouvez découvrir qu'il s'agit d'unArrayList
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 (asClass<T>
).Class<T>
paramètre à un constructeur (ou à une méthode générique) simplement parce que Java ne conserve pas ces informations. RegardezEnumSet.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:
Le
-printflat
est le drapeau qui est transmis au compilateur qui génère les fichiers. (La-XD
partie est ce qui ditjavac
de le remettre au jar exécutable qui fait réellement la compilation plutôt que justejavac
, 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 enfor
boucles régulières , etc. Il est agréable de voir les petites choses qui se produisent automatiquement.la source
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.
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.
la source
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:
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édiateClassCastException
.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) "
la source
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.
la source
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.
la source
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,
Code décompilé à partir de son bytecode,
la source
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>
etList<List<String>>
estList
.List<Integer>[]
estList[]
.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 deasList
estObject
, carT
n'a pas de limite.T
dans la définition demax
estComparable
, carT
a liéComparable<? super T>
.T
dans la définition finale demax
estObject
, carT
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.
Nous avons l'intention que ce code fonctionne comme suit:
Cependant, dans ce cas, les effacements des signatures des deux méthodes sont identiques:
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 :)
la source