W3docs

Les interfaces Java

Définissez des contrats en Java avec les interfaces : méthodes abstraites, méthodes par défaut et héritage multiple de type.

Une interface est un contrat : un ensemble nommé d'opérations que toute classe implémentante s'engage à fournir. Les interfaces n'ont pas d'état d'instance, pas de constructeurs, et (à une exception près traitée au chapitre suivant) pas de corps de méthode. Elles décrivent ce que peut faire un type, en laissant chaque comment aux implémentations.

Les interfaces sont la réponse de Java à l'héritage multiple. Une classe étend exactement une classe mais peut implémenter un nombre quelconque d'interfaces — vous permettant de combiner un Comparable, un AutoCloseable et un Iterable sur le même type sans ambiguïté.

Déclarer une interface

Utilisez interface à la place de class :

public interface Shape {
  double area();              // implicitly public abstract
  double perimeter();
}

Les déclarations de méthodes dans une interface sont implicitement public abstract. Vous pouvez écrire ces modificateurs, mais la plupart des guides de style les suppriment comme du bruit — ils sont redondants.

Implémenter une interface

Une classe le déclare avec implements. La classe doit fournir un corps pour chaque méthode déclarée par l'interface :

public class Circle implements Shape {
  private final double r;
  public Circle(double r) { this.r = r; }

  @Override public double area()      { return Math.PI * r * r; }
  @Override public double perimeter() { return 2 * Math.PI * r; }
}

Si une classe implémente une interface mais ne fournit pas toutes les méthodes, la classe doit être déclarée abstract — même règle que pour les classes abstraites.

Vous pouvez également implémenter plusieurs interfaces à la fois :

public class Money implements Comparable<Money>, java.io.Serializable {
  private final long cents;
  public Money(long cents) { this.cents = cents; }
  public int compareTo(Money other) { return Long.compare(this.cents, other.cents); }
}

C'est « l'héritage multiple de type » — Money est à la fois un Comparable<Money> et un Serializable. Le code qui en a besoin peut prendre un Money.

Programmer contre une interface

L'intérêt des interfaces est d'écrire du code contre le contrat, et non contre l'implémentation :

public double sumAreas(List<Shape> shapes) {
  double sum = 0;
  for (Shape s : shapes) sum += s.area();
  return sum;
}

sumAreas ne connaît pas et ne se soucie pas de Circle. Ajoutez Square implements Shape, Triangle implements Shape, et la fonction fonctionne aussi sur des listes de ceux-ci — sans aucune modification.

La bibliothèque standard est construite sur ce modèle. Vous déclarez presque toujours une variable de type interface et instanciez une classe concrète :

List<String>   names = new ArrayList<>();        // List, not ArrayList
Map<String, Integer> counts = new HashMap<>();   // Map,  not HashMap

Si vous basculez ensuite vers LinkedList ou LinkedHashMap, seule la ligne new change.

Constantes sur les interfaces

Chaque champ déclaré dans une interface est implicitement public static final — une constante. Vous n'ajoutez généralement pas de constantes aux interfaces (c'est considéré comme un mauvais style — l'antipattern d'interface constante), mais la syntaxe existe :

public interface Color {
  String DEFAULT = "black";    // implicitly public static final
}

Si vous avez besoin de constantes, préférez un enum ou une simple final class avec des champs public static final.

Les interfaces peuvent étendre des interfaces

Une interface peut étendre d'autres interfaces — même plusieurs à la fois :

public interface Readable    { String read(); }
public interface Writable    { void write(String s); }
public interface ReadWrite extends Readable, Writable { }

Désormais, toute classe qui implémente ReadWrite doit fournir à la fois read() et write(). Il n'y a pas de distinction entre class extends et interface implements ici — les interfaces étendent simplement des interfaces avec extends.

Méthodes default et static (aperçu)

Depuis Java 8, les interfaces peuvent avoir des méthodes default (corps fournis via le mot-clé default) et des méthodes static. Elles vous permettent d'ajouter un comportement à une interface sans casser tous les implémenteurs existants. Le traitement complet est au chapitre suivant, méthodes default :

public interface Shape {
  double area();

  // Default method — implementors get this for free.
  default String describe() {
    return getClass().getSimpleName() + " area=" + area();
  }

  // Static factory on the interface itself.
  static Shape unitCircle() { return new Circle(1); }
}

Interfaces marqueurs

Une interface marqueur ne déclare aucune méthode. Elle existe uniquement comme étiquette que le code à l'exécution peut vérifier :

public interface Cacheable { }      // no methods

public class Snapshot implements Cacheable { ... }

Puis ailleurs : if (obj instanceof Cacheable) { ... }. Java moderne préfère les annotations pour ce type de métadonnées (@Cacheable plutôt que implements Cacheable), mais Serializable, Cloneable et RandomAccess sont des interfaces marqueurs bien connues dans la bibliothèque standard.

Interfaces fonctionnelles

Une interface avec exactement une méthode abstraite est une interface fonctionnelle. Cela a de l'importance car Java vous permet de fournir une telle interface avec une expression lambda ou une référence de méthode plutôt qu'une classe anonyme complète — le lambda est l'implémentation de cette unique méthode.

@FunctionalInterface
public interface Transformer {
  String apply(String input);     // the single abstract method
}

Transformer upper = s -> s.toUpperCase();   // lambda implements apply
System.out.println(upper.apply("hi"));      // prints: HI

L'annotation optionnelle @FunctionalInterface est un garde-fou à la compilation : le code ne compilera pas si l'interface se retrouve avec plus d'une méthode abstraite. (Les méthodes default et static ne comptent pas dans la limite.) La bibliothèque standard propose toute une boîte à outils de ces interfaces — Runnable, Comparator, Function, Predicate, Supplier — couvertes dans interfaces fonctionnelles.

Choisir entre une interface et une classe abstraite

L'arbre de décision :

  1. Les sous-classes ont-elles besoin de partager un état ou des implémentations de méthodes communes ? Si oui, vous voudrez probablement une classe abstraite — les interfaces ne peuvent pas contenir d'état d'instance.
  2. Voulez-vous un type que plusieurs classes autrement sans rapport peuvent satisfaire ? Si oui, une interface — une classe peut implémenter de nombreuses interfaces mais n'en étendre qu'une seule.
  3. Le contrat va-t-il évoluer dans le temps ? Les interfaces évoluent avec plus de précaution — ajouter une méthode abstraite à une interface casse chaque implémenteur à moins de la rendre default. Les classes abstraites peuvent ajouter une méthode concrète sans rien casser.

Dans la plupart des bases de code réelles, les interfaces l'emportent pour les contrats de type et les classes abstraites apparaissent en coulisses lorsque plusieurs implémentations partagent une infrastructure commune.

Un exemple complet

java— editable, runs on the server

Et ensuite

Les interfaces étaient autrefois des contrats purs — sans corps de méthode du tout. Depuis Java 8, c'est assoupli : les interfaces peuvent fournir des méthodes default, des méthodes static et même des méthodes auxiliaires private. Le chapitre suivant est une présentation de ces ajouts. Continuez vers méthodes default.

Pratique

Pratique
Quelle est la raison pratique pour laquelle Java n'autorise pas une classe à étendre plus d'une classe mais lui permet d'implémenter plusieurs interfaces ?
Quelle est la raison pratique pour laquelle Java n'autorise pas une classe à étendre plus d'une classe mais lui permet d'implémenter plusieurs interfaces ?
Was this page helpful?