W3docs

Interfaces génériques Java

Concevez des interfaces génériques en Java qui paramètrent leurs signatures de méthode par un type.

Une interface générique est une interface dont la déclaration prend un ou plusieurs paramètres de type, exactement comme une classe générique. C'est le troisième endroit où des paramètres de type peuvent exister en Java, aux côtés des classes génériques et des méthodes génériques, et c'est le plus important — car presque chaque contrat réutilisable dans la bibliothèque standard est une interface générique. List<E>, Map<K, V>, Comparator<T>, Function<T, R>, Supplier<T>, Iterable<T>, Iterator<T> — ils constituent la colonne vertébrale du Java moderne.

La syntaxe

La liste des paramètres de type se place entre le nom de l'interface et le corps :

public interface Container<T> {
  void add(T item);
  T   get(int index);
  int size();
}

Lisez cela comme « un Container paramétré sur un certain type d'élément T. » À l'intérieur de l'interface, T peut apparaître dans les paramètres des méthodes, les types de retour, et toute autre position où un type peut se trouver. Les méthodes par défaut (Java 8+) et les méthodes d'interface privées (Java 9+) peuvent également utiliser T.

Lorsque vous implémentez l'interface, vous devez faire un choix — et ce choix constitue toute la décision architecturale :

// 1. Pick a concrete type — the implementation is specialised.
public class StringContainer implements Container<String> {
  private final List<String> items = new ArrayList<>();
  public void   add(String s)  { items.add(s); }
  public String get(int i)     { return items.get(i); }
  public int    size()         { return items.size(); }
}

// 2. Stay generic — pass the parameter through to the class.
public class ListContainer<E> implements Container<E> {
  private final List<E> items = new ArrayList<>();
  public void add(E e)    { items.add(e); }
  public E    get(int i)  { return items.get(i); }
  public int  size()      { return items.size(); }
}

