Effacement de type génériques Java: quand et que se passe-t-il?

238

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 Aa une méthode, empty(Box<? extends Number> b). Nous compilons A.javaet 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 Bqui invoque la méthode emptyavec un argument non paramétrées (type brut): empty(new Box()). Si nous compilons B.javaavec A.classdans 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?

Radiodef
la source
2
Une version plus "générique" de cette question: stackoverflow.com/questions/313584/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@afryingpan: L'article mentionné dans ma réponse explique en détail comment et quand l'effacement de type se produit. Il explique également quand les informations de type sont conservées. En d'autres termes: les génériques réifiés sont disponibles en Java, contrairement à une croyance répandue. Voir: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Richard Gomes

Réponses:

240

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:

List<String> list = new ArrayList<String>();
list.add("Hi");
String x = list.get(0);

est compilé en

List list = new ArrayList();
list.add("Hi");
String x = (String) list.get(0);

Au moment de l'exécution, il n'y a aucun moyen de savoir que T=Stringpour 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écouvrir T=Stringpour l'objet de liste lui-même.

Jon Skeet
la source
6
Non, même lors de l'utilisation d'un type générique, des métadonnées peuvent être disponibles au moment de l'exécution. Une variable locale n'est pas accessible via Reflection, mais pour un paramètre de méthode déclaré comme "List <String> l", il y aura des métadonnées de type lors de l'exécution, disponibles via l'API Reflection. Oui, "l'effacement de type" n'est pas aussi simple que beaucoup de gens le pensent ...
Rogério
4
@Rogerio: Comme je l'ai répondu à votre commentaire, je pense que vous vous confondez entre pouvoir obtenir le type d'une variable et être en mesure d'obtenir le type d'un objet . L'objet lui-même ne connaît pas son argument type, même si le champ le sait.
Jon Skeet
Bien sûr, en regardant simplement l'objet lui-même, vous ne pouvez pas savoir qu'il s'agit d'une liste <String>. Mais les objets n'apparaissent pas de nulle part. Ils sont créés localement, transmis comme argument d'invocation de méthode, retournés comme valeur de retour d'un appel de méthode, ou lus dans un champ d'un objet ... Dans tous ces cas, vous POUVEZ savoir à l'exécution quel est le type générique, soit implicitement ou en utilisant l'API Java Reflection.
Rogério
13
@Rogerio: Comment savez-vous d'où vient l'objet? Si vous avez un paramètre de type, 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.
Jon Skeet
Mais l'effacement de type EST une toute petite chose qui n'affecte pas vraiment les développeurs! Bien sûr, je ne peux pas parler pour les autres, mais dans MON expérience, cela n'a jamais été un gros problème. En fait, je profite des informations de type d'exécution dans la conception de mon API de simulation Java (JMockit); ironiquement, les API de simulation .NET semblent profiter moins du système de type générique disponible en C #.
Rogério
99

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.

Richard Gomes
la source
1
@ will824: J'ai considérablement amélioré la réponse et j'ai ajouté un lien vers certains cas de test. Santé :)
Richard Gomes
1
En fait, ils n'ont pas maintenu la compatibilité binaire et source: oracle.com/technetwork/java/javase/compatibility-137462.html Où puis-je en savoir plus sur leur intention? Les documents disent qu'il utilise l'effacement de type, mais ne dit pas pourquoi.
Dzmitry Lazerka
@Richard En effet, excellent article! Vous pouvez ajouter que les classes locales fonctionnent également et que, dans les deux cas (classes anonymes et locales), les informations sur l'argument de type souhaité ne sont conservées qu'en cas d'accès direct et new Box<String>() {};non en cas d'accès indirect void foo(T) {...new Box<T>() {};...}car le compilateur ne conserve pas les informations de type pour la déclaration de méthode englobante.
Yann-Gaël Guéhéneuc du
J'ai corrigé un lien cassé vers mon article. Je décompose lentement ma vie et récupère mes données. :-)
Richard Gomes
33

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>à la empty(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, getGenericReturnTypeet, 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 la Integerclasse dans l' Boxobjet.

La FAQ d'Angelika Langer est la meilleure référence que j'ai vue pour Java Generics.

erickson
la source
2
En fait, c'est le type générique formel des champs et des méthodes qui sont compilés dans la classe, c'est-à-dire typicall "T". Pour obtenir le type réel du type générique, vous devez utiliser le "truc de classe anonyme" .
Yann-Gaël Guéhéneuc
13

Les génériques en langage Java sont un très bon guide sur ce sujet.

Les génériques sont implémentés par le compilateur Java comme une conversion frontale appelée effacement. Vous pouvez (presque) la considérer comme une traduction de source à source, par laquelle la version générique de loophole()est convertie en version non générique.

Donc, c'est au moment de la compilation. La JVM ne saura jamais lequel ArrayListvous 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?

Eugene Yokota
la source
6

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

Box<String> b = new Box<String>();
String x = b.getDefault();

est converti en

Box b = new Box();
String x = (String) b.getDefault();

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.

Vinko Vrsalovic
la source
6

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 fait référence au processus de chargement par lequel les annotations de type explicites sont supprimées d'un programme, avant son exécution au moment de l'exécution

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 est le processus par lequel une idée abstraite d'un programme informatique est transformée en un modèle de données explicite ou un autre objet créé dans un langage de programmation.

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:

ArrayList<T>
{
    T[] elems;
    ...//methods
}

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:

ArrayList<Integer>
{
    Integer[] elems;
}

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:

ArrayList
{
    Object[] elems;
}

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:

List<String> l= List.<String>of("h","s");
List lRaw=l
l.add(new Object())
String s=l.get(2) //Cast Exception

cela provoque beaucoup d'ambiguïtés

void function(ArrayList<Integer> list){}
void function(ArrayList<Float> list){}
void function(ArrayList<String> list){}

se référer à la même fonction:

void function(ArrayList list)

et donc la surcharge de méthode générique ne peut pas être utilisée en Java.

iconfly
la source
2

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:

public static void printSuperclasses(Class clazz) {
    Type superClass = clazz.getGenericSuperclass();

    Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
    Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));

    while (superClass != null && clazz != null) {
        clazz = clazz.getSuperclass();
        superClass = clazz.getGenericSuperclass();

        Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
        Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));
    }
}

Et il y a deux résultats de cette fonction:

Code non minifié:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: com.example.App.SortedListWrapper<com.example.App.Models.User>

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: android.support.v7.util.SortedList$Callback<T>

D/Reflection: this class: android.support.v7.util.SortedList$Callback
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

Code réduit:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: class com.example.App.SortedListWrapper

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: class android.support.v7.g.e

D/Reflection: this class: android.support.v7.g.e
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

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.

porfirion
la source