W3docs

L'encapsulation en Java

Regroupez données et méthodes dans des classes Java et masquez les détails d'implémentation grâce aux champs privés et aux accesseurs publics.

L'encapsulation est le premier des quatre piliers de la POO, et le plus facile à mettre en œuvre concrètement : garder les données d'une classe privées, et exposer le comportement via des méthodes. La classe devient responsable de son propre état — aucun code extérieur ne peut le mettre dans un état incohérent — et le reste du programme lui parle à travers une interface que vous contrôlez.

Le mécanisme repose sur des champs private et des méthodes public. La discipline consiste à « ne pas exposer un état que vous ne pouvez pas contrôler ».

Le schéma

Une classe non encapsulée est simplement un ensemble de champs publics :

public class Account {
  public int balance;
}

Account a = new Account();
a.balance = 100;
a.balance = -50;   // nothing stops this

La version encapsulée masque le champ et expose des opérations délibérées :

public class Account {
  private int balance;

  public int  balance()              { return balance; }
  public void deposit(int amount) {
    if (amount <= 0) throw new IllegalArgumentException();
    balance += amount;
  }
  public boolean withdraw(int amount) {
    if (amount <= 0) throw new IllegalArgumentException();
    if (amount > balance) return false;
    balance -= amount;
    return true;
  }
}

Désormais, aucun code extérieur ne peut affecter une valeur négative à balance, l'écraser sans passer par la validation, ni le consulter sans passer par balance().

Ce que vous y gagnez

Des invariants fiables. La classe Account impose la règle « balance ≥ 0 après chaque opération publique ». Comme rien à l'extérieur ne peut toucher balance, la règle est applicable depuis un seul fichier, et non dans toute la base de code.

La liberté de changer. Avec balance en privé, vous pouvez plus tard changer son type de int en long, l'unité de dollars en centimes, le stocker dans un BigDecimal, ou le déplacer vers une base de données — sans modifier un seul appelant. Avec un champ public, le type et le stockage sont gravés dans chaque site d'appel.

Une API plus petite et plus claire. Les appelants ne voient que deposit, withdraw, balance — pas la douzaine d'auxiliaires privés qui les font fonctionner. La classe annonce ce qu'elle fait, pas comment.

Un raisonnement localisé. Quand quelque chose tourne mal avec balance, le bug se trouve dans l'une des trois méthodes, et non dans l'un des mille endroits qui pourraient autrement écrire le champ.

L'encapsulation ≠ getters et setters pour tout

Un anti-pattern courant consiste à générer mécaniquement un getter et un setter pour chaque champ :

public class Account {
  private int balance;
  public int  getBalance()           { return balance; }
  public void setBalance(int v)      { this.balance = v; }    // same as public field, with extra steps
}

C'est techniquement « encapsulé » au sens des manuels, mais cela n'apporte aucun des avantages réels de l'encapsulation — n'importe qui peut encore mettre l'objet dans n'importe quel état. La vraie encapsulation exprime des opérations, pas un accès brut aux champs :

  • deposit(amount) plutôt que setBalance(balance + amount)
  • withdraw(amount) retournant succès/échec plutôt que setBalance(balance - amount)
  • balance() (un accesseur en lecture seule) sans setBalance correspondant

Le prochain chapitre sur les getters et setters détaille les conventions pour savoir quand chacun est approprié.

Les copies défensives

Si le type d'un champ est mutable (un tableau, une liste, un Date), le retourner directement fait fuir le contrôle :

public class Order {
  private final List<String> items = new ArrayList<>();
  public List<String> items() { return items; }      // leak!
}

Order o = new Order();
o.items().add("apple");                              // outside code mutated the order

La solution consiste à retourner une vue non modifiable ou une copie défensive :

public List<String> items() {
  return List.copyOf(items);          // immutable snapshot
}

Le même soin s'applique aux setters qui prennent des valeurs mutables — copiez-les à l'entrée :

public Order(List<String> items) {
  this.items = new ArrayList<>(items);
}

Le chapitre sur les classes immuables approfondit ce sujet.

Choisir le bon niveau d'accès

private et public sont les deux que vous utilisez le plus souvent, mais Java en comporte quatre, et les niveaux intermédiaires ont leur importance pour l'encapsulation :

  • private — visible uniquement à l'intérieur de la même classe. La valeur par défaut pour les champs et les méthodes auxiliaires.
  • package-private (sans mot-clé) — visible pour les autres classes du même package. Utile quand quelques classes coopérantes forment une unité et doivent voir leurs membres internes, mais pas le monde extérieur.
  • protected — package-private plus visible pour les sous-classes. À réserver aux membres qu'une sous-classe a genuinement besoin de surcharger ou d'étendre.
  • public — visible partout. C'est votre API publiée ; une fois qu'un code externe en dépend, le modifier brise les appelants.

La règle générale est le principe du moindre accès : donnez à chaque membre l'accès le plus restreint qui permet encore au code de fonctionner, et élargissez seulement lorsqu'un besoin concret apparaît. Un champ ou une méthode public est une promesse que vous devez tenir. Consultez le chapitre sur les modificateurs d'accès pour le tableau complet.

L'encapsulation dans la conception

Au-delà d'une certaine taille, l'encapsulation devient un outil de conception, pas seulement une règle de codage. Chaque classe trace une frontière autour d'un ensemble d'état et des opérations sur celui-ci ; le reste du système lui parle à travers cette frontière. La plupart des patterns architecturaux — en couches, hexagonal, MVC, Clean — sont des arguments sur où tracer les frontières.

Directives pratiques :

  • Les champs sont privés par défaut. Commencez par là ; élargissez seulement avec une raison.
  • Exposez des verbes, pas des noms. cancel(), pay(), ship() valent mieux que setStatus(...).
  • Ne retournez pas les membres internes mutables tels quels. Encapsulez, copiez, ou utilisez une vue immuable.
  • Validez à l'entrée. Rejetez les états impossibles à la frontière afin que le code interne puisse supposer qu'il est valide.

Un exemple complet

java— editable, runs on the server

La suite

La partie mécanique de l'encapsulation — les champs private avec les méthodes public qui les lisent ou les écrivent — a ses propres conventions qui méritent d'être connues. Continuez avec les getters et setters.

Pratique

Pratique
Pourquoi générer mécaniquement un getter public et un setter public pour chaque champ privé va-t-il à l'encontre du but de l'encapsulation ?
Pourquoi générer mécaniquement un getter public et un setter public pour chaque champ privé va-t-il à l'encontre du but de l'encapsulation ?
Was this page helpful?