Les deux sont valides. Le premier est « un Container qui contient des Strings, spécifiquement. » Le second est « un Container paramétré sur le même E que le code appelant choisit. » La plupart des conteneurs réutilisables utilisent la seconde forme ; les spécialisés (un JsonObject est un « conteneur de JsonValues, rien d'autre ») utilisent la première.

Plusieurs paramètres de type

La forme se généralise directement à deux paramètres ou plus. Regardez java.util.Map :

public interface Map<K, V> {
  V    put(K key, V value);
  V    get(Object key);          // Object on purpose — see below
  Set<K> keySet();
  Collection<V> values();
  ...
}

La déclaration Map<K, V> dit « deux paramètres : K pour les clés, V pour les valeurs. » Les implémentations les fixent ou les transmettent :

public class StringIntMap implements Map<String, Integer> { ... }   // pinned
public class HashMap<K, V> implements Map<K, V>           { ... }   // passed through

Le get(Object key) dans la signature de Map est un élément délibéré de conception d'API — il accepte n'importe quel objet comme clé de recherche pour des raisons historiques. Nous y reviendrons dans la partie Collections ; ce n'est pas une règle des génériques, juste un compromis spécifique à Map.

Les interfaces fonctionnelles sont des interfaces génériques

Les interfaces dans java.util.functionFunction, Predicate, Consumer, Supplier, BiFunction, et ainsi de suite — sont toutes des interfaces génériques avec une seule méthode abstraite, ce qui en fait des cibles pour les lambdas :

public interface Function<T, R> {
  R apply(T t);
}

public interface Predicate<T> {
  boolean test(T t);
}

public interface Comparator<T> {
  int compare(T a, T b);
}

Lorsque vous écrivez s -> s.length(), le compilateur infère un Function<String, Integer> à partir du contexte. Les deux paramètres de type de Function<T, R> sont remplis par le code environnant — généralement une opération de stream ou un paramètre de méthode :

List<String> names = List.of("Ada", "Grace", "Linus");
List<Integer> lengths = names.stream()
    .map(s -> s.length())          // Function<String, Integer> — both inferred
    .toList();

C'est une interface générique et une méthode générique (Stream.map) qui coopèrent. La signature de la méthode est approximativement <R> Stream<R> map(Function<? super T, ? extends R> mapper) — des wildcards que nous rencontrerons dans Wildcards, et un paramètre de type qui choisit R en fonction de la fonction que vous avez fournie.

Interfaces auto-référentielles — Comparable<T>

L'un des patterns les plus utiles dans la bibliothèque standard est l'interface générique auto-référentielle, où l'argument de type est la classe qui l'implémente elle-même :

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

public class Money implements Comparable<Money> {
  private final long cents;
  // ...
  @Override public int compareTo(Money other) {
    return Long.compare(this.cents, other.cents);
  }
}

Lisez class Money implements Comparable<Money> comme « Money sait se comparer à d'autres Money. » C'est ce qui fait fonctionner Collections.sort(List<Money> list) sans Comparator — chaque élément porte déjà un compareTo(Money) hérité du contrat de l'interface, et le système de types garantit que l'argument est du même type que le receveur.

Comparable<T> est l'exemple canonique de cette forme — chaque type valeur dans le JDK ayant un ordre naturel l'implémente : Integer implements Comparable<Integer>, String implements Comparable<String>, LocalDate implements Comparable<LocalDate>, et ainsi de suite.

Hériter d'une interface générique

Les trois mêmes choix apparaissent pour l'héritage interface-à-interface — extends au lieu d'implements, mais les règles sont les mêmes :

// Pin the parameter.
public interface StringList extends List<String> { ... }

// Pass it through.
public interface MyList<E> extends List<E> { ... }

// Add new ones.
public interface IndexedList<E, I> extends List<E> { I indexOf(E e); }

Même idée que pour les classes — le paramètre du parent doit être fourni (avec un type réel ou un type transmis), et l'enfant peut ajouter ses propres paramètres en plus.

Les méthodes par défaut peuvent utiliser le paramètre de type

Java 8 a ajouté des méthodes par défaut aux interfaces. Elles peuvent utiliser le paramètre de type de l'interface exactement comme n'importe quelle méthode abstraite :

public interface Container<T> {
  void add(T item);
  T    get(int index);
  int  size();

  default boolean isEmpty()        { return size() == 0; }
  default void addAll(Iterable<T> items) {
    for (T item : items) add(item);
  }
}

La méthode par défaut addAll fonctionne pour chaque implémenteur, quel que soit le T qu'il a choisi. C'est ainsi que Collection<E> fournit forEach, removeIf, stream, et leurs équivalents — un seul corps par défaut, chaque implémentation en bénéficie.

Un exemple concret : une interface Repository générique

Une petite abstraction de dépôt — interface et deux implémentations. La première implémentation fixe le type d'entité (UserRepo ne contient que des utilisateurs) ; la seconde reste générique (InMemoryRepo<E> contient tout ce que le code appelant demande). Les deux satisfont le même contrat du point de vue de l'appelant.

java— editable, runs on the server

InMemoryRepo<E> est la forme réutilisable — le paramètre de type est transmis de l'interface à la classe, donc le même corps fonctionne pour User, String, ou tout autre type. UserRepo est la forme spécialisée — elle fixe E à User puis ajoute des méthodes qui n'ont de sens que pour les utilisateurs. Les deux respectent le même contrat Repository<E>, et tous deux héritent d'isEmpty() gratuitement grâce à la méthode par défaut.

Et ensuite

Jusqu'ici, chaque paramètre de type n'a pas eu de restriction — T pouvait être n'importe quoi. En pratique, vous souhaitez souvent dire « T doit être un Number » ou « T doit implémenter Comparable », pour pouvoir appeler des méthodes sur lui à l'intérieur du corps. C'est à cela que servent les paramètres de type bornés, et ils font l'objet du prochain chapitre. Continuez vers Paramètres de type bornés Java.

Pratique

Pratique
Vous avez `interface Repository<E> { E find(int id); }` et `class UserRepo implements Repository<User>`. Un appelant écrit `Repository r = new UserRepo();` (sans argument de type) puis `User u = r.find(1);`. Quel est le problème ?
Vous avez `interface Repository<E> { E find(int id); }` et `class UserRepo implements Repository<User>`. Un appelant écrit `Repository r = new UserRepo();` (sans argument de type) puis `User u = r.find(1);`. Quel est le problème ?
Was this page helpful?