JavaRush /Blog Java /Random-FR /La théorie des génériques en Java ou comment mettre les p...
Viacheslav
Niveau 3

La théorie des génériques en Java ou comment mettre les parenthèses en pratique

Publié dans le groupe Random-FR

Introduction

À partir de JSE 5.0, des génériques ont été ajoutés à l'arsenal du langage Java.
La théorie des génériques en Java ou comment mettre les parenthèses en pratique - 1

Que sont les génériques en Java ?

Les génériques (généralisations) sont des moyens spéciaux du langage Java pour implémenter une programmation généralisée : une approche particulière de la description des données et des algorithmes qui permet de travailler avec différents types de données sans modifier leur description. Sur le site d'Oracle, un tutoriel séparé est dédié aux génériques : « Leçon : Génériques ».

Tout d’abord, pour comprendre les génériques, vous devez comprendre pourquoi ils sont nécessaires et ce qu’ils apportent. Dans le tutoriel de la section " Pourquoi utiliser des génériques ? " On dit que l'un des objectifs est de renforcer la vérification du type au moment de la compilation et d'éliminer le besoin de conversion explicite.
La théorie des génériques en Java ou comment mettre les parenthèses en pratique - 2
Préparons notre compilateur java en ligne tutorielspoint préféré pour les expériences . Imaginons ce code :
import java.util.*;
public class HelloWorld{
	public static void main(String []args){
		List list = new ArrayList();
		list.add("Hello");
		String text = list.get(0) + ", world!";
		System.out.print(text);
	}
}
Ce code fonctionnera correctement. Mais que se passerait-il s’ils venaient nous voir et nous disaient que la phrase « Bonjour tout le monde ! » battu et tu ne peux que revenir Bonjour ? Supprimons la concaténation avec la chaîne du Code ", world!". Il semblerait que quoi de plus inoffensif ? Mais en fait, nous recevrons une erreur DURANT LA COMPILATION : error: incompatible types: Object cannot be converted to String Le fait est que dans notre cas List stocke une liste d'objets de type Object. Puisque String est un descendant d'Object (puisque toutes les classes sont implicitement héritées d'Object en Java), cela nécessite une conversion explicite, ce que nous n'avons pas fait. Et lors de la concaténation, la méthode statique String.valueOf(obj) sera appelée sur l'objet, qui appellera finalement la méthode toString sur l'objet. Autrement dit, notre liste contient un objet. Il s'avère que lorsque nous avons besoin d'un type spécifique, et non d'un objet, nous devrons effectuer nous-mêmes le transtypage :
import java.util.*;
public class HelloWorld{
	public static void main(String []args){
		List list = new ArrayList();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println((String)str);
		}
	}
}
Cependant, dans ce cas, parce que List accepte une liste d'objets, elle stocke non seulement String, mais aussi Integer. Mais le pire, c'est que dans ce cas, le compilateur ne verra rien d'anormal. Et ici, nous recevrons une erreur PENDANT L'EXÉCUTION (ils disent aussi que l'erreur a été reçue « au moment de l'exécution »). L'erreur sera : java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String D'accord, pas la plus agréable. Et tout cela est dû au fait que le compilateur n’est pas une intelligence artificielle et qu’il ne peut pas deviner tout ce que veut dire le programmeur. Pour en savoir plus au compilateur sur les types que nous allons utiliser, Java SE 5 a introduit les génériques . Corrigeons notre version en indiquant au compilateur ce que nous voulons :
import java.util.*;
public class HelloWorld {
	public static void main(String []args){
		List<String> list = new ArrayList<>();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println(str);
		}
	}
}
Comme nous pouvons le voir, nous n’avons plus besoin du cast en String. De plus, nous avons désormais des crochets angulaires qui encadrent les génériques. Désormais, le compilateur ne permettra pas à la classe d'être compilée tant que nous n'aurons pas supprimé l'ajout de 123 à la liste, car c'est un entier. Il nous le dira. Beaucoup de gens appellent les génériques « sucre syntaxique ». Et ils ont raison, puisque les génériques deviendront effectivement ces mêmes castes une fois compilés. Regardons le bytecode des classes compilées : avec casting manuel et utilisation de génériques :
La théorie des génériques en Java ou comment mettre les parenthèses en pratique - 3
Après compilation, toute information sur les génériques est effacée. C'est ce qu'on appelle "Type Erasure" ou " Type Erasure ". L'effacement de type et les génériques sont conçus pour assurer une compatibilité descendante avec les anciennes versions du JDK, tout en permettant au compilateur d'aider à l'inférence de type dans les versions plus récentes de Java.
La théorie des génériques en Java ou comment mettre les parenthèses en pratique - 4

Types bruts ou types bruts

Quand on parle de génériques, on a toujours deux catégories : les types typés (Generic Types) et les types « bruts » (Raw Types). Les types bruts sont des types sans précision de « qualification » entre crochets :
La théorie des génériques en Java ou comment mettre les parenthèses en pratique - 5
Les types typés sont à l'opposé, avec l'indication de « clarification » :
La théorie des génériques en Java ou comment mettre les parenthèses en pratique - 6
Comme nous pouvons le constater, nous avons utilisé un design inhabituel, marqué d'une flèche sur la capture d'écran. Il s'agit d'une syntaxe spéciale qui a été ajoutée dans Java SE 7, et elle s'appelle " le diamant ", ce qui signifie diamant. Pourquoi? Vous pouvez faire une analogie entre la forme d'un losange et la forme des accolades : <> La syntaxe du diamant est également associée au concept de « Type Inference », ou inférence de type. Après tout, le compilateur, voyant <> à droite, regarde le côté gauche, où se trouve la déclaration du type de variable à laquelle la valeur est affectée. Et à partir de cette partie, il comprend de quel type est saisie la valeur de droite. En fait, si un générique est spécifié à gauche et non spécifié à droite, le compilateur pourra en déduire le type :
import java.util.*;
public class HelloWorld{
	public static void main(String []args) {
		List<String> list = new ArrayList();
		list.add("Hello World");
		String data = list.get(0);
		System.out.println(data);
	}
}
Cependant, ce serait un mélange du nouveau style avec les génériques et de l’ancien style sans eux. Et c'est extrêmement indésirable. Lors de la compilation du code ci-dessus, nous recevrons le message : Note: HelloWorld.java uses unchecked or unsafe operations. En fait, la raison pour laquelle nous devons ajouter le diamant ici ne semble pas claire. Mais voici un exemple :
import java.util.*;
public class HelloWorld{
	public static void main(String []args) {
		List<String> list = Arrays.asList("Hello", "World");
		List<Integer> data = new ArrayList(list);
		Integer intNumber = data.get(0);
		System.out.println(data);
	}
}
Comme on s'en souvient, ArrayList possède également un deuxième constructeur qui prend une collection en entrée. Et c’est là que réside la tromperie. Sans la syntaxe Diamond, le compilateur ne comprend pas qu'il est trompé, mais avec Diamond, il le fait. Par conséquent, règle n°1 : utilisez toujours la syntaxe diamant si nous utilisons des types typés. Sinon, nous risquons de manquer les endroits où nous utilisons le type brut. Pour éviter les avertissements dans le journal qui « utilisent des opérations non contrôlées ou non sécurisées », vous pouvez spécifier une annotation spéciale sur la méthode ou la classe utilisée : @SuppressWarnings("unchecked") Suppress est traduit par supprimer, c'est-à-dire littéralement supprimer les avertissements. Mais réfléchissez à la raison pour laquelle vous avez décidé de l’indiquer ? N'oubliez pas la règle numéro un et vous devrez peut-être ajouter de la saisie.
La théorie des génériques en Java ou comment mettre les parenthèses en pratique - 7

Méthodes génériques

Les génériques vous permettent de saisir des méthodes. Il existe une section distincte dédiée à cette fonctionnalité dans le didacticiel Oracle : « Méthodes génériques ». A partir de ce tutoriel, il est important de retenir la syntaxe :
  • comprend une liste de paramètres saisis entre crochets angulaires ;
  • la liste des paramètres tapés précède la méthode renvoyée.
Regardons un exemple :
import java.util.*;
public class HelloWorld{

    public static class Util {
        public static <T> T getValue(Object obj, Class<T> clazz) {
            return (T) obj;
        }
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList("Author", "Book");
		for (Object element : list) {
		    String data = Util.getValue(element, String.class);
		    System.out.println(data);
		    System.out.println(Util.<String>getValue(element));
		}
    }
}
Si vous regardez la classe Util, nous y voyons deux méthodes typées. Avec l'inférence de type, nous pouvons fournir la définition du type directement au compilateur, ou nous pouvons la spécifier nous-mêmes. Les deux options sont présentées dans l'exemple. D’ailleurs, la syntaxe est assez logique si on y réfléchit. Lors de la saisie d'une méthode, nous spécifions le générique AVANT la méthode car si nous utilisons le générique après la méthode, Java ne pourra pas déterminer quel type utiliser. Par conséquent, nous annonçons d’abord que nous utiliserons le générique T, puis nous disons que nous allons rendre ce générique. Naturellement, Util.<Integer>getValue(element, String.class)cela échouera avec une erreur incompatible types: Class<String> cannot be converted to Class<Integer>. Lorsque vous utilisez des méthodes typées, vous devez toujours vous rappeler de l’effacement de type. Regardons un exemple :
import java.util.*;
public class HelloWorld {

    public static class Util {
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList(2, 3);
		for (Object element : list) {
		    System.out.println(Util.<Integer>getValue(element) + 1);
		}
    }
}
Cela fonctionnera très bien. Mais seulement tant que le compilateur comprend que la méthode appelée a un type Integer. Remplaçons la sortie de la console par la ligne suivante : System.out.println(Util.getValue(element) + 1); Et nous obtenons l'erreur : mauvais types d'opérandes pour l'opérateur binaire '+', premier type : Object , deuxième type : int Autrement dit, les types ont été effacés. Le compilateur voit que personne n'a spécifié le type, le type est spécifié comme Objet et l'exécution du code échoue avec une erreur.
La théorie des génériques en Java ou comment mettre les parenthèses en pratique - 8

Types génériques

Vous pouvez saisir non seulement des méthodes, mais également des classes elles-mêmes. Oracle a une section « Types génériques » dédiée à cela dans son guide. Regardons un exemple :
public static class SomeType<T> {
	public <E> void test(Collection<E> collection) {
		for (E element : collection) {
			System.out.println(element);
		}
	}
	public void test(List<Integer> collection) {
		for (Integer element : collection) {
			System.out.println(element);
		}
	}
}
Tout est simple ici. Si nous utilisons une classe, le générique est répertorié après le nom de la classe. Créons maintenant une instance de cette classe dans la méthode main :
public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
Cela fonctionnera bien. Le compilateur voit qu'il existe une liste de nombres et une collection de type String. Mais que se passe-t-il si nous effaçons les génériques et faisons ceci :
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Nous obtiendrons l’erreur : java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer Tapez à nouveau l’effacement. Puisque la classe n’a plus de générique, le compilateur décide que puisque nous avons passé une List, une méthode avec List<Integer> est plus appropriée. Et nous tombons avec une erreur. Par conséquent, règle n°2 : si une classe est typée, spécifiez toujours le type dans le générique .

Restrictions

Nous pouvons appliquer une restriction aux types spécifiés dans les génériques. Par exemple, nous voulons que le conteneur n'accepte que Number comme entrée. Cette fonctionnalité est décrite dans le didacticiel Oracle, dans la section Paramètres de type limité . Regardons un exemple :
import java.util.*;
public class HelloWorld{

    public static class NumberContainer<T extends Number> {
        private T number;

        public NumberContainer(T number)  { this.number = number; }

        public void print() {
            System.out.println(number);
        }
    }

    public static void main(String []args) {
		NumberContainer number1 = new NumberContainer(2L);
		NumberContainer number2 = new NumberContainer(1);
		NumberContainer number3 = new NumberContainer("f");
    }
}
Comme vous pouvez le voir, nous avons limité le type générique à la classe/interface Number et à ses descendants. Fait intéressant, vous pouvez spécifier non seulement une classe, mais également des interfaces. Par exemple : public static class NumberContainer<T extends Number & Comparable> { les génériques ont également le concept de Wildcard https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html. Ils sont à leur tour divisés en trois types : Le principe dit Get Put s'applique aux Wildcards . Ils peuvent être exprimés sous la forme suivante :
La théorie des génériques en Java ou comment mettre les parenthèses en pratique - 9
Ce principe est également appelé principe PECS (Producer Extends Consumer Super). Vous pouvez en savoir plus sur Habré dans l'article « Utiliser des caractères génériques pour améliorer la convivialité de l'API Java », ainsi que dans l'excellente discussion sur stackoverflow : « Utiliser des caractères génériques dans Generics Java ». Voici un petit exemple de la source Java - la méthode Collections.copy :
La théorie des génériques en Java ou comment mettre les parenthèses en pratique - 10
Eh bien, un petit exemple de la façon dont cela ne fonctionnera PAS :
public static class TestClass {
	public static void print(List<? extends String> list) {
		list.add("Hello World!");
		System.out.println(list.get(0));
	}
}

public static void main(String []args) {
	List<String> list = new ArrayList<>();
	TestClass.print(list);
}
Mais si vous remplacez extends par super, tout ira bien. Puisque nous remplissons la liste avec une valeur avant de la sortir, c'est un consommateur pour nous, c'est-à-dire un consommateur. Par conséquent, nous utilisons super.

Héritage

Il existe une autre caractéristique inhabituelle des génériques : leur héritage. L'héritage des génériques est décrit dans le tutoriel Oracle dans la section " Génériques, héritage et sous-types ". L'essentiel est de se rappeler et de réaliser ce qui suit. Nous ne pouvons pas faire ceci :
List<CharSequence> list1 = new ArrayList<String>();
Parce que l'héritage fonctionne différemment avec les génériques :
La théorie des génériques en Java ou comment mettre les parenthèses en pratique - 11
Et voici un autre bon exemple qui échouera avec une erreur :
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Ici aussi, tout est simple. List<String> n'est pas un descendant de List<Object>, bien que String soit un descendant de Object.

Final

Nous avons donc rafraîchi notre mémoire des génériques. S’ils sont rarement utilisés dans toute leur puissance, certains détails échappent à la mémoire. J'espère que cette courte revue vous aidera à vous rafraîchir la mémoire. Et pour de meilleurs résultats, je vous recommande fortement de lire les documents suivants : #Viacheslav
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION