JavaRush /Blog Java /Random-FR /Génériques pour chats
Viacheslav
Niveau 3

Génériques pour chats

Publié dans le groupe Random-FR
Génériques pour chats - 1

Introduction

Aujourd’hui est une excellente journée pour se souvenir de ce que nous savons sur Java. Selon le document le plus important, c'est-à-dire Spécification du langage Java (JLS - Java Language Specifiaction), Java est un langage fortement typé, comme décrit dans le chapitre « Chapitre 4. Types, valeurs et variables ». Qu'est-ce que cela signifie? Disons que nous avons une méthode principale :
public static void main(String[] args) {
String text = "Hello world!";
System.out.println(text);
}
Un typage fort garantit que lorsque ce code est compilé, le compilateur vérifiera que si nous avons spécifié le type de la variable de texte comme String, nous n'essayons pas de l'utiliser n'importe où comme variable d'un autre type (par exemple, comme un entier) . Par exemple, si nous essayons de sauvegarder une valeur au lieu de texte 2L(c'est-à-dire long au lieu de String), nous obtiendrons une erreur au moment de la compilation :

Main.java:3: error: incompatible types: long cannot be converted to String
String text = 2L;
Ceux. Le typage fort vous permet de garantir que les opérations sur les objets sont effectuées uniquement lorsque ces opérations sont légales pour ces objets. C'est ce qu'on appelle également la sécurité de type. Comme indiqué dans le JLS, il existe deux catégories de types en Java : les types primitifs et les types référence. Vous vous souvenez des types primitifs dans l'article de synthèse : « Types primitifs en Java : ils ne sont pas si primitifs . » Les types de référence peuvent être représentés par une classe, une interface ou un tableau. Et aujourd'hui, nous nous intéresserons aux types de référence. Et commençons par les tableaux :
class Main {
  public static void main(String[] args) {
    String[] text = new String[5];
    text[0] = "Hello";
  }
}
Ce code s'exécute sans erreur. Comme nous le savons (par exemple, grâce à « Tutoriel Oracle Java : Arrays »), un tableau est un conteneur qui stocke des données d'un seul type. Dans ce cas, uniquement des lignes. Essayons d'ajouter long au tableau au lieu de String :
text[1] = 4L;
Exécutons ce code (par exemple, dans Repl.it Online Java Compiler ) et obtenons une erreur :
error: incompatible types: long cannot be converted to String
Le tableau et la sécurité des types du langage ne nous permettaient pas de sauvegarder dans un tableau ce qui ne correspondait pas au type. Il s'agit d'une manifestation de la sécurité de type. On nous a dit : « Corrigez l’erreur, mais d’ici là, je ne compilerai pas le code. » Et le plus important, c'est que cela se produit au moment de la compilation, et non au lancement du programme. Autrement dit, nous voyons les erreurs immédiatement, et non « un jour ». Et puisque nous nous sommes souvenus des tableaux, rappelons-nous également du Java Collections Framework . Là-bas, nous avions différentes structures. Par exemple, des listes. Réécrivons l'exemple :
import java.util.*;
class Main {
  public static void main(String[] args) {
    List text = new ArrayList(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(0);
  }
}
Lors de sa compilation, nous recevrons testune erreur sur la ligne d'initialisation de la variable :
incompatible types: Object cannot be converted to String
Dans notre cas, List peut stocker n'importe quel objet (c'est-à-dire un objet de type Object). Par conséquent, le compilateur déclare qu’il ne peut pas assumer une telle responsabilité. Par conséquent, nous devons spécifier explicitement le type que nous obtiendrons dans la liste :
String test = (String) text.get(0);
Cette indication est appelée conversion de type ou conversion de type. Et tout fonctionnera bien maintenant jusqu'à ce que nous essayions d'obtenir l'élément à l'index 1, car il est de type Long. Et nous obtiendrons une erreur équitable, mais déjà pendant l'exécution du programme (au Runtime) :

type conversion, typecasting
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
Comme nous pouvons le constater, il existe ici plusieurs inconvénients importants. Premièrement, nous sommes obligés de « convertir » la valeur obtenue de la liste vers la classe String. D'accord, c'est moche. Deuxièmement, en cas d'erreur, nous la verrons uniquement lors de l'exécution du programme. Si notre code était plus complexe, nous ne détecterions peut-être pas immédiatement une telle erreur. Et les développeurs ont commencé à réfléchir à la manière de rendre le travail dans de telles situations plus facile et le code plus clair. Et ils sont nés - Génériques.
Génériques pour chats - 2

Génériques

Donc les génériques. Qu'est-ce que c'est? Un générique est une manière particulière de décrire les types utilisés, que le compilateur de code peut utiliser dans son travail pour garantir la sécurité des types. Cela ressemble à ceci :
Génériques pour chats - 3
Voici un court exemple et une explication :
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> text = new ArrayList<String>(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(1);
  }
}
Dans cet exemple, nous disons que nous n'avons pas seulement List, mais List, qui fonctionne UNIQUEMENT avec des objets de type String. Et pas d'autres. Ce qui est juste indiqué entre parenthèses, on peut le stocker. De tels « supports » sont appelés « équerres », c'est-à-dire équerres. Le compilateur vérifiera gentiment pour nous si nous avons commis des erreurs en travaillant avec une liste de chaînes (la liste est nommée texte). Le compilateur verra que nous essayons effrontément de mettre Long dans la liste String. Et au moment de la compilation, cela donnera une erreur :
error: no suitable method found for add(long)
Vous vous souvenez peut-être que String est un descendant de CharSequence. Et décidez de faire quelque chose comme :
public static void main(String[] args) {
	ArrayList<CharSequence> text = new ArrayList<String>(5);
	text.add("Hello");
	String test = text.get(0);
}
Mais ce n'est pas possible et nous obtiendrons l'erreur : error: incompatible types: ArrayList<String> cannot be converted to ArrayList<CharSequence> Cela semble étrange, car. la ligne CharSequence sec = "test";ne contient aucune erreur. Voyons cela. Ils disent de ce comportement : « Les génériques sont invariants. » Qu'est-ce qu'un « invariant » ? J'aime la façon dont cela est dit sur Wikipédia dans l'article « Covariance et contravariance » :
Génériques pour chats - 4
Ainsi, l'invariance est l'absence d'héritage entre les types dérivés. Si Cat est un sous-type d’Animals, alors Set<Cats> n’est pas un sous-type de Set<Animals> et Set<Animals> n’est pas un sous-type de Set<Cats>. À propos, il convient de dire qu'à partir de Java SE 7, le soi-disant « Diamond Operator » est apparu. Parce que les deux équerres <> sont comme un diamant. Cela nous permet d'utiliser des génériques comme celui-ci :
public static void main(String[] args) {
  List<String> lines = new ArrayList<>();
  lines.add("Hello world!");
  System.out.println(lines);
}
Sur la base de ce code, le compilateur comprend que si nous indiquons sur le côté gauche qu'il Listcontiendra des objets de type String, alors sur le côté droit nous voulons dire que nous voulons enregistrer linesun nouveau ArrayList dans une variable, qui stockera également un objet. du type spécifié sur le côté gauche. Ainsi, le compilateur du côté gauche comprend ou déduit le type du côté droit. C'est pourquoi ce comportement est appelé inférence de type ou « Type Inference » en anglais. Une autre chose intéressante à noter est les types RAW ou « types bruts ». Parce que Les génériques n'ont pas toujours existé et Java essaie de maintenir une compatibilité ascendante autant que possible. Les génériques sont alors obligés de fonctionner d'une manière ou d'une autre avec du code où aucun générique n'est spécifié. Voyons un exemple :
List<CharSequence> lines = new ArrayList<String>();
On s'en souvient, une telle ligne ne sera pas compilée en raison de l'invariance des génériques.
List<Object> lines = new ArrayList<String>();
Et celui-ci ne sera pas compilé non plus, pour la même raison.
List lines = new ArrayList<String>();
List<String> lines2 = new ArrayList();
De telles lignes seront compilées et fonctionneront. C'est en eux que sont utilisés les types bruts, c'est-à-dire types non précisés. Encore une fois, il convient de souligner que les types bruts NE DEVRAIENT PAS être utilisés dans le code moderne.
Génériques pour chats - 5

