W3docs

Abstraction en Java

Masquez les détails d'implémentation derrière des types abstraits en Java grâce aux classes abstraites et aux interfaces.

L'abstraction est le quatrième pilier de la POO : décrire ce que fait quelque chose sans s'engager sur le comment. Vous déclarez les opérations qu'un type prend en charge, laissez l'implémentation aux classes concrètes, et rédigez le reste de votre programme en ciblant le type abstrait. Ce chapitre est la vue d'ensemble conceptuelle — les deux mécanismes Java pour cela, abstract class et interface, ont chacun leur propre chapitre dédié.

Les deux questions

Toute déclaration de type en Java répond à deux questions :

  1. Que peuvent faire les appelants avec des valeurs de ce type ? (son API)
  2. Comment chacune de ces opérations est-elle implémentée ? (son corps)

Une classe concrète répond aux deux. Un type abstrait ne répond qu'à la première et laisse la seconde aux sous-types :

public interface Shape {
  double area();         // what — every Shape has an area
}

public class Circle implements Shape {
  double r;
  public Circle(double r) { this.r = r; }
  public double area() { return Math.PI * r * r; }   // how
}
public class Square implements Shape {
  double side;
  public Square(double side) { this.side = side; }
  public double area() { return side * side; }
}

Shape dit « toute forme a une aire. » Circle et Square disent comment la calculer. Le code qui accepte un Shape s'en moque :

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

Cette fonction est fermée sur l'abstraction. Elle fonctionne pour Circle et Square aujourd'hui ; pour Triangle demain ; pour Polygon dans six mois. Aucun des nouveaux types ne nécessite de modification de sumAreas.

Les deux mécanismes Java

MécanismeCe qu'il fournitQuand l'utiliser
abstract classUne classe partielle — certaines méthodes abstraites, d'autres avec des corps, ainsi que des champs et des constructeursQuand les sous-types partagent un état et du code d'infrastructure
interfaceUn contrat pur (ou quasi-pur) — des méthodes que les classes implémentantes doivent fournir ; pas d'état d'instanceQuand les sous-types doivent seulement s'accorder sur un ensemble d'opérations et peuvent n'avoir rien d'autre en commun

Une classe étend une seule classe abstraite. Une classe peut implémenter de nombreuses interfaces. Cette asymétrie oriente beaucoup de conceptions : si vous vous trouvez à vouloir un « héritage multiple », les interfaces sont généralement la réponse.

Classes abstraites — implémentation partielle

abstract sur une classe signifie « vous ne pouvez pas l'instancier directement — seulement les sous-classes. » abstract sur une méthode signifie « pas de corps ici ; chaque sous-classe concrète doit en fournir un » :

public abstract class Shape {
  public abstract double area();      // every Shape must define this

  // a concrete method, shared across all shapes
  public final String describe() {
    return getClass().getSimpleName() + " area=" + area();
  }
}

new Shape() est une erreur de compilation. new Circle() fonctionne. Dans describe, l'appel area() est dispatché vers l'implémentation de la sous-classe réelle — le même mécanisme de polymorphisme que toute méthode redéfinie.

Utilisez une classe abstraite quand les sous-types partagent vraiment du code. Si vous vous retrouvez à écrire le même helper dans trois sous-classes, c'est le signe de le remonter dans le parent.

Interfaces — le contrat

Une interface déclare des opérations et laisse l'implémentation entièrement à celui qui l'implémente :

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

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

Maintenant Money fonctionne partout où un Comparable est attendu — Collections.sort(...), TreeMap, Arrays.sort(...), vos propres algorithmes génériques. La bibliothèque standard et votre code s'accordent sur Comparable comme abstraction partagée ; aucun des deux côtés ne connaît l'autre.

La grande majorité des interfaces standard de Java (List, Map, Iterable, Runnable, Function, Comparator, AutoCloseable) fonctionnent ainsi : un contrat petit et ciblé dans lequel de nombreuses classes concrètes s'insèrent.

L'abstraction comme levier de conception

