web-dev-qa-db-fra.com

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

J'ai entendu parler de l'effacement de types de Java sur le site Web d'Oracle .

Quand l'effacement de type a-t-il lieu? à la compilation ou à l'exécution? Quand la classe est chargée? Quand la classe est instanciée?

Un grand nombre de sites (y compris le didacticiel officiel mentionné ci-dessus) affirment que l'effacement du 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é de types lorsqu'une méthode utilisant des génériques est appelée sans informations de type ou informations de type incorrect?

Prenons l'exemple suivant: Disons 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 brut): empty(new Box()). Si nous compilons B.Java Avec A.class Dans le chemin de classe, javac est suffisamment intelligent pour émettre un avertissement. Donc A.class a certaines informations de type qui y sont stockées.

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

Mon hypothèse serait que l'effacement de type se produise lorsque la classe est chargée, mais il ne s'agit que d'une supposition. Alors, quand ça se passe?

227
afryingpan

L'effacement de type s'applique à la tilisation des 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 quand les génériques sont tilisés, ils sont convertis lors des contrôles au moment de la compilation et des exécutions 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 est impossible de savoir que T=String pour l'objet liste - cette information a disparu.

... mais le List<T> interface 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 savoir que T=String pour l'objet liste lui-même.

230
Jon Skeet

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 appelé type erasure. Tout se passe au moment de la compilation.

Remarque: Contrairement aux croyances de la majorité des Java, il est possible de conserver les informations de type à la compilation et de les récupérer cette information au moment de l'exécution, malgré des restrictions très strictes, autrement dit: Java fournit des génériques réifiés de manière très restreinte .

En ce qui concerne l'effacement de type

Notez que, au moment de la compilation, le compilateur dispose de toutes les informations de type, mais que ces informations sont intentionnellement supprimées en général lorsque le code octet est généré, dans le cadre d'un processus appelé type effacement. . Ceci est dû aux problèmes de compatibilité: les concepteurs de langages souhaitaient une compatibilité totale avec le code source et entre les versions de la plate-forme. Si elle était mise en œuvre différemment, vous devrez recompiler vos applications héritées lors de la migration vers les 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 les informations de type à la compilation, vous devez utiliser des classes anonymes. Le problème est le suivant: dans le cas très particulier des classes anonymes, il est possible de récupérer des informations de type compile-time complètes à 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 les récupérer.

J'ai écrit un article sur ce sujet:

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

Une remarque sur la technique décrite dans l'article ci-dessus est que cette technique est obscure pour la majorité des développeurs. Bien que cela 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.

90
Richard Gomes

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 retourne 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> À la méthode empty(Box<T extends Number>).

L’API est compliquée, mais vous pouvez inspecter ces informations de type via l’API de réflexion avec des méthodes telles que getGenericParameterTypes , getGenericReturnType et, pour champs, getGenericType .

Si votre code utilise un type générique, le compilateur insère les conversions nécessaires (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 une new Box<Integer>(), il n'y a aucune information sur la classe Integer dans l'objet Box.

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

33
erickson

Génériques dans Java Langage est un très bon guide sur ce sujet.

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

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

13
Eugene Yokota

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

De plus, le compilateur conserve les informations de type sur les paramètres d'un appel de méthode, que vous pouvez récupérer via une réflexion.

Ce guide est le meilleur que j'ai trouvé sur le sujet.

6
Vinko Vrsalovic

Le terme "type effacement" n'est pas vraiment la description correcte du problème de Java avec les génériques. L'effacement de types n'est pas en soi une mauvaise chose, en effet il est très nécessaire pour la performance et est souvent utilisé dans plusieurs langages tels que C++, Haskell, D.

Avant de vous dégoûter, rappelez-vous s'il vous plaît la définition correcte de l'effacement de type de Wiki

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

le type effacement 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 l'élimination des balises de type créées au moment de la conception ou des balises de type inférées lors de la compilation, de sorte que le programme compilé en code binaire ne contienne aucune balise de type. Et c'est le cas pour chaque langage 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 sous-typables, tout type dans de nombreuses langues, types d'union). La raison de l'effacement des types est que les programmes sont transformés en un langage uni-typé (langage binaire n'autorisant que les bits), car les types sont des abstractions et affirment une structure pour ses valeurs et la sémantique appropriée pour les gérer.

Donc, ceci est en retour, une chose naturelle normale.

Le problème de Java est différent et lié à sa réification.

Les déclarations souvent faites à propos de Java n’a pas de génériques réifiés) sont également fausses.

Java se réifie, mais de manière erronée à cause de la compatibilité ascendante.

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 en 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:

Un 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, 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 des types abstraits avec leurs limites, c'est-à-dire qu'ils produisent 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, cela 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 crée 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)

par conséquent, la surcharge de méthode générique ne peut pas être utilisée en Java.

4
iconfly

J'ai rencontré avec effacement de type dans Android. En production, nous utilisons gradle avec l'option minify. Après minification, j'ai une exception fatale. J'ai créé 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 réduit, les classes paramétrées réelles sont remplacées par des types de classes brutes sans aucune information de type. En tant que solution pour mon projet, j'ai supprimé tous les appels de réflexion et les ai répliqués avec des types de paramètres explicites passés en arguments de fonction.

2
porfirion