W3docs

Classes génériques Java

Apprenez à écrire des classes génériques Java avec des paramètres de type, l'opérateur diamant et un exemple de pile typée.

Une classe générique est une classe dont la déclaration porte un ou plusieurs paramètres de type — des espaces réservés que l'appelant renseigne lors de la création d'une instance. Le même corps de classe décrit alors toute une famille de types : Box<String>, Box<Integer>, Box<User> sont des types distincts à la compilation qui partagent une seule source. C'est la forme la plus courante que prennent les génériques, et c'est ainsi que sont écrites toutes les collections de java.util, chaque Optional, chaque Future et chaque CompletableFuture.

La syntaxe

La liste des paramètres de type se place entre le nom de la classe et le corps, entre chevrons :

public class Box<T> {
  private T value;

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

  public T get()              { return value; }
  public void set(T value)    { this.value = value; }
}

Lisez la déclaration comme « un Box paramétré sur un certain type T ». À l'intérieur de la classe, T se comporte comme n'importe quel autre type — vous pouvez déclarer des champs de type T, des méthodes renvoyant T, des paramètres de type T. Le compilateur le traite comme un type réel et inconnu jusqu'à ce que l'appelant en choisisse un.

Au site d'appel, vous fournissez le type réel :

Box<String>  greeting = new Box<>("hello");
Box<Integer> answer   = new Box<>(42);

String s = greeting.get();   // already a String — no cast
int i    = answer.get();     // auto-unboxed from Integer

Le <> à droite est l'opérateur diamant — le compilateur déduit l'argument de type à partir de la déclaration du côté gauche. Vous pouvez écrire new Box<String>("hello") explicitement, mais vous n'en avez presque jamais besoin.

Plusieurs paramètres de type

Une classe peut déclarer plus d'un paramètre de type. L'exemple classique est une paire clé/valeur :

public class Entry<K, V> {
  private final K key;
  private final V value;

  public Entry(K key, V value) {
    this.key   = key;
    this.value = value;
  }

  public K key()   { return key; }
  public V value() { return value; }
}

Entry<String, Integer> score = new Entry<>("Ada", 100);
String name = score.key();
int    n    = score.value();

La convention est d'utiliser des noms à une seule lettre — K pour clé, V pour valeur, E pour élément, R pour retour, T pour « type générique ». Lorsque vous avez besoin de plus de clarté (rare), des noms plus longs sont permis : Map<KeyType, ValueType> est légal, simplement peu usité.

Borner le paramètre de type

Par défaut, un paramètre de type représente « n'importe quel type », de sorte qu'à l'intérieur de la classe vous ne pouvez appeler que les méthodes que tout objet possède (equals, toString, hashCode). Si votre classe a besoin d'agir sur les valeurs — les comparer, les additionner, lire une propriété — vous contraignez T avec une borne supérieure grâce à extends :

// T can be any type that is (or extends) Number, so .doubleValue() is callable.
public class NumberBox<T extends Number> {
  private final T value;

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

  public double asDouble() { return value.doubleValue(); }
}

NumberBox<Integer> n = new NumberBox<>(42);   // fine — Integer is a Number
// NumberBox<String> bad = ...;               // ❌ String is not a Number

extends signifie ici « est un sous-type de », et cela fonctionne aussi bien pour les classes que pour les interfaces. Vous pouvez même exiger plusieurs bornes à la fois — <T extends Number & Comparable<T>> — en plaçant la borne de classe (le cas échéant) en premier. La borne rend aussi le type utilisable : sans extends Number, value.doubleValue() ne compilerait pas.

Constructeurs génériques

Le paramètre de type est fixé par l'instance, donc chaque constructeur d'une classe générique a déjà accès à T :

public class Pair<T> {
  private final T first;
  private final T second;

  public Pair(T first, T second) { this.first = first; this.second = second; }
  public Pair(T both)            { this(both, both); }
}

Les constructeurs eux-mêmes peuvent aussi être génériques sur des paramètres de type supplémentaires indépendants de ceux de la classe — mais c'est assez rare pour être abordé dans le prochain chapitre sur les méthodes génériques.

Les classes génériques peuvent s'étendre mutuellement

Une sous-classe peut hériter d'une classe générique de trois manières. Chacune a une signification différente :

// 1. Lock the parent's type parameter — concrete subclass for one element type.
public class StringList extends ArrayList<String> { ... }

// 2. Pass the type parameter through — the subclass is still generic.
public class MyList<E> extends ArrayList<E> { ... }

// 3. Add new type parameters of your own.
public class TaggedList<E, Tag> extends ArrayList<E> { ... }

La forme intermédiaire est la plus courante — vous propagez le paramètre du parent à vos propres appelants. La première forme est celle utilisée lorsque la sous-classe est spécialisée : un arbre de nœuds de chaînes uniquement.

Les champs et le paramètre de type

Chaque instance de Box<...> porte son propre T. Le bytecode, lui, ne le fait pas — à l'exécution, la JVM voit juste Box (c'est l'effacement de type, abordé plus loin dans cette partie). Il en résulte que le paramètre de type appartient à l'instance, et non à l'objet classe :

Box<String>  a = new Box<>("hi");
Box<Integer> b = new Box<>(5);

a.getClass() == b.getClass();   // true — both are class Box

C'est un fait utile à garder à l'esprit : Box<String> et Box<Integer> sont des types différents pour le compilateur mais la même classe à l'exécution. Nous y reviendrons dans Java Type Erasure.

Les membres statiques ne voient pas le paramètre de type

Les champs statiques et les méthodes statiques appartiennent à la classe, non à une instance particulière — ils ne peuvent donc pas voir le T de l'instance. Ceci est illégal :

public class Box<T> {
  private static T defaultValue;        // ❌ won't compile — no T at the static level
  public  static T empty() { ... }      // ❌ same problem
}

Une méthode statique qui a besoin d'un paramètre de type doit déclarer le sien propre, indépendamment de celui de la classe. C'est l'objet du prochain chapitre.

Concevoir la vôtre : une petite pile typée

Une classe fonctionnelle et complète pour tout relier — une Stack générique avec push, pop, peek et size. Elle est paramétrée sur E (élément), soutenue en interne par un Object[] (en raison des restrictions sur les tableaux génériques), et le cast non vérifié dans pop est le type de contournement bien encapsulé que vous rencontrerez dans du vrai code.

java— editable, runs on the server

Les annotations @SuppressWarnings("unchecked") se trouvent sur les deux lectures qui doivent effectuer un cast de Object vers E. Ces casts sont sûrs — push ne stocke que des valeurs de type E — mais le compilateur ne peut pas le voir, car l'effacement a supprimé E du bytecode. Supprimer l'avertissement localement, sur la portée la plus réduite possible, est la bonne approche.

La suite

Vous avez vu le paramètre au niveau de la classe. Parfois, vous avez besoin qu'une seule méthode soit générique, avec son propre paramètre de type indépendant de celui de la classe — utile pour les méthodes utilitaires, les helpers statiques et toute opération dont la relation de type ne concerne que cette seule méthode. Continuez vers Java Generic Methods.

Pratique

Pratique
Vous écrivez `public class Box<T> { private static T value; }`. Le compilateur le rejette. Pourquoi ?
Vous écrivez `public class Box<T> { private static T value; }`. Le compilateur le rejette. Pourquoi ?
Was this page helpful?