W3docs

Méthodes génériques en Java

Définissez des méthodes avec leurs propres paramètres de type en Java, indépendamment de la classe englobante.

Une méthode générique est une méthode qui introduit son propre paramètre de type dans sa signature, indépendamment de tout paramètre au niveau de la classe. C'est l'outil adapté lorsque la relation de type appartient à une seule méthode — un utilitaire qui échange deux éléments d'un tableau, une fabrique qui retourne une liste de ce que le code appelant lui passe, un assistant statique qui n'a pas d'instance pour y attacher un T. Les méthodes génériques sont la façon dont presque toutes les méthodes statiques de java.util.Collections et java.util.Arrays sont écrites.

Où placer le paramètre de type

Le paramètre de type est déclaré avant le type de retour, entre les modificateurs et le retour :

public static <T> T identity(T value) {
  return value;
}

De gauche à droite : "public, static, déclare un paramètre de type T, retourne un T, nommé identity, prend un T." Le <T> est ce qui fait de cette méthode une méthode générique plutôt qu'une méthode qui utilise par hasard un T au niveau de la classe.

Appelez-la comme une méthode normale — le compilateur infère l'argument de type à partir des arguments que vous passez :

String s = identity("hello");   // T inferred as String
Integer n = identity(42);       // T inferred as Integer

Si l'inférence échoue ou si vous souhaitez la remplacer, vous pouvez fournir l'argument de type explicitement avec la syntaxe du témoin de type, après le point :

String s = MyUtil.<String>identity("hello");   // rarely needed

En dix ans de Java, vous écrirez cette forme explicite peut-être une douzaine de fois.

Pourquoi un paramètre au niveau de la méthode plutôt qu'au niveau de la classe

Un paramètre au niveau de la classe dit « cette classe entière concerne un type ». Un paramètre au niveau de la méthode dit « cette opération unique est polymorphe dans un type qui n'a pas besoin de survivre à l'appel ». Ce ne sont pas des substituts — ils répondent à des questions différentes :

// Method-level: the class isn't generic; the method is.
public class Arrays {
  public static <T> void swap(T[] arr, int i, int j) { ... }
}

// Class-level: the class is parameterised; methods share that T.
public class Box<T> {
  public T get() { ... }
  public void set(T value) { ... }
}

Utilisez un paramètre au niveau de la méthode quand :

  • La méthode est static (elle n'a pas d'instance, donc pas de T au niveau de la classe à emprunter).
  • La relation de type est locale à la méthode — l'entrée et la sortie partagent un type, mais la classe ne le fait pas.
  • Vous souhaitez que différents appels de la même méthode utilisent des types différents : swap sur un String[] et swap sur un Integer[] doivent tous les deux fonctionner, et la classe ne devrait pas avoir à s'engager sur l'un d'eux.

Plusieurs paramètres de type dans une même méthode

La même règle s'applique : déclarez-les entre les modificateurs et le type de retour, séparés par des virgules :

public static <K, V> Map.Entry<K, V> entry(K key, V value) {
  return new AbstractMap.SimpleImmutableEntry<>(key, value);
}

Map.Entry<String, Integer> e = entry("Ada", 100);

K et V sont tous les deux inférés à partir des arguments. Si les deux paramètres partagent un type, le type inféré est celui sur lequel les deux arguments s'accordent :

public static <T> T firstOf(T a, T b) { return a; }

firstOf("x", "y");   // T = String
firstOf("x", 42);    // T = Object — the closest common supertype

Ce dernier cas peut parfois être trompeur. Le compilateur ne le rejette pas ; il élargit silencieusement T en Object. Si vous vouliez « deux arguments du même type exact », les génériques ne peuvent pas l'imposer au-delà de l'élargissement — il faudrait faire des arguments des paramètres de type séparés.

Un paramètre au niveau de la méthode sur une classe générique

Une classe générique peut avoir des méthodes génériques qui introduisent leurs propres paramètres, distincts de ceux de la classe. Les deux paramètres coexistent :

