Java generics type erasure: quand et que se passe-t-il?


J'ai lu à propos de l'effacement de type de 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?

Beaucoup de 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 appelée sans type informations ou informations de type erronées?

Considérons l'exemple suivant: supposons que la classe 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 appelle la méthode empty avec un argument non paramétré (type raw): 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. Donc A.class a des informations de type stockées dedans.

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());
    }
}

Ma supposition c'est ce type d'effacement se produit lorsque la classe est chargée, mais c'est juste une supposition. Alors quand est-ce arrivé?

Author: Radiodef, 2008-12-04

7 answers

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 les génériques sontutilisés , ils sont convertis en vérifications au moment de la compilation et en moulages au moment de l'exécution. Donc ce code:

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

Est compilé dans

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 découvrir que T=String pour l'objet list - cette information a disparu.

... mais l'interface List<T> elle-même s'annonce toujours comme 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 découvrir que T=String pour l'objet list lui-même.

 210
Author: Jon Skeet, 2013-06-06 01:21:56

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 type d'effacement. Tout se passe au moment de la compilation.

Note: Contrairement aux croyances de la majorité des développeurs Java, il est possible de conserver les informations de type à la compilation et de récupérer ces informations lors de l'exécution, bien que de manière très restreinte. En d'autres termes: Java fournit les 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 complètes mais que ces informations sont intentionnellement suppriméesen général lorsque le code octet est généré, dans un processus appelétype erasure . Ceci est 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é complète du code octet 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 conservées (compatibilité du code source) et vous n'avez pas besoin de recompiler quoi que ce soit (compatibilité binaire).

Concernant les génériques réifiés en Java

Si vous devez conserver les informations de type à la compilation, vous devez utiliser des classes anonymes. Le point est: dans le très spécial dans le cas des classes anonymes, il est possible de récupérer des informations de type complètes à la compilation au moment de l'exécution, ce qui signifie en d'autres termes: 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 à ce sujet objet:

Http://rgomes-info.blogspot.co.uk/2013/12/using-typetokens-to-retrieve-generic.html

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é 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 procure.

Exemple de code

L'article ci-dessus contient des liens vers un exemple de code.

 87
Author: Richard Gomes, 2016-01-26 11:12:50

Si vous avez un champ qui est un 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.

Cette information est ce que le compilateur utilise pour vous dire que vous ne pouvez pas passer un Box<String> de la empty(Box<T extends Number>) méthode.

L'API est compliquée, mais vous pouvez inspecter ces informations de type via l'API reflection 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 moulages au besoin (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 classe Integer dans l'objet Box.

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

 31
Author: erickson, 2008-12-04 06:50:05

Generics en langage Java est un très bon guide sur ce sujet.

Les génériques sont implémentés par Java compilateur en tant que conversion frontale appelé à l'effacement. Vous pouvez (presque) pense que comme une source-à-source traduction, par laquelle le générique la version de {[0] } est convertie en le non-générique.

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 Qu'est-ce que le concept d'effacement dans les génériques en Java?

 11
Author: Eugene Yokota, 2017-06-08 19:45:18

L'effacement de type se produit au moment de la compilation. Ce que l'effacement de type signifie, c'est 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 obtenir des avertissements non pas parce que le compilateur sait de quel type est le générique, mais au contraire, parce qu'il n'en sait pas assez, il ne peut donc pas garantir la sécurité du type.

En outre, le 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 de est le meilleur que j'ai trouvé sur le sujet.

 6
Author: Vinko Vrsalovic, 2008-12-04 06:40:54

J'ai rencontré avec type erasure 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 minifié:

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 une solution pour mon projet j'ai supprimé tous les appels de réflexion et les ai replcés avec des types de paramètres explicites passés dans les arguments de fonction.

 2
Author: porfirion, 2016-10-14 12:47:33

Le terme "type erasure" 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 vous dégoûter, veuillez rappeler la définition correcte de type erasure de Wiki

Qu'est-ce que l'effacement de type?

Type erasure fait référence au processus de temps de chargement par lequel le type explicite les annotations sont supprimées d'un programme, avant qu'il ne soit exécuté à l'exécution

L'effacement de type signifie jeter les balises de type créées au moment de la conception ou les balises de type inférées 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 compilant 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-typable, 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é (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 causé à la façon dont il se réifie.

Les déclarations souvent faites sur Java ne ne pas avoir réifié les génériques est également faux.

Java reify, mais d'une mauvaise manière en raison de la rétrocompatibilité.

Qu'est-Ce que la réification?

Et 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 béton (Type de béton) par spécialisation.

, Nous illustrons cela par un exemple simple:

Une ArrayList 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é avec un type concret, disons Entier:

ArrayList<Integer>
{
    Integer[] elems;
}

ArrayList<Integer> est vraiment un type.

, Mais c'est exactement la chose que Java ne fonctionne pas!!!, au lieu de cela, ils réifient constamment des types abstraits avec leurs limites, c'est-à-dire en produisant le même type de béton indépendamment des paramètres transmis 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

, Il 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.

 1
Author: iconfly, 2018-07-31 12:50:26