La partie mécanique de l'abstraction — le mot-clé abstract, la déclaration interface — est petite. La partie difficile est de choisir quelles abstractions définir. Trois patterns reviennent sans cesse :

  • Strategy. Définir une interface pour « l'algorithme ». Différentes implémentations échangent l'algorithme sans changer le code qui l'utilise. Comparator en est l'exemple classique.
  • Template method. Une classe abstraite implémente le flux global, avec des méthodes abstraites aux points de variation. Les sous-classes remplissent les étapes spécifiques. La méthode service de HttpServlet en est un exemple célèbre.
  • Plugin / point d'extension. Une bibliothèque publie une interface ; le code utilisateur l'implémente ; la bibliothèque le rappelle. Les API Servlet, les drivers JDBC, le BeanPostProcessor de Spring.

Dans chaque cas, le gain est le même : le code qui dépend de l'abstraction est fermé aux modifications des implémentations, et ouvert à l'ajout de nouvelles implémentations ultérieurement.

Encapsulation vs abstraction

Ces deux concepts sont cousins proches et souvent confondus.

  • L'encapsulation cache l'implémentation d'une classe spécifique (champs privés, méthodes contrôlées). C'est une préoccupation interne à la classe.
  • L'abstraction cache quelle classe vous utilisez derrière un contrat partagé. C'est une préoccupation externe à la classe.

Une classe avec des champs private et une API publique soignée est encapsulée, mais pas encore abstraite — les appelants dépendent toujours de cette classe spécifique. Remplacez le type à la frontière de l'API par une interface, et les appelants dépendent du contrat à la place. Vous pouvez alors changer d'implémentation.

Pour les voir fonctionner ensemble, consultez le chapitre encapsulation : l'encapsulation verrouille une seule classe, l'abstraction permet aux appelants d'ignorer quelle classe ils détiennent.

Erreurs courantes

Quelques pièges attrapent les débutants en abstraction :

  • Essayer d'instancier un type abstrait. new Shape() est une erreur de compilation quand Shape est abstract ou une interface. Vous instanciez un sous-type concret (new Circle(2)) et l'assignez à la référence abstraite.
  • Abstraire trop tôt. Une interface avec exactement une implémentation, écrite « au cas où nous en aurions besoin d'une autre plus tard », est généralement du poids mort. Ajoutez l'abstraction quand la deuxième implémentation apparaît réellement, ou quand vous avez vraiment besoin de découpler deux modules. Une abstraction prématurée ajoute de l'indirection sans apporter de flexibilité.
  • Laisser fuir le type concret. Déclarer un champ ou un paramètre comme ArrayList au lieu de List, ou retourner HashMap au lieu de Map, lie les appelants à cette classe spécifique et défait l'abstraction. Préférez le type le plus abstrait qui exprime encore ce dont vous avez besoin.
  • Confondre « pas de corps » avec « ne fait rien ». Une méthode abstraite n'a pas de corps parce que les sous-classes doivent en fournir un. Une méthode concrète avec un corps vide est une vraie méthode qui ne fait rien — un contrat très différent.

Un exemple complet

Exécutez le programme ci-dessous. Il utilise les deux mécanismes : une classe Shape abstraite avec du code describe partagé, et une interface pure Greeter. La sortie attendue est :

Circle area=12.566370614359172
Square area=9.0
total = 21.57

Dear Alice,
hey Alice!

Remarquez que totalArea et la boucle de salutation ne nomment jamais Circle, Square, FormalGreeter ou CasualGreeter — ils parlent uniquement aux abstractions Shape et Greeter.

java— editable, runs on the server

Et ensuite

Le chapitre suivant aborde les mécanismes concrets des classes abstraites — les méthodes abstraites, ce qu'elles permettent à une sous-classe d'hériter, et quand les préférer aux interfaces.

Pratique

Pratique
Laquelle des options saisit le mieux la différence entre encapsulation et abstraction ?
Laquelle des options saisit le mieux la différence entre encapsulation et abstraction ?
Pratique
Quand devriez-vous choisir une interface plutôt qu'une classe abstraite ?
Quand devriez-vous choisir une interface plutôt qu'une classe abstraite ?
Was this page helpful?