public class Box<T> {
  private T value;

  public Box(T value) { this.value = value; }

  public T get() { return value; }

  // U is local to this method — independent of T.
  public <U> Box<U> map(java.util.function.Function<T, U> fn) {
    return new Box<>(fn.apply(value));
  }
}

Box<String> name   = new Box<>("Ada");
Box<Integer> length = name.map(String::length);     // T=String, U=Integer

Le <U> de map est en portée uniquement à l'intérieur de map. Il peut utiliser T (car il est à l'intérieur d'un Box<T>) mais il ne peut pas le remplacer.

L'inférence de type en pratique

Le compilateur infère les paramètres de type d'une méthode à partir de :

  1. Les types des arguments explicites.
  2. Le type cible — ce à quoi vous assignez le résultat, ou le type de paramètre d'une méthode à laquelle vous passez le résultat.

La deuxième source explique pourquoi List.of(), Collections.emptyList(), et les génériques similaires qui ne retournent rien fonctionnent sans témoin de type explicite la plupart du temps :

List<String> empty = Collections.emptyList();           // T inferred from the left side
process(Collections.emptyList());                       // T inferred from `process`'s parameter

Lorsqu'aucune des deux sources n'est disponible (pas d'arguments, pas de type cible), le compilateur se replie sur Object. Ce n'est presque jamais ce que vous voulez — écrivez le témoin de type ou ajoutez un type cible :

var x = Collections.emptyList();   // List<Object> — probably not what you meant
List<String> y = Collections.emptyList();   // List<String> ✓

Une forme concrète : les méthodes utilitaires sur les collections

Les méthodes Collections.unmodifiableList, Collections.sort, Collections.shuffle de la bibliothèque standard et leurs équivalents sont toutes des méthodes génériques sur une classe utilitaire non générique. Prenons sort, dans son esprit :

public static <T extends Comparable<T>> void sort(List<T> list) {
  // ... sorts using natural order
}

Cette signature fait deux choses à la fois. Le <T> déclare un paramètre de type. Le extends Comparable<T> est une borneT doit être un type qui sait se comparer à lui-même. Nous consacrerons un chapitre entier aux paramètres bornés ; pour l'instant, notez simplement que la borne est ce qui permet à la méthode d'appeler compareTo sur ses éléments.

Exemple pratique : un échange typé, un dernier typé, un map typé

Une petite classe utilitaire avec trois méthodes génériques — une void, une retournant le même type que celui reçu, une effectuant un mapping élément par élément vers un nouveau type. Ensemble, elles couvrent les trois formes que vous écrirez le plus souvent.

java— editable, runs on the server

Trois choses à remarquer. swap fonctionne à la fois sur String[] et Integer[] car T est inféré par appel. last retourne le type d'élément que le code appelant a passé — pas de cast du côté récepteur. map introduit deux paramètres de type et les lie ensemble via le paramètre Function<T, R> — le compilateur impose que la fonction prenne le type d'élément de la liste et retourne le type d'élément de la liste de résultat.

Et ensuite

Vous avez vu les deux façons de déclarer un paramètre de type — sur une classe et sur une méthode. L'étape suivante est le troisième endroit où un paramètre de type peut vivre : sur une interface. C'est ainsi que la bibliothèque standard définit List<E>, Comparator<T>, Function<T, R>, et tout autre contrat que vous implémentez lorsque vous écrivez du code polymorphe. Continuez vers Les interfaces génériques Java.

Pratique

Pratique
Vous écrivez un utilitaire statique `public static <T> T firstNonNull(T a, T b) { return a != null ? a : b; }`. Un appelant écrit `firstNonNull('hi', 42)`. Que déduit le compilateur pour `T` ?
Vous écrivez un utilitaire statique `public static <T> T firstNonNull(T a, T b) { return a != null ? a : b; }`. Un appelant écrit `firstNonNull('hi', 42)`. Que déduit le compilateur pour `T` ?
Was this page helpful?