J'ai lu sur l'effacement de type Java sur le site Web d'Oracle .
Quand l'effacement de type se produit-il? Au moment de la compilation ou de l'exécution? Quand la classe est chargée? Quand la classe est instanciée?
De nombreux sites (y compris le tutoriel officiel mentionné ci-dessus) disent que l'effacement de type se produit au moment de la compilation. Si les informations de type sont complètement supprimées au moment de la compilation, comment le JDK vérifie-t-il la compatibilité des types lorsqu'une méthode utilisant des génériques est invoquée sans informations de type ou avec des informations de type incorrectes?
Prenons l'exemple suivant: classe Say A
a une méthode, empty(Box<? extends Number> b)
. Nous compilons A.java
et obtenons le fichier de classe A.class
.
public class A {
public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}
Maintenant , nous créons une autre classe B
qui invoque la méthode empty
avec un argument non paramétrées (type brut): empty(new Box())
. Si nous compilons B.java
avec A.class
dans le chemin de classe, javac est assez intelligent pour déclencher un avertissement. AlorsA.class
est de même pour certaines informations de type.
public class B {
public static void invoke() {
// java: unchecked method invocation:
// method empty in class A is applied to given types
// required: Box<? extends java.lang.Number>
// found: Box
// java: unchecked conversion
// required: Box<? extends java.lang.Number>
// found: Box
A.empty(new Box());
}
}
Je suppose que l'effacement de type se produit lorsque la classe est chargée, mais ce n'est qu'une supposition. Alors, quand cela se produit-il?
la source
Réponses:
L'effacement de type s'applique à l' utilisation de génériques. Il y a certainement des métadonnées dans le fichier de classe pour dire si une méthode / type est générique et quelles sont les contraintes, etc. Mais lorsque des génériques sont utilisés , ils sont convertis en vérifications au moment de la compilation et en cas d'exécution. Donc, ce code:
est compilé en
Au moment de l'exécution, il n'y a aucun moyen de savoir que
T=String
pour l'objet liste - ces informations ont disparu.... mais l'
List<T>
interface elle-même s'annonce toujours comme étant générique.EDIT: Juste pour clarifier, le compilateur conserve les informations sur la variable étant un
List<String>
- mais vous ne pouvez toujours pas le découvrirT=String
pour l'objet de liste lui-même.la source
List<? extends InputStream>
comment savoir de quel type il s'agissait lors de sa création? Même si vous pouvez trouver le type de champ dans lequel la référence a été stockée, pourquoi devriez-vous le faire? Pourquoi devriez-vous pouvoir obtenir toutes les autres informations sur l'objet au moment de l'exécution, mais pas ses arguments de type générique? Vous semblez essayer de faire de l'effacement de type ce petit élément qui n'affecte pas vraiment les développeurs - alors que je trouve que c'est un problème très important dans certains cas.Le compilateur est responsable de la compréhension des génériques au moment de la compilation. Le compilateur est également responsable de jeter cette "compréhension" des classes génériques, dans un processus que nous appelons l' effacement de type . Tout se passe au moment de la compilation.
Remarque: Contrairement à la croyance de la majorité des développeurs Java, il est possible de conserver les informations de type au moment de la compilation et de récupérer ces informations lors de l'exécution, malgré de manière très restreinte. En d'autres termes: Java fournit des génériques réifiés de manière très restreinte .
Concernant l'effacement de type
Notez qu'au moment de la compilation, le compilateur dispose d'informations de type complet mais que ces informations sont intentionnellement supprimées en général lorsque le code d'octet est généré, dans un processus appelé effacement de type . Cela se fait de cette façon en raison de problèmes de compatibilité: l'intention des concepteurs de langage était de fournir une compatibilité complète du code source et une compatibilité du code d'octets complet entre les versions de la plate-forme. S'il était implémenté différemment, vous devrez recompiler vos applications héritées lors de la migration vers des versions plus récentes de la plate-forme. De la façon dont cela a été fait, toutes les signatures de méthode sont préservées (compatibilité du code source) et vous n'avez rien à recompiler (compatibilité binaire).
Concernant les génériques réifiés en Java
Si vous devez conserver des informations de type à la compilation, vous devez utiliser des classes anonymes. Le point est le suivant: dans le cas très particulier des classes anonymes, il est possible de récupérer des informations complètes de type à la compilation lors de l'exécution ce qui signifie, en d'autres termes: des génériques réifiés. Cela signifie que le compilateur ne jette pas les informations de type lorsque des classes anonymes sont impliquées; ces informations sont conservées dans le code binaire généré et le système d'exécution vous permet de récupérer ces informations.
J'ai écrit un article sur ce sujet:
https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/
Une note sur la technique décrite dans l'article ci-dessus est que la technique est obscure pour la majorité des développeurs. Malgré le fait que cela fonctionne et fonctionne bien, la plupart des développeurs se sentent confus ou mal à l'aise avec la technique. Si vous avez une base de code partagée ou prévoyez de publier votre code au public, je ne recommande pas la technique ci-dessus. D'autre part, si vous êtes le seul utilisateur de votre code, vous pouvez profiter de la puissance que cette technique vous offre.
Exemple de code
L'article ci-dessus contient des liens vers un exemple de code.
la source
new Box<String>() {};
non en cas d'accès indirectvoid foo(T) {...new Box<T>() {};...}
car le compilateur ne conserve pas les informations de type pour la déclaration de méthode englobante.Si vous avez un champ de type générique, ses paramètres de type sont compilés dans la classe.
Si vous avez une méthode qui prend ou renvoie un type générique, ces paramètres de type sont compilés dans la classe.
Ces informations sont ce que le compilateur utilise pour vous dire que vous ne pouvez pas passer un
Box<String>
à laempty(Box<T extends Number>)
méthode.L'API est compliquée, mais vous pouvez vérifier ces informations de type via l'API de réflexion avec des méthodes telles que
getGenericParameterTypes
,getGenericReturnType
et, pour les champs,getGenericType
.Si vous avez du code qui utilise un type générique, le compilateur insère des transtypages selon les besoins (dans l'appelant) pour vérifier les types. Les objets génériques eux-mêmes ne sont que le type brut; le type paramétré est "effacé". Ainsi, lorsque vous créez un
new Box<Integer>()
, il n'y a aucune information sur laInteger
classe dans l'Box
objet.La FAQ d'Angelika Langer est la meilleure référence que j'ai vue pour Java Generics.
la source
Les génériques en langage Java sont un très bon guide sur ce sujet.
Donc, c'est au moment de la compilation. La JVM ne saura jamais lequel
ArrayList
vous avez utilisé.Je recommanderais également la réponse de M. Skeet sur Quel est le concept d'effacement dans les génériques en Java?
la source
L'effacement de type se produit au moment de la compilation. L'effacement de type signifie qu'il oubliera le type générique, pas tous les types. En outre, il y aura toujours des métadonnées sur les types génériques. Par exemple
est converti en
au moment de la compilation. Vous pouvez recevoir des avertissements non pas parce que le compilateur sait de quel type est le générique, mais au contraire, car il n'en sait pas assez et ne peut donc pas garantir la sécurité du type.
En outre, le compilateur conserve les informations de type sur les paramètres d'un appel de méthode, que vous pouvez récupérer via la réflexion.
Ce guide est le meilleur que j'ai trouvé sur le sujet.
la source
Le terme "effacement de type" n'est pas vraiment la description correcte du problème de Java avec les génériques. L'effacement de type n'est pas en soi une mauvaise chose, en effet, il est très nécessaire pour les performances et est souvent utilisé dans plusieurs langages comme C ++, Haskell, D.
Avant de dégoûter, veuillez vous rappeler la définition correcte de l'effacement de type du Wiki
Qu'est-ce que l'effacement de type?
L'effacement de type signifie supprimer les balises de type créées au moment de la conception ou les balises de type déduites au moment de la compilation de sorte que le programme compilé en code binaire ne contienne aucune balise de type. Et c'est le cas pour tous les langages de programmation qui se compilent en code binaire, sauf dans certains cas où vous avez besoin de balises d'exécution. Ces exceptions incluent par exemple tous les types existentiels (types de référence Java qui sont sous-typables, tout type dans de nombreuses langues, types d'union). La raison de l'effacement de type est que les programmes sont transformés en un langage qui est en quelque sorte uni-typé (le langage binaire n'autorisant que les bits) car les types ne sont que des abstractions et affirment une structure pour ses valeurs et la sémantique appropriée pour les gérer.
C'est donc en retour, une chose naturelle normale.
Le problème de Java est différent et lié à sa réification.
Les déclarations souvent faites au sujet de Java n'ont pas de génériques réifiés sont également fausses.
Java réifie, mais d'une mauvaise manière en raison de la compatibilité descendante.
Qu'est-ce que la réification?
De notre Wiki
La réification signifie convertir quelque chose d'abstrait (type paramétrique) en quelque chose de concret (type concret) par spécialisation.
Nous illustrons cela par un exemple simple:
Une liste de tableaux avec définition:
est une abstraction, en détail un constructeur de type, qui est "réifié" lorsqu'il est spécialisé dans un type concret, dit Integer:
où
ArrayList<Integer>
est vraiment un type.Mais c'est exactement ce que Java ne fait pas !!! , au lieu de cela, ils réifient constamment les types abstraits avec leurs bornes, c'est-à-dire produisant le même type concret indépendamment des paramètres passés pour la spécialisation:
qui est ici réifié avec l'objet lié implicite (
ArrayList<T extends Object>
==ArrayList<T>
).Malgré cela, il rend les tableaux génériques inutilisables et provoque des erreurs étranges pour les types bruts:
cela provoque beaucoup d'ambiguïtés
se référer à la même fonction:
et donc la surcharge de méthode générique ne peut pas être utilisée en Java.
la source
J'ai rencontré l'effacement de type dans Android. En production, nous utilisons gradle avec l'option minify. Après la minification, j'ai une exception fatale. J'ai fait une fonction simple pour montrer la chaîne d'héritage de mon objet:
Et il y a deux résultats de cette fonction:
Code non minifié:
Code réduit:
Ainsi, dans le code minifié, les classes paramétrées réelles sont remplacées par des types de classes brutes sans aucune information de type. Comme solution pour mon projet, j'ai supprimé tous les appels de réflexion et les ai remplacés avec des types de paramètres explicites passés en arguments de fonction.
la source