Cours dactylographiés

Donc, des cours dactylographiés. Voyons comment nous pouvons écrire notre propre classe typée. Par exemple, nous avons une hiérarchie de classes :
public static abstract class Animal {
  public abstract void voice();
}

public static class Cat extends Animal {
  public void voice(){
    System.out.println("Meow meow");
  }
}

public static class Dog extends Animal {
  public void voice(){
    System.out.println("Woof woof");
  }
}
Nous voulons créer une classe qui implémente un conteneur animal. Il serait possible d'écrire une classe qui contiendrait n'importe quel fichier Animal. C'est simple, compréhensible, MAIS... mélanger des chiens et des chats, c'est mauvais, ils ne sont pas amis les uns avec les autres. De plus, si quelqu'un reçoit un tel conteneur, il peut par erreur jeter des chats du conteneur dans une meute de chiens... et cela ne mènera à rien de bon. Et ici, les génériques nous aideront. Par exemple, écrivons l'implémentation comme ceci :
public static class Box<T> {
  List<T> slots = new ArrayList<>();
  public List<T> getSlots() {
    return slots;
  }
}
Notre classe fonctionnera avec des objets de type spécifié par un générique nommé T. C'est une sorte d'alias. Parce que Le générique est précisé dans le nom de la classe, nous le recevrons ensuite lors de la déclaration de la classe :
public static void main(String[] args) {
  Box<Cat> catBox = new Box<>();
  Cat murzik = new Cat();
  catBox.getSlots().add(murzik);
}
Comme nous pouvons le voir, nous avons indiqué que nous disposons de Box, qui ne fonctionne qu'avec Cat. Le compilateur s'est rendu compte qu'au catBoxlieu d'un générique, Tvous devez remplacer le type Catpartout où le nom du générique est spécifiéT :
Génériques pour chats - 6
Ceux. c'est grâce au Box<Cat>compilateur qu'il comprend ce que slotscela devrait être réellement List<Cat>. Car Box<Dog>à l’intérieur il y aura slots, contenant List<Dog>. Il peut y avoir plusieurs génériques dans une déclaration de type, par exemple :
public static class Box<T, V> {
Le nom du générique peut être n'importe quoi, bien qu'il soit recommandé de respecter certaines règles tacites - "Conventions de dénomination des paramètres de type" : type d'élément - E, type de clé - K, type de nombre - N, T - pour le type, V - pour type de valeur. Au fait, rappelez-vous que nous avons dit que les génériques sont invariants, c'est-à-dire ne préservent pas la hiérarchie d’héritage. En fait, nous pouvons influencer cela. Autrement dit, nous avons la possibilité de fabriquer des génériques COvariant, c'est-à-dire garder les héritages dans le même ordre. Ce comportement est appelé « Type limité », c'est-à-dire types limités. Par exemple, notre classe Boxpourrait contenir tous les animaux, alors nous déclarerions un générique comme ceci :
public static class Box<T extends Animal> {
Autrement dit, nous fixons la limite supérieure de la classe Animal. On peut également spécifier plusieurs types après le mot-clé extends. Cela signifie que le type avec lequel nous allons travailler doit être un descendant d'une classe et en même temps implémenter une interface. Par exemple:
public static class Box<T extends Animal & Comparable> {
Dans ce cas, si nous essayons de mettre Boxquelque chose dans tel qui n'est pas un héritier Animalet n'implémente pas Comparable, alors lors de la compilation, nous recevrons une erreur :
error: type argument Cat is not within bounds of type-variable T
Génériques pour chats - 7

Méthodes de saisie

Les génériques sont utilisés non seulement dans les types, mais également dans les méthodes individuelles. L'application des méthodes peut être vue dans le tutoriel officiel : " Méthodes génériques ".

Arrière-plan:

Génériques pour chats - 8
Regardons cette photo. Comme vous pouvez le voir, le compilateur examine la signature de la méthode et voit que nous prenons une classe non définie en entrée. Cela ne détermine pas par signature que nous renvoyons une sorte d'objet, c'est-à-dire Objet. Par conséquent, si nous voulons créer, disons, une ArrayList, alors nous devons faire ceci :
ArrayList<String> object = (ArrayList<String>) createObject(ArrayList.class);
Vous devez écrire explicitement que la sortie sera une ArrayList, ce qui est moche et ajoute un risque de commettre une erreur. Par exemple, nous pouvons écrire de telles bêtises et cela compilera :
ArrayList object = (ArrayList) createObject(LinkedList.class);
Pouvons-nous aider le compilateur ? Oui, les génériques nous permettent de le faire. Regardons le même exemple :
Génériques pour chats - 9
Ensuite, nous pouvons créer un objet simplement comme ceci :
ArrayList<String> object = createObject(ArrayList.class);
Génériques pour chats - 10

Carte générique

Selon le didacticiel Oracle sur les génériques, en particulier la section " Wildcards ", nous pouvons décrire un " type inconnu " avec un point d'interrogation, appelé point d'interrogation. Wildcard est un outil pratique pour atténuer certaines des limitations des génériques. Par exemple, comme nous l’avons vu précédemment, les génériques sont invariants. Cela signifie que bien que toutes les classes soient des descendants (sous-types) du type Object, ce List<любой тип>n'est pas un sous-type List<Object>. MAIS, List<любой тип>c'est un sous-type List<?>. On peut donc écrire le code suivant :
public static void printList(List<?> list) {
  for (Object elem: list) {
    System.out.print(elem + " ");
  }
  System.out.println();
}
Comme les génériques classiques (c'est-à-dire sans utilisation de caractères génériques), les génériques avec caractères génériques peuvent être limités. Le caractère générique de limite supérieure semble familier :
public static void printCatList(List<? extends Cat> list) {
  for (Cat cat: list) {
    System.out.print(cat + " ");
  }
  System.out.println();
}
Mais vous pouvez également le limiter par le caractère générique Limite inférieure :
public static void printCatList(List<? super Cat> list) {
Ainsi, la méthode commencera à accepter tous les chats, ainsi que plus haut dans la hiérarchie (jusqu'à Objet).
Génériques pour chats - 11

Tapez Effacement

En parlant de génériques, il vaut la peine de connaître le « Type Erasing ». En fait, l’effacement de type concerne le fait que les génériques sont des informations destinées au compilateur. Lors de l'exécution du programme, il n'y a plus d'informations sur les génériques, c'est ce qu'on appelle « l'effacement ». Cet effacement a pour effet de remplacer le type générique par le type spécifique. Si le générique n'avait pas de limite, alors le type Object sera remplacé. Si la bordure a été spécifiée (par exemple <T extends Comparable>), alors elle sera remplacée. Voici un exemple tiré du didacticiel Oracle : " Effacement des types génériques :
Génériques pour chats - 12
Comme cela a été dit plus haut, dans cet exemple le générique Test effacé jusqu'à sa bordure, c'est à dire avant Comparable.
Génériques pour chats - 13

Conclusion

Les génériques sont un sujet très intéressant. J'espère que ce sujet vous intéresse. Pour résumer, nous pouvons dire que les génériques sont un excellent outil que les développeurs ont reçu pour fournir au compilateur des informations supplémentaires afin de garantir la sécurité des types d'une part et la flexibilité d'autre part. Et si vous êtes intéressé, alors je vous propose de consulter les ressources qui m'ont plu : #Viacheslav
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION