CodeGym /Blog Java /Random-FR /Java Generics : comment utiliser les crochets angulaires ...
John Squirrels
Niveau 41
San Francisco

Java Generics : comment utiliser les crochets angulaires 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.

Que sont les génériques en Java ?

Les génériques sont le mécanisme spécial de Java pour implémenter la programmation générique - un moyen de décrire des données et des algorithmes qui vous permet de travailler avec différents types de données sans modifier la description des algorithmes. Le site Web d'Oracle propose un didacticiel séparé dédié aux génériques : " Lesson ". Pour comprendre les génériques, vous devez d'abord comprendre pourquoi ils sont nécessaires et ce qu'ils donnent. La section « Pourquoi utiliser des génériques ? Génériques en Java : comment utiliser les crochets en pratique - 1Préparons-nous à quelques tests dans notre bien-aimé compilateur Java en ligne Tutorialspoint . Supposons que vous ayez le code suivant :
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 parfaitement bien. Mais que se passe-t-il si le patron vient à nous et dit que "Hello, world!" est une expression galvaudée et que vous devez renvoyer uniquement "Bonjour" ? Nous supprimerons le code qui concatène ", monde !" Cela semble assez inoffensif, non ? Mais nous obtenons en fait une erreur AT COMPILE TIME :

error: incompatible types: Object cannot be converted to String
Le problème est que dans notre liste stocke des objets. String est un descendant de Object (puisque toutes les classes Java héritent implicitement de Object ), ce qui signifie que nous avons besoin d'un cast explicite, mais nous n'en avons pas ajouté. Lors de l'opération de concaténation, la méthode statique String.valueOf(obj) sera appelée à l'aide de l'objet. Finalement, il appellera la méthode toString de la classe Object . En d'autres termes, notre List contient un Object . Cela signifie que partout où nous avons besoin d'un type spécifique (pas Object ), nous devrons faire la conversion de type nous-mêmes :
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, comme List prend des objets, il peut stocker non seulement des String s, mais aussi des Integer s. Mais le pire, c'est que le compilateur ne voit rien de mal ici. Et maintenant, nous aurons une erreur AU MOMENT DE L'EXÉCUTION (appelée "erreur d'exécution"). L'erreur sera :

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Vous devez convenir que ce n'est pas très bon. Et tout cela parce que le compilateur n'est pas une intelligence artificielle capable de toujours deviner correctement l'intention du programmeur. Java SE 5 a introduit des génériques pour nous permettre d'informer le compilateur de nos intentions - des types que nous allons utiliser. Nous corrigeons notre code 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 vous pouvez le voir, nous n'avons plus besoin d'un cast en String . De plus, nous avons des crochets angulaires autour de l'argument de type. Maintenant, le compilateur ne nous laissera pas compiler la classe tant que nous n'aurons pas supprimé la ligne qui ajoute 123 à la liste, puisqu'il s'agit d'un Integer . Et il nous le dira. Beaucoup de gens appellent les génériques "sucre syntaxique". Et ils ont raison, car une fois les génériques compilés, ils deviennent vraiment des conversions de même type. Regardons le bytecode des classes compilées : une qui utilise un cast explicite et une qui utilise des génériques : Génériques en Java : comment utiliser les crochets en pratique - 2Après compilation, tous les génériques sont effacés. C'est ce qu'on appelle " l'effacement de type". L'effacement de type et les génériques sont conçus pour être rétrocompatibles avec les anciennes versions du JDK tout en permettant simultanément au compilateur d'aider avec les définitions de type dans les nouvelles versions de Java.

Types bruts

En parlant de génériques, nous avons toujours deux catégories : les types paramétrés et les types bruts. Les types bruts sont des types qui omettent la « clarification de type » entre crochets : Génériques en Java : comment utiliser les crochets en pratique - 3les types paramétrés, quant à eux, incluent une « clarification » : Génériques en Java : comment utiliser les crochets en pratique - 4comme vous pouvez le voir, nous avons utilisé une construction inhabituelle, marquée par une flèche dans la capture d'écran. C'est une syntaxe spéciale qui a été ajoutée à Java SE 7. C'est ce qu'on appelle le « diamant ». Pourquoi? Les chevrons forment un losange : <> . Il faut aussi savoir que la syntaxe en losange est associée à la notion d'« inférence de type ». Après tout, le compilateur, voyant <>à droite, regarde le côté gauche de l'opérateur d'affectation, où il trouve le type de la variable dont la valeur est affectée. Sur la base de ce qu'il trouve dans cette partie, il comprend le type de la valeur à droite. En fait, si un type générique est donné à gauche, mais pas à droite, le compilateur peut 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);
	}
}
Mais cela mélange le nouveau style avec des génériques et l'ancien style sans eux. Et ceci est hautement indésirable. Lors de la compilation du code ci-dessus, nous obtenons le message suivant :

Note: HelloWorld.java uses unchecked or unsafe operations
En fait, la raison pour laquelle vous devez même ajouter un diamant ici semble incompréhensible. 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);
	}
}
Vous vous souviendrez que ArrayList a un deuxième constructeur qui prend une collection comme argument. Et c'est là que quelque chose de sinistre se cache. Sans la syntaxe diamant, le compilateur ne comprend pas qu'il est trompé. Avec la syntaxe du diamant, c'est le cas. Ainsi, la règle n°1 est : utilisez toujours la syntaxe en diamant avec des types paramétrés. Sinon, nous risquons de manquer où nous utilisons des types bruts. Pour éliminer les avertissements "utilise des opérations non contrôlées ou non sécurisées", nous pouvons utiliser l' annotation @SuppressWarnings ("unchecked") sur une méthode ou une classe. Mais réfléchissez à la raison pour laquelle vous avez décidé de l'utiliser. Rappelez-vous la règle numéro un. Peut-être avez-vous besoin d'ajouter un argument de type.

