CodeGym /Blog Java /Random-FR /Type d'effacement
Auteur
Andrey Gorkovenko
Frontend Engineer at NFON AG

Type d'effacement

Publié dans le groupe Random-FR
Salut! Nous poursuivons notre série de cours sur les génériques. Nous avons déjà eu une idée générale de ce qu'ils sont et pourquoi ils sont nécessaires. Aujourd'hui, nous en apprendrons davantage sur certaines des fonctionnalités des génériques et sur leur utilisation. Allons-y! Effacement de type - 1Dans la dernière leçon , nous avons parlé de la différence entre les types génériques et les types bruts . Un type brut est une classe générique dont le type a été supprimé.

List list = new ArrayList();
Voici un exemple. Ici, nous n'indiquons pas quel type d'objets seront placés dans notre fichier List. Si nous essayons de créer un tel Listet d'y ajouter des objets, nous verrons un avertissement dans IDEA :

"Unchecked call to add(E) as a member of raw type of java.util.List".
Mais nous avons également parlé du fait que les génériques n'apparaissaient que dans Java 5. Au moment de la sortie de cette version, les programmeurs avaient déjà écrit un tas de code en utilisant des types bruts, donc cette fonctionnalité du langage ne pouvait pas s'arrêter de fonctionner, et la possibilité de créer des types bruts en Java a été préservé. Cependant, le problème s'est avéré être plus répandu. Comme vous le savez, le code Java est converti dans un format compilé spécial appelé bytecode, qui est ensuite exécuté par la machine virtuelle Java. Mais si nous mettions des informations sur les paramètres de type dans le bytecode pendant le processus de conversion, cela casserait tout le code précédemment écrit, car il n'y avait pas de paramètres de type avant Java 5 ! Lorsque vous travaillez avec des génériques, il y a un concept très important dont vous devez vous souvenir. C'est ce qu'on appelle l'effacement de type. Cela signifie qu'une classe ne contient aucune information sur un paramètre de type. Ces informations ne sont disponibles que lors de la compilation et sont effacées (deviennent inaccessibles) avant l'exécution. Si vous essayez de mettre le mauvais type d'objet dans votre fichier List<String>, le compilateur générera une erreur. C'est exactement ce que les créateurs du langage veulent réaliser lorsqu'ils ont créé des génériques : des vérifications au moment de la compilation. Mais lorsque tout votre code Java se transforme en bytecode, il ne contient plus d'informations sur les paramètres de type. En bytecode, votre List<Cat>liste de chats n'est pas différente des List<String>chaînes. En bytecode, rien ne dit qu'il catss'agit d'une liste d' Catobjets. Ces informations sont effacées lors de la compilation — seul le fait que vous ayez une List<Object> catsliste se retrouvera dans le bytecode du programme. Voyons comment cela fonctionne :

public class TestClass<T> {

   private T value1;
   private T value2;

   public void printValues() {
       System.out.println(value1);
       System.out.println(value2);
   }

   public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) {
       TestClass<T> result = new TestClass<>();
       result.value1 = (T) o1;
       result.value2 = (T) o2;
       return result;
   }

   public static void main(String[] args) {
       Double d = 22.111;
       String s = "Test String";
       TestClass<Integer> test = createAndAdd2Values(d, s);
       test.printValues();
   }
}
Nous avons créé notre propre TestClassclasse générique. C'est assez simple : il s'agit en fait d'une petite "collection" de 2 objets, qui sont stockés immédiatement lors de la création de l'objet. Il a 2 Tchamps. Lorsque la createAndAdd2Values()méthode est exécutée, les deux objets passés ( Object aet Object bdoivent être convertis en Ttype puis ajoutés à l' TestClassobjet. Dans la main()méthode, nous créons un TestClass<Integer>, c'est-à-dire que l' Integerargument type remplace le Integerparamètre type. Nous passons également a Doubleet a Stringà la createAndAdd2Values()méthode. Pensez-vous que notre programme fonctionnera ? Après tout, nous avons spécifié Integercomme argument de type, mais a Stringne peut certainement pas être transtypé en anInteger ! Exécutons lemain()méthode et vérification. Sortie console :

22.111 
Test String
C'était inattendu! Pourquoi est-ce arrivé? C'est le résultat de l'effacement du type. Les informations sur l' Integerargument de type utilisé pour instancier notre TestClass<Integer> testobjet ont été effacées lors de la compilation du code. Le champ devient TestClass<Object> test. Nos arguments Doubleet Stringont été facilement convertis en Objectobjets (ils ne sont pas convertis en Integerobjets comme prévu !) et ajoutés discrètement à TestClass. Voici un autre exemple simple mais très révélateur d'effacement de type :

import java.util.ArrayList;
import java.util.List;

public class Main {

   private class Cat {

   }

   public static void main(String[] args) {

       List<String> strings = new ArrayList<>();
       List<Integer> numbers = new ArrayList<>();
       List<Cat> cats = new ArrayList<>();

       System.out.println(strings.getClass() == numbers.getClass());
       System.out.println(numbers.getClass() == cats.getClass());

   }
}
Sortie console :

true 
true
Il semble que nous ayons créé des collections avec trois arguments de types différents - String, Integer, et notre propre Catclasse. Mais lors de la conversion en bytecode, les trois listes deviennent List<Object>, donc lorsque le programme s'exécute, il nous indique que nous utilisons la même classe dans les trois cas.

Effacement de type lors de l'utilisation de tableaux et de génériques

Il y a un point très important qui doit être clairement compris lorsque l'on travaille avec des tableaux et des classes génériques (telles que List). Vous devez également en tenir compte lors du choix des structures de données pour votre programme. Les génériques sont soumis à l'effacement de type. Les informations sur les paramètres de type ne sont pas disponibles lors de l'exécution. En revanche, les tableaux connaissent et peuvent utiliser des informations sur leur type de données lorsque le programme est en cours d'exécution. Tenter de mettre un type invalide dans un tableau provoquera la levée d'une exception :

public class Main2 {

   public static void main(String[] args) {

       Object x[] = new String[3];
       x[0] = new Integer(222);
   }
}
Sortie console :

Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
Parce qu'il y a une si grande différence entre les tableaux et les génériques, ils peuvent avoir des problèmes de compatibilité. Surtout, vous ne pouvez pas créer un tableau d'objets génériques ou même simplement un tableau paramétré. Cela vous semble-t-il un peu déroutant ? Nous allons jeter un coup d'oeil. Par exemple, vous ne pouvez rien faire de cela en Java :

new List<T>[]
new List<String>[]
new T[]
Si nous essayons de créer un tableau d' List<String>objets, nous obtenons une erreur de compilation qui se plaint de la création d'un tableau générique :

import java.util.List;

public class Main2 {

   public static void main(String[] args) {

       // Compilation error! Generic array creation
       List<String>[] stringLists = new List<String>[1];
   }
}
Mais pourquoi est-ce fait ? Pourquoi la création de tels tableaux n'est-elle pas autorisée ? C'est tout pour assurer la sécurité du type. Si le compilateur nous laissait créer de tels tableaux d'objets génériques, nous pourrions nous créer une tonne de problèmes. Voici un exemple simple tiré du livre "Effective Java" de Joshua Bloch :

public static void main(String[] args) {

   List<String>[] stringLists = new List<String>[1];  //  (1)
   List<Integer> intList = Arrays.asList(42, 65, 44);  //  (2)
   Object[] objects = stringLists;  //  (3)
   objects[0] = intList;  //  (4)
   String s = stringLists[0].get(0);  //  (5)
}
Imaginons que la création d'un tableau comme List<String>[] stringListssoit autorisée et ne génère pas d'erreur de compilation. Si c'était vrai, voici certaines choses que nous pourrions faire : À la ligne 1, nous créons un tableau de listes : List<String>[] stringLists. Notre tableau contient un List<String>. A la ligne 2, nous créons une liste de nombres : List<Integer>. À la ligne 3, nous affectons our List<String>[]à une Object[] objectsvariable. Le langage Java le permet : un tableau d' Xobjets peut stocker Xdes objets et des objets de toutes les sous-classes X. En conséquence, vous pouvez mettre n'importe quoi dans un Objecttableau. À la ligne 4, nous remplaçons le seul élément du objects()tableau (a List<String>) par a List<Integer>. Ainsi, nous avons mis a List<Integer>dans un tableau qui n'était destiné qu'à stockerList<String>objets! Nous rencontrerons une erreur uniquement lorsque nous exécuterons la ligne 5. A ClassCastExceptionsera lancé à l'exécution. En conséquence, une interdiction de créer de tels tableaux a été ajoutée à Java. Cela nous permet d'éviter de telles situations.

Comment puis-je contourner l'effacement de type ?

Eh bien, nous avons appris l'effacement de type. Essayons de tromper le système ! :) Tâche : Nous avons une TestClass<T>classe générique. Nous voulons écrire une createNewT()méthode pour cette classe qui créera et renverra un nouvel Tobjet. Mais c'est impossible, non ? Toutes les informations sur le Ttype sont effacées lors de la compilation et lors de l'exécution, nous ne pouvons pas déterminer le type d'objet que nous devons créer. Il y a en fait une façon délicate de le faire. Vous vous souvenez probablement que Java a une Classclasse. Nous pouvons l'utiliser pour déterminer la classe de n'importe lequel de nos objets :

public class Main2 {

   public static void main(String[] args) {

       Class classInt = Integer.class;
       Class classString = String.class;

       System.out.println(classInt);
       System.out.println(classString);
   }
}
Sortie console :

class java.lang.Integer 
class java.lang.String
Mais voici un aspect dont nous n'avons pas parlé. Dans la documentation Oracle, vous verrez que la classe Class est générique ! Effacement de type - 3

https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html

La documentation indique "T - le type de la classe modélisée par cet objet Class". En traduisant cela du langage de la documentation au discours clair, nous comprenons que la classe de l' Integer.classobjet n'est pas seulement Class, mais plutôt Class<Integer>. Le type de l' String.classobjet n'est pas seulement Class, mais plutôt Class<String>, etc. Si ce n'est toujours pas clair, essayez d'ajouter un paramètre de type à l'exemple précédent :

public class Main2 {

   public static void main(String[] args) {

       Class<Integer> classInt = Integer.class;
       // Compilation error!
       Class<String> classInt2 = Integer.class;
      
      
       Class<String> classString = String.class;
       // Compilation error!
       Class<Double> classString2 = String.class;
   }
}
Et maintenant, en utilisant ces connaissances, nous pouvons contourner l'effacement de type et accomplir notre tâche ! Essayons d'obtenir des informations sur un paramètre de type. Notre argument de type sera MySecretClass:

public class MySecretClass {

   public MySecretClass() {

       System.out.println("A MySecretClass object was created successfully!");
   }
}
Et voici comment nous utilisons notre solution en pratique :

public class TestClass<T> {

   Class<T> typeParameterClass;

   public TestClass(Class<T> typeParameterClass) {
       this.typeParameterClass = typeParameterClass;
   }

   public T createNewT() throws IllegalAccessException, InstantiationException {
       T t = typeParameterClass.newInstance();
       return t;
   }

   public static void main(String[] args) throws InstantiationException, IllegalAccessException {

       TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
       MySecretClass secret = testString.createNewT();

   }
}
Sortie console :

A MySecretClass object was created successfully!
Nous venons de passer l'argument de classe requis au constructeur de notre classe générique :

TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Cela nous a permis de sauvegarder les informations sur l'argument de type, l'empêchant d'être entièrement effacé. Nous avons ainsi pu créer unTobjet! :) Avec cela, la leçon d'aujourd'hui touche à sa fin. Vous devez toujours vous souvenir de l'effacement de type lorsque vous travaillez avec des génériques. Cette solution de contournement ne semble pas très pratique, mais vous devez comprendre que les génériques ne faisaient pas partie du langage Java lors de sa création. Cette fonctionnalité, qui nous aide à créer des collections paramétrées et à détecter les erreurs lors de la compilation, a été ajoutée ultérieurement. Dans certains autres langages qui incluaient des génériques de la première version, il n'y a pas d'effacement de type (par exemple, en C#). Au fait, nous n'avons pas fini d'étudier les génériques ! Dans la leçon suivante, vous vous familiariserez avec quelques fonctionnalités supplémentaires des génériques. Pour l'instant, il serait bon de résoudre quelques tâches ! :)
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION