JavaRush /Blog Java /Random-FR /Types d'effacement

Types d'effacement

Publié dans le groupe Random-FR
Bonjour! Nous poursuivons notre série de conférences sur les génériques. Auparavant , nous avons compris en termes généraux ce que c'est et pourquoi cela est nécessaire. Aujourd’hui, nous allons parler de certaines fonctionnalités des génériques et examiner quelques pièges liés à leur utilisation. Aller! Types d'effacement - 1Dans la dernière leçon, nous avons parlé de la différence entre les types génériques et les types bruts . Au cas où vous l'auriez oublié, Raw Type est une classe générique dont son type a été supprimé.
List list = new ArrayList();
Voici un exemple. Ici nous ne précisons pas quel type d'objets seront placés dans notre List. Si nous essayons d'en créer un 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 la version du langage Java 5. Au moment de sa sortie, les programmeurs avaient écrit beaucoup de code en utilisant les types bruts, et pour qu'il ne cesse de fonctionner, la possibilité de créer et travailler avec des types bruts en Java a été préservé. Cependant, ce problème s’est avéré beaucoup plus vaste. Le code Java, comme vous le savez, est converti en bytecode spécial, qui est ensuite exécuté par la machine virtuelle Java. Et si pendant le processus de traduction nous placions des informations sur les types de paramètres dans le bytecode, cela briserait tout le code écrit précédemment, car avant Java 5, aucun type de paramètre n'existait ! Lorsque vous travaillez avec des génériques, vous devez garder à l’esprit une caractéristique très importante. C'est ce qu'on appelle l'effacement de type. Son essence réside dans le fait qu'aucune information sur son type de paramètre n'est stockée à l'intérieur de la classe. Ces informations sont disponibles uniquement au stade de la compilation et sont effacées (deviennent inaccessibles) au moment de l'exécution. Si vous essayez de mettre un objet du mauvais type dans votre List<String>, le compilateur générera une erreur. C'est précisément ce que les créateurs du langage ont réalisé en créant des génériques : des contrôles au stade de la compilation. Mais lorsque tout le code Java que vous écrivez se transforme en bytecode, il n'y aura aucune information sur les types de paramètres. À l'intérieur du bytecode, votre liste de List<Cat>chats ne différera pas des List<String>chaînes. Rien dans le bytecode ne dira qu'il catss'agit d'une liste d'objets Cat. Les informations à ce sujet seront effacées lors de la compilation, et seules les informations selon lesquelles vous avez une certaine liste dans votre programme entreront dans le code d'octet List<Object> cats. Voyons voir comment ça 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 classe générique TestClass. C'est assez simple : il s'agit essentiellement d'une petite « collection » de 2 objets, qui y sont placés immédiatement lors de la création de l'objet. Il a 2 objets comme champs T. Lorsque la méthode est exécutée, createAndAdd2Values()les deux objets passés doivent être convertis Object aen Object bnotre type T, après quoi ils seront ajoutés à l'objet TestClass. Dans la méthode main()que nous créons TestClass<Integer>, c'est-à-dire dans la qualité, Tnous aurons Integer. Mais en même temps, createAndAdd2Values()on passe un nombre Doubleet un objet à la méthode String. Pensez-vous que notre programme fonctionnera ? Après tout, nous avons spécifié comme paramètre type Integer, mais Stringil ne peut certainement pas être converti en Integer! Exécutons la méthode main()et vérifions. Sortie de la console :  22.111 Chaîne de test Résultat inattendu ! Pourquoi est-ce arrivé? Précisément à cause de l'effacement de type. Lors de la compilation du code, les informations sur le type de paramètre Integerde notre objet TestClass<Integer> testont été effacées. Il s'est transformé en TestClass<Object> test. Nos paramètres ont été transformés en sans problème Double( et non en , comme nous l'espérions !) et ont été discrètement ajoutés à . Voici un autre exemple simple mais très illustratif d'effacement de type : StringObjectIntegerTestClass
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 de la console : true true Il semblerait que nous ayons créé des collections avec trois types de paramètres différents : String, Integeret la classe que nous avons créée Cat. Mais lors de la conversion en bytecode, les trois listes se sont transformées en List<Object>, donc une fois exécuté, le programme nous indique que dans les trois cas, nous utilisons la même classe.

Tapez l'effacement lorsque vous travaillez avec des tableaux et des 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 génériques (par exemple, List). Cela vaut également la peine d'être pris en compte lors du choix d'une structure de données pour votre programme. Les génériques sont sujets à l'effacement de type. Les informations sur le type de paramètre ne sont pas disponibles pendant l'exécution du programme. En revanche, les tableaux connaissent et peuvent utiliser des informations sur leur type de données lors de l'exécution du programme. Essayer de mettre une valeur du mauvais type dans un tableau lèvera une exception :
public class Main2 {

   public static void main(String[] args) {

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

Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
En raison de la grande différence entre les baies et les génériques, ils peuvent rencontrer des problèmes de compatibilité. Tout d’abord, vous ne pouvez pas créer un tableau d’objets génériques, ni même simplement un tableau typé. Cela semble un peu déroutant ? Regardons de plus près. Par exemple, vous ne pouvez rien faire de tout cela en Java :
new List<T>[]
new List<String>[]
new T[]
Si nous essayons de créer un tableau de listes List<String>, nous obtenons une erreur de compilation générique de création de tableau :
import java.util.List;

public class Main2 {

   public static void main(String[] args) {

       //ошибка компиляции! Generic array creation
       List<String>[] stringLists = new List<String>[1];
   }
}
Mais pourquoi cela a-t-il été fait ? Pourquoi la création de tels tableaux est-elle interdite ? Tout cela pour garantir la sécurité du type. Si le compilateur nous permettait de créer de tels tableaux à partir d'objets génériques, nous pourrions avoir beaucoup de problèmes. Voici un exemple simple tiré du livre de Joshua Bloch « Effective Java » :
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 de tableaux List<String>[] stringListssoit autorisée et que le compilateur ne se plaint pas. Voici ce que nous pourrions faire dans ce cas : À la ligne 1, nous créons un tableau de feuilles List<String>[] stringLists. Notre tableau en contient un List<String>. Sur la ligne 2, nous créons une liste de nombres List<Integer>. À la ligne 3, nous affectons notre tableau List<String>[]à une variable Object[] objects. Le langage Java vous permet de faire cela : Xvous pouvez placer à la fois les objets Xet les objets de toutes les classes enfants dans un tableau d'objets Х. En conséquence, Objectsvous pouvez mettre n'importe quoi dans le tableau. À la ligne 4, nous remplaçons l'unique élément du tableau objects (List<String>)par une liste List<Integer>. En conséquence, nous avons placé List<Integer>dans notre tableau, qui était uniquement destiné au stockage List<String>! Nous ne rencontrerons une erreur que lorsque le code atteint la ligne 5. Une exception sera levée lors de l'exécution du programme ClassCastException. Par conséquent, l'interdiction de créer de tels tableaux a été introduite dans le langage 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 classe générique TestClass<T>. Nous devons y créer une méthode createNewT()qui créera et renverra un nouvel objet de type Т. Mais c’est impossible à faire, n’est-ce pas ? Toutes les informations sur le type Тseront effacées lors de la compilation et pendant l'exécution du programme, nous ne pourrons pas savoir quel type d'objet nous devons créer. En fait, il existe une méthode délicate. Vous vous souvenez probablement qu'il existe une classe en Java Class. En l'utilisant, nous pouvons obtenir 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 de la console :

class java.lang.Integer
class java.lang.String
Mais voici une fonctionnalité dont nous n'avons pas parlé. Dans la documentation Oracle, vous verrez que Class est une classe générique ! Types d'effacement - 3La documentation dit : « T est le type de classe modélisée par cet objet Class. » Si nous traduisons cela du langage de documentation en langage humain, cela signifie que la classe d'un objet Integer.classn'est pas seulement Class, mais Class<Integer>. Le type d'un objet string.classn'est pas seulement Class, 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;
       //ошибка компиляции!
       Class<String> classInt2 = Integer.class;


       Class<String> classString = String.class;
       //ошибка компиляции!
       Class<Double> classString2 = String.class;
   }
}
Et maintenant, grâce à ces connaissances, nous pouvons contourner l’effacement de type et résoudre notre problème ! Essayons d'obtenir des informations sur le type de paramètre. Son rôle sera joué par la classe MySecretClass:
public class MySecretClass {

   public MySecretClass() {

       System.out.println("Объект секретного класса успешно создан!");
   }
}
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 de la console :

Объект секретного класса успешно создан!
Nous avons simplement passé le paramètre de classe requis au constructeur de notre classe générique :
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Grâce à cela, nous avons enregistré les informations sur le type de paramètre et les avons protégées contre leur effacement. Du coup, nous avons pu créer un objet T! :) Ceci conclut la conférence d'aujourd'hui. L'effacement de type est toujours quelque chose à garder à l'esprit lorsque vous travaillez avec des génériques. Cela 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. Il s'agit d'une fonctionnalité ajoutée ultérieurement qui nous aide à créer des collections typées et à détecter les erreurs au stade de la compilation. Certains autres langages dans lesquels les génériques existent depuis la version 1 n'ont pas d'effacement de type (par exemple, C#). Mais nous n’avons pas fini d’étudier les génériques ! Dans la prochaine conférence, vous vous familiariserez avec plusieurs autres fonctionnalités liées à leur utilisation. En attendant, ce serait bien de résoudre quelques problèmes ! :)
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION