Exemple de tutoriel sur les génériques Java – Méthode générique, classe, interface

Les génériques Java sont l’une des fonctionnalités les plus importantes introduites dans Java 5. Si vous avez travaillé sur les collections Java et avec la version 5 ou supérieure, je suis sûr que vous l’avez utilisée. Les génériques en Java avec les classes de collection sont très faciles, mais ils offrent beaucoup plus de fonctionnalités que simplement créer le type de collection. Nous essaierons de comprendre les fonctionnalités des génériques dans cet article. Comprendre les génériques peut parfois devenir confus si nous utilisons un jargon technique, donc je vais essayer de le rendre simple et facile à comprendre.

Nous examinerons les sujets suivants des génériques en Java.

  1. Génériques en Java

  2. Classe générique Java

  3. Interface générique Java

  4. Type générique Java

  5. Méthode générique Java

  6. Paramètres de type borné des génériques Java

  7. Les génériques Java et l’héritage

  8. Classes génériques Java et sous-typage

  9. Les jokers génériques Java

  10. Le joker générique borné supérieur en Java

  11. Le joker générique non borné en Java

  12. Le joker générique borné inférieur en Java

  13. Sous-typage à l’aide de jokers génériques

  14. L’effacement des types génériques en Java

  15. FAQ sur les génériques

1. Génériques en Java

Les génériques ont été ajoutés dans Java 5 pour fournir une vérification des types au moment de la compilation et éliminer le risque de ClassCastException qui était courant lors de l’utilisation des classes de collection. L’ensemble du framework de collection a été réécrit pour utiliser des génériques pour la sécurité des types. Voyons comment les génériques nous aident à utiliser les classes de collection en toute sécurité.

List list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //OK

for(Object obj : list){
	// La conversion de type conduit à une ClassCastException à l'exécution
    String str=(String) obj; 
}

Le code ci-dessus se compile bien mais génère une ClassCastException à l’exécution car nous essayons de convertir un objet de la liste en String alors qu’un des éléments est de type Integer. Après Java 5, nous utilisons des classes de collection comme ci-dessous.

List list1 = new ArrayList(); // java 7 ? List list1 = new ArrayList<>(); 
list1.add("abc");
//list1.add(new Integer(5)); //erreur de compilation

for(String str : list1){
     //aucune conversion de type nécessaire, évite les ClassCastException
}

Remarquez qu’au moment de la création de la liste, nous avons spécifié que le type des éléments dans la liste sera String. Donc, si nous essayons d’ajouter un autre type d’objet dans la liste, le programme générera une erreur de compilation. Remarquez également que dans la boucle for, nous n’avons pas besoin de convertir le type de l’élément dans la liste, supprimant ainsi la ClassCastException à l’exécution.

2. Classe générique Java

Nous pouvons définir nos propres classes avec des types génériques. Un type générique est une classe ou une interface qui est paramétrée sur des types. Nous utilisons des crochets angulaires (<>) pour spécifier le paramètre de type. Pour comprendre l’avantage, disons que nous avons une classe simple comme suit :

package com.journaldev.generics;

public class GenericsTypeOld {

	private Object t;

	public Object get() {
		return t;
	}

	public void set(Object t) {
		this.t = t;
	}

        public static void main(String args[]){
		GenericsTypeOld type = new GenericsTypeOld();
		type.set("Pankaj"); 
		String str = (String) type.get(); //type casting, error prone and can cause ClassCastException
	}
}

Remarquez que lors de l’utilisation de cette classe, nous devons effectuer une conversion de type et cela peut produire une ClassCastException à l’exécution. Maintenant, nous allons utiliser une classe générique Java pour réécrire la même classe comme indiqué ci-dessous.

package com.journaldev.generics;

public class GenericsType<T> {

	private T t;
	
	public T get(){
		return this.t;
	}
	
	public void set(T t1){
		this.t=t1;
	}
	
	public static void main(String args[]){
		GenericsType<String> type = new GenericsType<>();
		type.set("Pankaj"); //valid
		
		GenericsType type1 = new GenericsType(); //raw type
		type1.set("Pankaj"); //valid
		type1.set(10); //valid and autoboxing support
	}
}

Remarquez l’utilisation de la classe GenericsType dans la méthode principale. Nous n’avons pas besoin de faire de conversion de type et nous pouvons supprimer ClassCastException à l’exécution. Si nous ne fournissons pas le type au moment de la création, le compilateur produira un avertissement indiquant que « GenericsType est un type brut. Les références au type générique GenericsType<T> doivent être paramétrées ». Lorsque nous ne fournissons pas le type, le type devient Object et donc il permet à la fois les objets String et Integer. Cependant, nous devrions toujours essayer d’éviter cela car nous devrons effectuer une conversion de type lors du travail sur un type brut qui peut produire des erreurs à l’exécution.

Conseil : Nous pouvons utiliser l’annotation @SuppressWarnings("rawtypes") pour supprimer l’avertissement du compilateur, consultez le tutoriel sur les annotations Java.

Remarquez également qu’il prend en charge l’autoboxing Java.

3. Interface Générique Java

L’interface Comparable est un excellent exemple de généricité dans les interfaces et est écrite comme suit :

package java.lang;
import java.util.*;

public interface Comparable<T> {
    public int compareTo(T o);
}

De la même manière, nous pouvons créer des interfaces génériques en Java. Nous pouvons également avoir plusieurs paramètres de type comme dans l’interface Map. Encore une fois, nous pouvons fournir une valeur paramétrée à un type paramétré également, par exemple new HashMap<String, List<String>>(); est valide.

4. Type Générique Java

La convention de nommage des types génériques en Java nous aide à comprendre facilement le code et avoir une convention de nommage est l’une des meilleures pratiques du langage de programmation Java. Ainsi, les génériques sont également livrés avec leurs propres conventions de nommage. En général, les noms de paramètres de type sont des lettres majuscules simples pour les distinguer facilement des variables Java. Les noms de paramètres de type les plus couramment utilisés sont :

  • E – Element (used extensively by the Java Collections Framework, for example ArrayList, Set etc.)
  • K – Key (Used in Map)
  • N – Number
  • T – Type
  • V – Value (Used in Map)
  • S,U,V etc. – 2nd, 3rd, 4th types

5. Méthode Générique Java

Parfois, nous ne voulons pas que toute la classe soit paramétrée. Dans ce cas, nous pouvons créer une méthode générique en Java. Étant donné que le constructeur est une sorte spéciale de méthode, nous pouvons également utiliser des types génériques dans les constructeurs. Voici une classe montrant un exemple de méthode générique en Java.

package com.journaldev.generics;

public class GenericsMethods {

	//Méthode générique en Java
	public static  boolean isEqual(GenericsType g1, GenericsType g2){
		return g1.get().equals(g2.get());
	}
	
	public static void main(String args[]){
		GenericsType g1 = new GenericsType<>();
		g1.set("Pankaj");
		
		GenericsType g2 = new GenericsType<>();
		g2.set("Pankaj");
		
		boolean isEqual = GenericsMethods.isEqual(g1, g2);
		//L'instruction ci-dessus peut être simplement écrite comme
		isEqual = GenericsMethods.isEqual(g1, g2);
		//Cette fonctionnalité, connue sous le nom d'inférence de type, vous permet d'appeler une méthode générique comme une méthode ordinaire, sans spécifier de type entre les chevrons.
		//Le compilateur inférera le type nécessaire
	}
}

Remarquez la signature de la méthode isEqual montrant la syntaxe pour utiliser des types génériques dans les méthodes. Remarquez également comment utiliser ces méthodes dans notre programme Java. Nous pouvons spécifier le type lors de l’appel de ces méthodes ou nous pouvons les invoquer comme une méthode normale. Le compilateur Java est suffisamment intelligent pour déterminer le type de variable à utiliser, cette facilité est appelée inférence de type.

6. Paramètres de type bornés en Java Generics

Supposons que nous voulions restreindre le type d’objets pouvant être utilisés dans le type paramétré, par exemple dans une méthode qui compare deux objets et nous voulons nous assurer que les objets acceptés sont Comparables. Pour déclarer un paramètre de type borné, listez le nom du paramètre de type, suivi du mot-clé extends, suivi de sa borne supérieure, de manière similaire à la méthode ci-dessous.

public static <T extends Comparable<T>> int compare(T t1, T t2){
		return t1.compareTo(t2);
	}

L’invocation de ces méthodes est similaire à la méthode sans limite, à l’exception que si nous essayons d’utiliser une classe qui n’est pas Comparable, une erreur de compilation sera levée. Les paramètres de type bornés peuvent être utilisés avec des méthodes ainsi qu’avec des classes et des interfaces. Les génériques Java prennent également en charge plusieurs bornes, c’est-à-dire <T extends A & B & C>. Dans ce cas, A peut être une interface ou une classe. Si A est une classe, alors B et C doivent être des interfaces. Nous ne pouvons pas avoir plus d’une classe dans plusieurs bornes.

7. Génériques Java et Héritage

Nous savons que l’héritage Java nous permet d’assigner une variable A à une autre variable B si A est une sous-classe de B. Nous pourrions donc penser que n’importe quel type générique de A peut être assigné à un type générique de B, mais ce n’est pas le cas. Voyons cela avec un programme simple.

package com.journaldev.generics;

public class GenericsInheritance {

	public static void main(String[] args) {
		String str = "abc";
		Object obj = new Object();
		obj=str; // works because String is-a Object, inheritance in java
		
		MyClass myClass1 = new MyClass();
		MyClass myClass2 = new MyClass();
		//myClass2=myClass1; // erreur de compilation car MyClass n'est pas un MyClass
		obj = myClass1; // MyClass parent is Object
	}
	
	public static class MyClass{}

}

Nous ne sommes pas autorisés à attribuer une variable MyClass à une variable MyClasscar elles ne sont pas liées, en fait le parent de MyClass est Object.

8. Classes génériques Java et sous-typage

Nous pouvons sous-typer une classe générique ou une interface en l’étendant ou en l’implémentant. La relation entre les paramètres de type d’une classe ou interface et les paramètres de type d’une autre est déterminée par les clauses extends et implements. Par exemple, ArrayList implémente List qui étend Collection, donc ArrayList est un sous-type de List et List est un sous-type de Collection. La relation de sous-typage est préservée tant que nous ne changeons pas l’argument de type, ci-dessous un exemple de plusieurs paramètres de type.

interface MyList<E,T> extends List<E>{
}

Les sous-types de List peuvent être MyList, MyList et ainsi de suite.

9. Jokers génériques Java

Le point d’interrogation (?) est le joker dans les génériques et représente un type inconnu. Le joker peut être utilisé comme type d’un paramètre, d’un champ ou d’une variable locale et parfois comme type de retour. Nous ne pouvons pas utiliser de jokers lors de l’invocation d’une méthode générique ou de l’instanciation d’une classe générique. Dans les sections suivantes, nous allons apprendre les jokers à bornes supérieures, les jokers à bornes inférieures et la capture des jokers.

9.1) Joker à borne supérieure en Java

Les jokers à borne supérieure sont utilisés pour assouplir la restriction sur le type de variable dans une méthode. Supposons que nous voulions écrire une méthode qui retournera la somme des nombres dans la liste, donc notre implémentation sera quelque chose comme ceci.

public static double sum(List<Number> list){
		double sum = 0;
		for(Number n : list){
			sum += n.doubleValue();
		}
		return sum;
	}

Le problème avec l’implémentation ci-dessus est qu’elle ne fonctionnera pas avec une liste d’entiers ou de doubles car nous savons que List et List ne sont pas liés, c’est là qu’un joker à borne supérieure est utile. Nous utilisons un joker générique avec le mot-clé extends et la classe ou l’interface de la borne supérieure qui nous permettra de passer un argument de type de borne supérieure ou de ses sous-classes. L’implémentation ci-dessus peut être modifiée comme dans le programme ci-dessous.

package com.journaldev.generics;

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

public class GenericsWildcards {

	public static void main(String[] args) {
		List<Integer> ints = new ArrayList<>();
		ints.add(3); ints.add(5); ints.add(10);
		double sum = sum(ints);
		System.out.println("Sum of ints="+sum);
	}

	public static double sum(List<? extends Number> list){
		double sum = 0;
		for(Number n : list){
			sum += n.doubleValue();
		}
		return sum;
	}
}

C’est similaire à écrire notre code en termes d’interface, dans la méthode ci-dessus, nous pouvons utiliser toutes les méthodes de la classe de borne supérieure Number. Notez qu’avec une liste bornée supérieure, nous ne sommes pas autorisés à ajouter d’objet à la liste sauf null. Si nous essayons d’ajouter un élément à la liste à l’intérieur de la méthode sum, le programme ne se compilera pas.

9.2) Génériques Java Joker non borné

Parfois, nous avons une situation où nous voulons que notre méthode générique fonctionne avec tous les types, dans ce cas, un joker non borné peut être utilisé. C’est la même chose que d’utiliser <? extends Object>.

public static void printData(List<?> list){
		for(Object obj : list){
			System.out.print(obj + "::");
		}
	}

Nous pouvons fournir List<String> ou List<Integer> ou tout autre type d’argument de liste d’objet à la méthode printData. Tout comme pour la liste bornée supérieure, nous ne sommes pas autorisés à ajouter quoi que ce soit à la liste.

9.3) Génériques Java Joker borné inférieurement

Supposons que nous voulions ajouter des entiers à une liste d’entiers dans une méthode, nous pouvons conserver le type d’argument en tant que List<Integer>, mais il sera lié aux Integers alors que List<Number> et List<Object> peuvent également contenir des entiers, donc nous pouvons utiliser un joker de borne inférieure pour y parvenir. Nous utilisons le joker de génériques (?) avec le mot-clé super et la classe de borne inférieure pour y parvenir. Nous pouvons passer une borne inférieure ou n’importe quel supertype de la borne inférieure en tant qu’argument, dans ce cas, le compilateur Java autorise l’ajout de types d’objets de borne inférieure à la liste.

public static void addIntegers(List<? super Integer> list){
		list.add(new Integer(50));
	}

10. Sous-typage à l’aide du Joker de Génériques

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>

11. Effacement de Type des Génériques Java

Les génériques en Java ont été ajoutés pour fournir une vérification de type lors de la compilation et ils ne sont pas utilisés lors de l’exécution, donc le compilateur Java utilise la fonctionnalité d’effacement de type pour supprimer tout le code de vérification de type générique dans le code octet et insérer des conversions de type si nécessaire. L’effacement de type garantit qu’aucune nouvelle classe n’est créée pour les types paramétrés ; par conséquent, les génériques n’entraînent aucun surcoût d’exécution. Par exemple, si nous avons une classe générique comme ci-dessous ;

public class Test<T extends Comparable<T>> {

    private T data;
    private Test<T> next;

    public Test(T d, Test<T> n) {
        this.data = d;
        this.next = n;
    }

    public T getData() { return this.data; }
}

Le compilateur Java remplace le paramètre de type borné T par la première interface de limite, Comparable, comme dans le code ci-dessous :

public class Test {

    private Comparable data;
    private Test next;

    public Node(Comparable d, Test n) {
        this.data = d;
        this.next = n;
    }

    public Comparable getData() { return data; }
}

12. Questions fréquemment posées sur les génériques

12.1) Pourquoi utilisons-nous des génériques en Java?

Les génériques fournissent une vérification de type forte lors de la compilation et réduisent le risque de ClassCastException et de conversion explicite d’objets.

12.2) Que représente T dans les génériques?

Nous utilisons pour créer une classe générique, une interface et une méthode. Le T est remplacé par le type réel lorsque nous l’utilisons.

12.3) Comment fonctionnent les génériques en Java?

Le code générique assure la sécurité des types. Le compilateur utilise l’effacement des types pour supprimer tous les paramètres de type lors de la compilation afin de réduire la surcharge à l’exécution.

13. Génériques en Java – Lectures complémentaires

C’est tout pour les génériques en Java, les génériques Java sont un sujet vraiment vaste et nécessitent beaucoup de temps pour les comprendre et les utiliser efficacement. Cet article est une tentative de fournir des détails de base sur les génériques et comment nous pouvons les utiliser pour étendre notre programme en toute sécurité de type.

Source:
https://www.digitalocean.com/community/tutorials/java-generics-example-method-class-interface