Méthodes génériques Java

Les génériques vous permettent de créer des méthodes dont les types de paramètres et le type de retour sont paramétrés. Une section distincte est consacrée à cette fonctionnalité dans le didacticiel Oracle : " Méthodes génériques ". Il est important de se souvenir de la syntaxe enseignée dans ce tutoriel :
  • il inclut une liste de paramètres de type entre crochets ;
  • la liste des paramètres de type précède le type de retour de la méthode.
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 , vous verrez qu'elle a deux méthodes génériques. Grâce à la possibilité d'inférence de type, nous pouvons soit indiquer le type directement au compilateur, soit le spécifier nous-mêmes. Les deux options sont présentées dans l'exemple. Soit dit en passant, la syntaxe a beaucoup de sens si vous y réfléchissez. Lors de la déclaration d'une méthode générique, nous spécifions le paramètre de type AVANT la méthode, car si nous déclarons le paramètre de type après la méthode, la JVM ne serait pas en mesure de déterminer quel type utiliser. En conséquence, nous déclarons d'abord que nous allons utiliser le paramètre de type T , puis nous disons que nous allons retourner ce type. Naturellement, Util.<Integer>getValue(element, String.class) échouera avec une erreur :types incompatibles : Class<String> ne peut pas être converti en Class<Integer> . Lorsque vous utilisez des méthodes génériques, vous devez toujours vous souvenir de l'effacement du 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 le type de retour de la méthode appelée est Integer . Remplacez l'instruction de sortie de la console par la ligne suivante :
System.out.println(Util.getValue(element) + 1);
Nous obtenons une erreur :

bad operand types for binary operator '+', first type: Object, second type: int.
En d'autres termes, l'effacement du type s'est produit. Le compilateur voit que personne n'a spécifié le type, donc le type est indiqué comme Object et la méthode échoue avec une erreur.

Classes génériques

Il n'y a pas que les méthodes qui peuvent être paramétrées. Les cours peuvent aussi. La section "Generic Types" du tutoriel d'Oracle y est consacrée. Prenons 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 la classe générique, le paramètre type est indiqué 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);
}
Ce code fonctionnera bien. Le compilateur voit qu'il existe une List of numbers et une Collection of Strings . Mais que se passe-t-il si nous éliminons le paramètre de type et faisons ceci :
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Nous obtenons une erreur :

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Encore une fois, il s'agit d'un effacement de type. Étant donné que la classe n'utilise plus de paramètre de type, le compilateur décide que, puisque nous avons passé un List , la méthode avec List<Integer> est la plus appropriée. Et nous échouons avec une erreur. Par conséquent, nous avons la règle n° 2 : si vous avez une classe générique, spécifiez toujours les paramètres de type.

Restrictions

Nous pouvons restreindre les types spécifiés dans les méthodes génériques et n classes. Par exemple, supposons que nous voulions qu'un conteneur n'accepte qu'un nombre comme argument de type. Cette fonctionnalité est décrite dans la section Bounded Type Parameters du tutoriel d'Oracle. 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 paramètre type à la classe/interface Number ou à ses descendants. Notez que 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 prennent également en charge les caractères génériques. Ils sont divisés en trois types :
  • Caractères génériques à limite supérieure — < ? étend Nombre >
  • Caractères génériques illimités — < ? >
  • Caractères génériques délimités inférieurs — < ? super entier >
Votre utilisation des caractères génériques doit respecter le principe Get-Put . Il peut être exprimé comme suit :
  • Utilisez un caractère générique d'extension lorsque vous n'obtenez que des valeurs d'une structure.
  • Utilisez un super caractère générique lorsque vous mettez uniquement des valeurs dans une structure.
  • Et n'utilisez pas de caractère générique lorsque vous voulez tous les deux obtenir et mettre depuis/vers une structure.
Ce principe est également appelé le principe Producer Extends Consumer Super (PECS). Voici un petit exemple tiré du code source de la méthode Collections.copy de Java : Génériques en Java : comment utiliser les crochets en pratique - 5Et voici un petit exemple de ce qui 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 extend par super , alors tout va bien. Comme nous remplissons la liste avec une valeur avant d'afficher son contenu, il s'agit d'un consommateur . En conséquence, nous utilisons super.

Héritage

Les génériques ont une autre caractéristique intéressante : l'héritage. Le fonctionnement de l'héritage pour les génériques est décrit sous " Génériques, héritage et sous-types " dans le didacticiel d'Oracle. L'important est de se rappeler et de reconnaître 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 : Génériques en Java : comment utiliser les crochets en pratique - 6Et voici un autre bon exemple qui échouera avec une erreur :
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Encore une fois, tout est simple ici. List<String> n'est pas un descendant de List<Object> , même si String est un descendant de Object . Pour renforcer ce que vous avez appris, nous vous suggérons de regarder une leçon vidéo de notre cours JavaGénériques Java - 11

Conclusion

Nous nous sommes donc rafraîchi la mémoire concernant les génériques. Si vous tirez rarement pleinement parti de leurs capacités, certains détails deviennent flous. J'espère que cette courte critique vous a aidé à vous rafraîchir la mémoire. Pour des résultats encore meilleurs, je vous recommande fortement de vous familiariser avec le matériel suivant :
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION