C2 — EncapsulationDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Licence 2 · Programmation orientée objet

Cours 2Encapsulation

Rendre un objet responsable de sa propre cohérence : ce qu'il expose, ce qu'il cache, et ce qu'il refuse.

2 chapitres · 10 h de travail estimé

  1. 1. Visibilité et accesseurs5 h
  2. 2. Membres de classe et surcharge5 h

Chapitre 1 · 5 h

Visibilité et accesseurs

private, public, protected ; getters et setters ; invariants de classe et validation ; pourquoi un attribut public est une erreur de conception.

Le chapitre 2 a écrit une règle dans une méthode : on ne dépose pas un montant négatif, on n'emprunte pas un livre déjà emprunté. C'était censé être le gain de l'objet — un seul endroit où faire respecter une règle.

Sauf que rien n'oblige à passer par là.

compte.deposer(-500);      /* refusé par la méthode */compte.solde = -500;       /* accepté, et personne n'a rien vu */

La deuxième ligne contourne toute la logique de la classe. Tant qu'elle est possible, l'invariant n'est pas une garantie mais une politesse. Ce chapitre la rend impossible.

Les quatre niveaux de visibilité

Un modificateur devant un attribut ou une méthode dit qui a le droit d'y toucher.

ModificateurVisible depuis
privatela classe elle-même, et rien d'autre
(aucun)le paquetage — visibilité par défaut en Java
protectedle paquetage, plus les classes filles (chapitre 5)
publicpartout

Deux remarques sur ce tableau.

La visibilité par défaut n'est pas public, contrairement à ce que beaucoup supposent : c'est la visibilité de paquetage, plus restrictive. Écrire un attribut sans modificateur n'est donc pas neutre, c'est un choix — généralement fait par inadvertance.

protected est plus permissif que la visibilité par défaut, et pas seulement « pour les filles » : il ouvre aussi au paquetage entier. C'est une nuance qui surprend, et une raison de ne pas l'employer par réflexe.

La règle de conduite tient en une phrase : partir de private, et n'ouvrir que ce qu'on peut justifier. Restreindre après coup casse le code des autres ; ouvrir après coup ne casse rien.

Accesseurs

public class Compte {    private double solde;     public double getSolde() { return this.solde; }     public void deposer(double montant) {        if (montant <= 0) throw new IllegalArgumentException("montant non positif");        this.solde += montant;    }}

Trois choses à observer, et la troisième est le vrai sujet du chapitre.

Un accesseur en lecturegetSolde — laisse consulter sans laisser modifier. C'est le cas le plus fréquent, et le plus inoffensif.

Il n'y a pas de setSolde, et c'est délibéré. Un solde ne se fixe pas, il se modifie par des opérations métier : deposer, retirer. Fournir un setSolde public rétablirait exactement le trou qu'on vient de boucher.

Un accesseur n'est pas obligatoire. L'erreur la plus répandue chez les débutants est de générer mécaniquement un getter et un setter pour chaque attribut — l'éditeur le propose en un clic. Le résultat est une classe dont tous les champs sont modifiables de l'extérieur, avec deux lignes de cérémonie en plus : l'encapsulation est perdue, et le code est plus long.

La question à se poser pour chaque attribut est donc : qui a besoin de le lire ? qui a besoin de le modifier, et sous quelles conditions ? Le plus souvent, la réponse à la seconde question est « personne, sinon la classe elle-même ».

L'invariant de classe

C'est la notion que le chapitre doit installer, et elle donne un critère objectif.

Un invariant de classe est une propriété qui doit être vraie de la fin du constructeur jusqu'à la fin de la vie de l'objet, et qu'aucune méthode publique ne doit pouvoir violer.

Compte :   solde ≥ 0   et   titulaire ≠ nullLivre  :   si emprunteur ≠ null alors disponible = falseDate   :   1 ≤ jour ≤ nombre de jours du mois

Deux devoirs en découlent, et ils se répartissent clairement.

Le constructeur l'établit. Un objet ne doit pas pouvoir naître dans un état invalide : c'est le rôle de la validation vue au chapitre 2. Un constructeur qui accepte n'importe quoi reporte le problème sur toutes les méthodes.

Chaque méthode publique le préserve. Elle peut le rompre temporairement à l'intérieur — le temps d'une suite d'affectations — mais il doit être rétabli quand elle rend la main.

L'invariant donne enfin la réponse à « faut-il un setter ? ». Un setter est acceptable s'il peut vérifier l'invariant :

public void setTitulaire(String titulaire) {    if (titulaire == null || titulaire.isBlank()) {        throw new IllegalArgumentException("titulaire vide");    }    this.titulaire = titulaire;}

Un setter qui se contente de this.x = x n'est pas un setter : c'est un attribut public écrit en trois lignes.

Quiz · 1 question

Un étudiant déclare tous ses attributs private, puis génère un getter et un setter pour chacun. Qu'a-t-il gagné ?

  • L'encapsulation complète : les attributs ne sont plus accessibles directementencapsulation complète
  • Presque rien : n'importe qui peut toujours mettre n'importe quelle valeur, à travers le setter. L'encapsulation n'est pas de cacher les attributs mais de contrôler qui peut les changer, et sous quelles conditionspresque rien
  • Rien du tout, et c'est même une régression : le code est plus long et plus lentrégression

Réponse : Un setter qui se contente de this.x = x expose exactement ce qu'un attribut public exposait : n'importe quel appelant peut y mettre n'importe quoi, l'invariant n'est pas protégé, et le code a seulement gagné deux lignes de cérémonie. Le gain réel n'est pas nul pour autant — la classe peut plus tard ajouter une validation, journaliser, ou changer sa représentation interne sans casser ses appelants, ce qu'un attribut public interdirait. Mais tant que le setter est vide de règles, cette possibilité n'est pas exploitée. La bonne démarche est de se demander, attribut par attribut : qui a besoin de le lire ? qui a besoin de le modifier, et sous quelles conditions ? Le plus souvent, la réponse à la seconde question est « personne, sinon la classe » — et l'on écrit alors des opérations métier (deposer, retirer) plutôt qu'un setter.

Pourquoi un attribut public est une erreur de conception

Quatre raisons, indépendantes les unes des autres.

On ne peut plus rien vérifier. C'est le cas de l'introduction, et le plus visible.

On ne peut plus changer la représentation interne. Si Point expose x et y publics, tout le code utilisateur écrit p.x — et passer plus tard en coordonnées polaires devient impossible sans réécrire chaque appelant. Avec getX(), la conversion se fait dans la méthode et personne d'autre n'est touché. C'est le bénéfice le moins visible et le plus durable.

On ne peut rien ajouter au passage. Journaliser les modifications, mettre un cache à jour, notifier un observateur, verrouiller pour l'accès concurrent : tout cela suppose un point de passage. Un attribut public n'en a pas.

On perd la lecture seule. Un attribut public est lisible et modifiable, sans nuance. Un getter sans setter donne exactement ce que final ne peut pas toujours exprimer.

Une fuite plus subtile

Encapsuler les attributs ne suffit pas toujours, et c'est le piège avancé du chapitre.

public class Reservation {    private Date debut;    public Date getDebut() { return this.debut; }     /* fuite */}

L'attribut est private, le getter ne fait que lire — et pourtant l'invariant n'est pas protégé. getDebut() rend la référence vers l'objet Date interne, au sens du chapitre 2 : l'appelant peut donc appeler setTime dessus et modifier la réservation de l'extérieur, sans jamais passer par une méthode de Reservation.

Deux parades. Rendre une copiereturn new Date(this.debut.getTime()) — ou, bien mieux, employer un type immuable, comme LocalDate en Java moderne : un objet qu'on ne peut pas modifier ne peut pas fuir.

La règle générale : l'encapsulation porte sur ce que l'objet laisse faire, pas seulement sur ce qu'il laisse voir. Rendre une référence vers une structure interne modifiable — un tableau, une liste, une date mutable — revient à la rendre publique.

Et l'excès inverse

Une nuance, pour ne pas transformer la règle en rituel.

Une classe qui n'est **qu'**un ensemble de getters et de setters, sans aucun comportement, est un symptôme : la logique qui devrait lui appartenir a été écrite ailleurs, chez ses appelants. On parle alors de modèle anémique, et l'on retombe sur le procédural du chapitre 1 avec plus de cérémonie.

Le remède tient en une formule : « dire, ne pas demander ». Plutôt que

if (compte.getSolde() >= montant) { compte.setSolde(compte.getSolde() - montant); }

on écrit compte.retirer(montant), et la règle reste où elle doit être. Le premier code met la logique chez l'appelant — donc dupliquée partout où l'on retire — le second la met dans l'objet qui possède la donnée.

Quiz · 1 question

Une classe Reservation a un attribut private Date debut et un getter qui rend cet objet. L'invariant est-il protégé ?

  • Oui : l'attribut est private, et le getter ne fait que lire sans permettre l'affectationprotégé
  • Non : le getter rend la RÉFÉRENCE vers l'objet Date interne, que l'appelant peut ensuite modifier avec setTime — sans jamais passer par une méthode de Reservationfuite de référence
  • Non, mais uniquement si la classe est dans un autre paquetagequestion de paquetage

Réponse : C'est le piège avancé de l'encapsulation, et il découle du chapitre 2 : une variable ne contient pas l'objet mais une RÉFÉRENCE vers lui. Rendre cette référence, c'est donner à l'appelant exactement la même prise que celle de la classe. Il ne peut certes pas faire reservation.debut = ..., mais il peut faire reservation.getDebut().setTime(...), ce qui modifie l'état interne sans passer par aucune méthode. Deux parades : rendre une COPIE défensive, ou — bien mieux — employer un type IMMUABLE comme LocalDate, qu'on ne peut pas modifier et qui ne peut donc pas fuir. La règle générale : l'encapsulation porte sur ce que l'objet laisse FAIRE, pas seulement sur ce qu'il laisse voir. Rendre un tableau ou une liste interne pose exactement le même problème.

À vous

L'exercice suit le fil de bibliothèque du semestre, et procède par dégradations successives.

D'abord un Livre à attributs publics : vous cassez son invariant en deux lignes, sans que rien ne proteste. Puis vous encapsulez, et vous constatez que les mêmes deux lignes ne compilent plus — ici, ne s'exécutent plus.

Ensuite le piège de la référence qui fuit : la classe rend sa liste d'emprunteurs, et l'appelant la vide. Vous écrirez les deux parades.

Enfin la démonstration du « dire, ne pas demander » : la même opération écrite chez l'appelant puis dans l'objet, et ce qui se passe quand la règle change.

Exercice de code

Cassez un invariant par des attributs publics, encapsulez, puis réparez la fuite de référence.

Point de départ

// ── 1. Attributs publics : l'invariant ne tient à rien ────────────────────
class LivreOuvert {
  constructor(titre) {
    this.titre = titre;
    this.disponible = true;
    this.emprunteur = null;
  }
  // Invariant voulu : emprunteur non nul  <=>  disponible faux.
  emprunter(qui) {
    if (!this.disponible) return false;
    this.disponible = false;
    this.emprunteur = qui;
    return true;
  }
  invariantOk() {
    return (this.emprunteur === null) === this.disponible;
  }
  toString() {
    return this.titre + " [" + (this.disponible ? "libre" : "pris par " + this.emprunteur) + "]";
  }
}

// ── 2. Version encapsulée ─────────────────────────────────────────────────
class Livre {
  #titre; #disponible; #emprunteurs;      // # = champ réellement privé

  constructor(titre) {
    if (!titre) throw new Error("un livre doit avoir un titre");
    this.#titre = titre;
    this.#disponible = true;
    this.#emprunteurs = [];
  }
  get titre() { return this.#titre; }
  estDisponible() { return this.#disponible; }

  emprunter(qui) {
    // ← à écrire : refuser si déjà emprunté, sinon enregistrer l'emprunteur
    return false;
  }
  rendre() { this.#disponible = true; }

  // ← FUITE : ce getter rend la liste INTERNE, que l'appelant peut vider.
  getEmprunteurs() { return this.#emprunteurs; }
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Cassez l'invariant de LivreOuvert en DEUX lignes, sans appeler emprunter.
// 2. Écrivez emprunter() dans Livre, et vérifiez que la même attaque échoue.
// 3. Réparez la fuite de getEmprunteurs, de deux façons : copie défensive,
//    puis exposition d'une valeur immuable.
// 4. Écrivez la même opération « emprunter si disponible » chez l'appelant,
//    puis dans l'objet. Que se passe-t-il quand la règle change ?

const a = new LivreOuvert("Dune");
console.log("   " + a.toString() + "   invariant :", a.invariantOk());

Solution

class LivreOuvert {
  constructor(titre) { this.titre = titre; this.disponible = true; this.emprunteur = null; }
  emprunter(qui) {
    if (!this.disponible) return false;
    this.disponible = false; this.emprunteur = qui; return true;
  }
  invariantOk() { return (this.emprunteur === null) === this.disponible; }
  toString() {
    return this.titre + " [" + (this.disponible ? "libre" : "pris par " + this.emprunteur) + "]";
  }
}

console.log("— 1. attributs publics : deux lignes suffisent —");
const a = new LivreOuvert("Dune");
console.log("   départ    : " + a.toString() + "   invariant : " + a.invariantOk());
a.emprunteur = "Ana";        // on contourne entièrement emprunter()
a.disponible = true;         // et l'on ment sur la disponibilité
console.log("   après     : " + a.toString() + "   invariant : " + a.invariantOk());
console.log("   Le livre est « libre » et pris par Ana. Aucune erreur, aucune trace.");

class Livre {
  #titre; #disponible; #emprunteurs;

  constructor(titre) {
    if (!titre) throw new Error("un livre doit avoir un titre");
    this.#titre = titre;
    this.#disponible = true;
    this.#emprunteurs = [];
  }
  get titre() { return this.#titre; }
  estDisponible() { return this.#disponible; }

  emprunter(qui) {
    // La règle, en un seul endroit — et cette fois on ne peut plus l'éviter.
    if (!this.#disponible) return false;
    if (!qui) throw new Error("emprunteur non renseigné");
    this.#disponible = false;
    this.#emprunteurs.push(qui);
    return true;
  }
  rendre() { this.#disponible = true; }

  // Copie défensive : l'appelant reçoit SA liste, pas la nôtre.
  getEmprunteurs() { return [...this.#emprunteurs]; }
  // Mieux encore : n'exposer qu'une valeur immuable, ici un simple compte.
  get nombreEmprunts() { return this.#emprunteurs.length; }

  toString() {
    return this.#titre + " [" + (this.#disponible ? "libre" : "emprunté") +
           ", " + this.#emprunteurs.length + " emprunt(s)]";
  }
}

console.log("");
console.log("— 2. la même attaque, sur la version encapsulée —");
const b = new Livre("Dune");
b.emprunter("Ana");
b.disponible = true;         // crée un champ PUBLIC sans rapport, inoffensif
console.log("   " + b.toString());
console.log("   estDisponible() rend toujours " + b.estDisponible() +
            " : le champ privé est intact, l'invariant tient.");
try { console.log(b.#titre); } catch (e) { console.log("   accès direct à #titre : refusé"); }

console.log("");
console.log("— 3. la fuite de référence —");
const c = new Livre("Solaris");
c.emprunter("Bo"); c.rendre(); c.emprunter("Cy");
const liste = c.getEmprunteurs();
liste.length = 0;            // l'appelant vide SA copie
console.log("   après que l'appelant a vidé la liste reçue : " + c.toString());
console.log("   Sans la copie défensive, le compte serait tombé à 0 : l'appelant");
console.log("   aurait modifié l'état interne sans passer par aucune méthode.");
console.log("   nombreEmprunts (valeur immuable) : " + c.nombreEmprunts + " — rien à fuir.");

console.log("");
console.log("— 4. dire, ne pas demander —");
// Chez l'appelant : la règle est ici, donc dupliquée partout où l'on emprunte.
function emprunterChezAppelant(livre, qui) {
  if (livre.estDisponible()) { livre.emprunter(qui); return true; }
  return false;
}
// Dans l'objet : la règle est là où vit la donnée.
const d = new Livre("Ubik");
console.log("   premier emprunt : " + d.emprunter("Ana"));
console.log("   second emprunt  : " + d.emprunter("Bo") + "   (refusé par l'objet)");
console.log("   Le jour où la règle devient « deux emprunts simultanés autorisés »,");
console.log("   la version « chez l'appelant » demande de retrouver tous les appels ;");
console.log("   la version « dans l'objet » demande de modifier une méthode.");

En travaux pratiques

Travaux pratiques 3 · 3 h

L'encapsulation qui fuit

Vérifier qu'un champ privé peut être modifié de l'extérieur si l'accesseur est mal écrit — et découvrir que l'invariant se protège, pas se déclare.

Avant de commencer

  • Le TP 2
  • Les collections de base de Java

Énoncé

  1. Casser l'invariantRendez publics les champs de Document. Écrivez du code extérieur qui met une année à moins mille et une disponibilité incohérente. Comptez les lignes qu'il vous a fallu.
  2. FermerRepassez tout en privé, ajoutez des accesseurs, et vérifiez que le code précédent ne compile plus.
  3. La fuite par l'accesseurAjoutez à Mediatheque une liste privée de documents et un accesseur qui la renvoie. Depuis l'extérieur, videz la liste. Constatez que le privé n'a rien protégé. Indice : Renvoyer une référence sur une collection mutable, c'est en donner les clés.
  4. Colmater, trois façonsCorrigez en renvoyant une copie, puis une vue non modifiable, puis en ne renvoyant rien du tout et en exposant les opérations utiles. Comparez les trois.
  5. La fuite par le constructeurPassez une liste au constructeur de Mediatheque et gardez-en la référence. Modifiez ensuite la liste d'origine depuis l'extérieur. Corrigez.
  6. Les accesseurs inutilesComptez, dans votre code, les accesseurs qui ne servent qu'à recopier un champ vers l'extérieur. Pour chacun, demandez-vous quelle question du domaine il répond.
  7. Déplacer le comportementTrouvez une portion de code extérieur qui lit trois accesseurs pour décider quelque chose. Déplacez cette décision DANS la classe et supprimez les accesseurs devenus inutiles.
  8. Le champ finalRendez immuables les champs qui ne changent jamais après construction. Vérifiez ce que le compilateur refuse désormais.

C'est réussi quand

  • Vous videz une liste « privée » depuis l'extérieur, en une ligne
  • Après correction, la même ligne ne compile plus ou n'a plus d'effet
  • Vous avez supprimé au moins deux accesseurs en déplaçant une décision

Correction

La fuite par l'accesseur
public class Mediatheque {
  private List<Document> documents = new ArrayList<>();
  public List<Document> getDocuments() { return documents; }   /* FUITE */
}

/* depuis l'extérieur */
mediatheque.getDocuments().clear();       /* tout est effacé */
mediatheque.getDocuments().add(null);     /* un null dans la collection */

Le champ est privé, la référence ne l'est pas. Renvoyer une collection mutable revient à la rendre publique — et c'est la fuite d'encapsulation la plus fréquente en Java, précisément parce que le mot private donne l'impression que le travail est fait.

Les trois corrections
/* 1. copie défensive : l'appelant modifie SA copie */
public List<Document> getDocuments() { return new ArrayList<>(documents); }

/* 2. vue non modifiable : moins coûteux, échoue à l'exécution */
public List<Document> getDocuments() {
  return Collections.unmodifiableList(documents);
}

/* 3. ne rien exposer : exposer les OPÉRATIONS */
public int nombreDeDocuments()          { return documents.size(); }
public Optional<Document> chercher(String titre) { … }
public void ajouter(Document d)         { … validation … }

La troisième est la meilleure, et c'est celle qu'on écrit le moins. Les deux premières laissent l'appelant décider quoi faire de la liste ; la troisième garde la décision dans la classe qui possède l'invariant. La deuxième a un défaut à connaître : elle échoue à l'exécution, pas à la compilation, et la vue reflète les modifications internes ultérieures.

La fuite par le constructeur
public Mediatheque(List<Document> initiaux) {
  this.documents = initiaux;          /* FUITE : référence partagée */
}

List<Document> liste = new ArrayList<>();
Mediatheque m = new Mediatheque(liste);
liste.clear();                          /* la médiathèque est vidée */

/* correction : copier À L'ENTRÉE aussi */
this.documents = new ArrayList<>(initiaux);

L'encapsulation se protège aux DEUX bouts : ce qui entre et ce qui sort. On l'oublie systématiquement à l'entrée, parce que le danger est moins visible. La règle : toute collection ou tout objet mutable reçu de l'extérieur, et conservé, se copie.

L'accesseur qui ne répond à aucune question
/* AVANT : la décision est dehors */
if (doc.getAnnee() < 1900
  && doc.getEtat().equals("fragile")
  && !doc.estDisponible()) { … }

/* APRÈS : la décision est dans l'objet */
if (doc.necessiteUneConsultationSurPlace()) { … }

/* et getEtat(), getAnnee() peuvent disparaître de l'interface publique */

Un accesseur n'est pas une faute en soi ; c'en est une quand il sert à prendre au-dehors une décision qui appartient à l'objet. Le symptôme est facile à repérer : du code extérieur qui lit plusieurs accesseurs du même objet pour conclure quelque chose. Ce qu'on appelle « tell, don't ask » — demandez à l'objet d'agir, ne lui soutirez pas ses données pour agir à sa place.

final, et ce qu'il garantit
private final String titre;      /* fixé à la construction, définitif */
private boolean disponible;      /* varie légitimement */

titre = "autre";                 /* ERREUR DE COMPILATION */

/* mais attention : */
private final List<Document> docs = new ArrayList<>();
docs.add(...);                   /* AUTORISÉ : la RÉFÉRENCE est finale,
                                  pas le contenu */

final s'applique à la référence, jamais à l'objet pointé. C'est une garantie vérifiée à la COMPILATION, donc gratuite, et elle documente l'intention mieux qu'un commentaire. La distinction entre référence immuable et objet immuable est exactement celle du pointeur constant en C — et c'est elle qui rend les copies défensives nécessaires malgré final.

Ce que la suite en fait

Le chapitre 4 complète la boîte à outils de la classe : ce qui appartient à la classe plutôt qu'aux objets — les membres statiques —, et la possibilité de donner plusieurs formes à une même opération, avec la surcharge.

Le chapitre 5 fera réapparaître protected, et l'on comprendra alors pourquoi ce modificateur est plus délicat qu'il n'en a l'air : ouvrir un attribut à ses classes filles, c'est signer un contrat avec du code qui n'est pas encore écrit.

À retenir

Flashcards · 6 cartes

Quels sont les quatre niveaux de visibilité en Java, et lequel surprend ?
private (la classe seule), AUCUN modificateur (le paquetage — c'est la visibilité PAR DÉFAUT, pas public), protected (le paquetage PLUS les classes filles), public (partout). Deux surprises : la visibilité par défaut n'est pas public ; et protected est plus PERMISSIF que le défaut, puisqu'il ouvre au paquetage entier en plus des filles. Règle : partir de private et n'ouvrir que ce qu'on peut justifier — restreindre après coup casse le code des autres, ouvrir ne casse rien.
Qu'est-ce qu'un invariant de classe, et qui en est responsable ?
Une propriété qui doit être vraie DE LA FIN DU CONSTRUCTEUR jusqu'à la fin de la vie de l'objet, et qu'aucune méthode publique ne peut violer — par exemple solde ≥ 0 et titulaire non nul. Le CONSTRUCTEUR l'établit : un objet ne doit pas pouvoir naître invalide. Chaque MÉTHODE PUBLIQUE le préserve : elle peut le rompre temporairement à l'intérieur, mais doit l'avoir rétabli quand elle rend la main.
Un getter et un setter générés pour chaque attribut : qu'a-t-on gagné ?
Presque rien tant que le setter est vide de règles : n'importe qui peut toujours mettre n'importe quoi, avec deux lignes de cérémonie en plus. Le gain potentiel subsiste — pouvoir ajouter plus tard une validation, journaliser, ou changer la représentation interne sans casser les appelants — mais il n'est pas exploité. Un setter n'est légitime que s'il VÉRIFIE l'invariant ; sinon c'est un attribut public écrit en trois lignes. Et souvent il ne faut pas de setter du tout, mais des opérations métier : deposer, retirer.
Pourquoi un attribut public est-il une erreur de conception ? Donnez plus d'une raison.
1) On ne peut plus rien VÉRIFIER. 2) On ne peut plus changer la REPRÉSENTATION INTERNE : si Point expose x et y, tout le code écrit p.x et passer en polaires devient impossible — c'est le bénéfice le moins visible et le plus durable. 3) On ne peut rien AJOUTER au passage : journalisation, cache, notification, verrou supposent un point de passage. 4) On perd la LECTURE SEULE : public est lisible et modifiable, sans nuance.
Pourquoi un getter qui rend un objet mutable est-il une fuite, et que faire ?
Parce qu'il rend la RÉFÉRENCE vers l'objet interne : l'appelant peut le modifier — getDebut().setTime(...) — sans passer par aucune méthode de la classe. Deux parades : rendre une COPIE défensive, ou employer un type IMMUABLE (LocalDate), qu'on ne peut pas modifier et qui ne peut donc pas fuir. Vaut aussi pour un tableau ou une liste interne. Règle : l'encapsulation porte sur ce que l'objet laisse FAIRE, pas seulement sur ce qu'il laisse voir.
Qu'est-ce qu'un modèle anémique, et quelle formule y répond ?
Une classe qui n'est QU'un ensemble de getters et de setters, sans comportement : la logique qui devrait lui appartenir a été écrite chez ses appelants, et l'on retombe sur le procédural avec plus de cérémonie. La formule : « DIRE, NE PAS DEMANDER ». Écrire compte.retirer(montant) plutôt que tester getSolde() puis appeler setSolde() chez l'appelant — sinon la règle est dupliquée partout où l'on retire.

Chapitre 2 · 5 h

Membres de classe et surcharge

Attributs et méthodes statiques, constantes, surcharge de méthodes et de constructeurs, variable de classe contre variable d'instance, passage par valeur et par référence.

Le tout premier programme Java qu'un étudiant écrit contient un mot qu'il ne peut pas encore comprendre :

public static void main(String[] args) { ... }

Pourquoi static ? Parce que la machine virtuelle doit appeler cette méthode avant qu'aucun objet n'existe — elle n'a aucune instance sur laquelle l'invoquer. C'est exactement ce que static signifie : appartenir à la classe, et non à un objet.

Ce chapitre traite de ce qui appartient à la classe, puis de la possibilité de donner plusieurs formes à une même opération. Et il referme le point qui coince du chapitre 2, en le posant frontalement : combien d'exemplaires de cette variable existe-t-il ?

Une case pour la classe

public class Compte {    private static int nombreDeComptes = 0;    // UNE case, pour toute la classe    private double solde;                       // une case PAR objet     public Compte(double depot) {        this.solde = depot;        nombreDeComptes++;    }     public static int getNombreDeComptes() { return nombreDeComptes; }}

L'animation du chapitre 2 l'a montré pas à pas : solde existe en autant d'exemplaires qu'il y a d'objets, nombreDeComptes en un seul, créé au chargement de la classe et vivant jusqu'à la fin du programme.

Trois conséquences suivent, et la troisième est le piège.

On accède à un membre statique par la classe : Compte.getNombreDeComptes(). Java tolère monCompte.getNombreDeComptes(), ce qui est légal et trompeur — cela donne l'impression d'une donnée de l'objet. Il faut prendre l'habitude d'écrire le nom de la classe.

Une méthode statique n'a pas de this. Elle ne s'exécute sur aucun objet, donc elle ne peut pas lire un attribut d'instance. C'est l'erreur du premier main : appeler une méthode d'instance depuis main sans avoir créé d'objet donne non-static variable cannot be referenced from a static context. Le remède est toujours le même — créer un objet, et travailler dessus.

L'inverse est permis : une méthode d'instance peut lire un membre statique, puisqu'il existe toujours.

Quand employer static ? Trois cas légitimes, et un seul contre-emploi.

Les constantes, écrites static final et en majuscules : public static final double TVA = 0.2;. Une par classe suffit, elles ne changent pas, et les dupliquer par objet serait absurde.

Les compteurs et registres partagés, comme ci-dessus.

Les méthodes utilitaires sans état : Math.sqrt, Integer.parseInt. Elles ne dépendent d'aucun objet, donc elles n'en demandent pas.

Le contre-emploi est de rendre statique par commodité, pour éviter d'avoir à créer un objet. On obtient alors un état global modifiable de partout — exactement le défaut du chapitre 1, sous un autre nom.

Constantes

public static final int CAPACITE_MAX = 100;

Trois mots, trois effets. static : une seule case. final : la valeur ne peut plus changer après initialisation. Le nom en majuscules est une convention, pas une règle du langage — mais elle est universellement suivie.

L'attention porte sur un point : final interdit de réaffecter la référence, pas de modifier l'objet qu'elle désigne.

public static final List<String> NOMS = new ArrayList<>();NOMS = new ArrayList<>();     /* refusé */NOMS.add("Ana");              /* AUTORISÉ */

C'est la même distinction qu'au chapitre 2 entre la référence et l'objet, et c'est ce qui rend final insuffisant à lui seul pour garantir l'immuabilité.

Surcharge

Surcharger une méthode, c'est déclarer plusieurs méthodes de même nom avec des signatures différentes — nombre, types ou ordre des paramètres.

public void afficher(String texte)              { ... }public void afficher(String texte, int fois)    { ... }public void afficher(double valeur)             { ... }

Le compilateur choisit d'après les types des arguments de l'appel. Deux règles à connaître.

Le type de retour ne fait pas partie de la signature. Deux méthodes ne différant que par leur retour ne compilent pas : int lire() et String lire() sont un conflit.

Le choix est fait à la COMPILATION, sur les types déclarés. Retenez cette phrase : elle sera le contraste central du chapitre 6, où la redéfinition, elle, se résout à l'exécution sur le type réel. Surcharge et redéfinition portent des noms voisins et fonctionnent de façon opposée.

La surcharge de constructeurs est le cas le plus fréquent, et elle se combine avec le this du chapitre 2 :

public Compte(String titulaire, double depot) {    this.titulaire = titulaire;    this.solde = depot;}public Compte(String titulaire) {    this(titulaire, 0);        /* enchaîne sur l'autre constructeur */}

Écrire this(...) plutôt que dupliquer l'initialisation garantit qu'il n'existe qu'un seul endroit où l'invariant est établi — c'est le chapitre 3 appliqué aux constructeurs.

Quiz · 1 question

Une classe déclare afficher(double) et afficher(int). On appelle afficher(5). Quelle méthode est choisie, et quand ?

  • afficher(int), choisie à l'EXÉCUTION d'après la valeur réelle de l'argumentà l'exécution
  • afficher(int), choisie à la COMPILATION d'après le type déclaré de l'argument : 5 est un littéral entier, et le compilateur préfère toujours la correspondance exacte à une conversionà la compilation
  • Le code ne compile pas : deux méthodes de même nom sont ambiguësambigu

Réponse : La résolution de surcharge est entièrement l'affaire du COMPILATEUR : il regarde les types déclarés des arguments et choisit la méthode la plus spécifique qui accepte l'appel, en préférant une correspondance exacte à une conversion élargissante. Le littéral 5 est un int, donc afficher(int) l'emporte ; afficher(5.0) choisirait l'autre, et si afficher(int) n'existait pas, le 5 serait converti en double. Deux méthodes de même nom ne sont ambiguës que si aucune n'est plus spécifique que l'autre. RETENEZ CETTE PHRASE : le choix se fait à la compilation, sur les types déclarés. Le chapitre 6 présentera la REDÉFINITION, qui porte un nom voisin et fonctionne à l'inverse — résolution à l'exécution, sur le type réel. Confondre les deux est l'erreur la plus fréquente du bloc III.

Passage de paramètres en Java

C'est la question d'examen classique du chapitre, et la réponse tient en une phrase que presque personne n'énonce correctement.

Java passe toujours par valeur. Il n'existe aucun passage par référence, contrairement à ce qu'on lit souvent. Ce qui trompe, c'est que la valeur d'une variable objet est une référence — et copier une référence donne un second nom pour le même objet.

D'où deux comportements qu'il faut savoir distinguer.

static void modifier(Compte c) {    c.deposer(100);            /* VISIBLE par l'appelant : même objet */} static void remplacer(Compte c) {    c = new Compte(0);         /* INVISIBLE : on change une copie locale */}

modifier agit sur l'objet désigné, et l'appelant le voit. remplacer réaffecte le paramètre, qui est une variable locale à la méthode : l'appelant garde sa référence d'origine.

C'est exactement le chapitre 7 du cours de programmation : passer un pointeur permet de modifier ce qu'il désigne, mais réaffecter le pointeur lui-même n'a aucun effet chez l'appelant. La seule différence est que Java écrit tout cela sans étoile ni esperluette.

Conséquence directe : on ne peut pas écrire echanger(a, b) en Java. La méthode échangerait ses deux copies de références, et l'appelant ne verrait rien. Il faut passer par un objet conteneur, un tableau, ou rendre un résultat.

Les types primitifsint, double, boolean, char — ne sont pas des objets : leur valeur est copiée directement, et aucune modification n'est jamais visible de l'appelant.

Quiz · 1 question

Une méthode reçoit un Compte en paramètre. Dans un cas elle appelle c.deposer(100), dans l'autre elle fait c = new Compte(0). Que voit l'appelant ?

  • Les deux modifications, puisque les objets sont passés par référence en Javales deux
  • Seulement le dépôt : Java passe la référence PAR VALEUR, donc modifier l'objet désigné est visible, mais réaffecter le paramètre ne change qu'une variable locale à la méthodeseulement le dépôt
  • Aucune des deux : les paramètres sont toujours des copies complètes de l'objetaucune

Réponse : Java passe TOUJOURS par valeur — il n'existe aucun passage par référence, malgré ce qu'on lit souvent. Ce qui trompe est que la valeur d'une variable objet EST une référence : la copier donne un second nom pour le même objet, donc c.deposer(100) agit bien sur l'objet de l'appelant. En revanche, c = new Compte(0) réaffecte le PARAMÈTRE, qui est une variable locale à la méthode : l'appelant conserve sa référence d'origine et ne voit rien. C'est mot pour mot le chapitre 7 du cours de programmation — passer un pointeur permet de modifier ce qu'il désigne, réaffecter le pointeur n'a aucun effet chez l'appelant — mais sans étoile ni esperluette. Conséquence pratique : on ne peut pas écrire echanger(a, b) en Java. Et les types primitifs, qui ne sont pas des objets, sont copiés directement : aucune modification n'est jamais visible.

À vous

L'exercice est en trois volets, et le troisième est celui qu'il faut faire jusqu'au bout.

Le compteur statique d'abord : plusieurs objets, une seule case, et la vérification qu'un compteur d'instance à la place donnerait toujours 1.

Puis la résolution de surcharge : plusieurs méthodes de même nom, et un journal indiquant laquelle a été choisie pour chaque appel. Vous chercherez le cas où la conversion l'emporte, et celui où l'appel devient ambigu.

Enfin le passage de paramètres, avec les trois cas côte à côte — modifier l'objet, réaffecter le paramètre, et un type primitif — puis la tentative d'écrire echanger, qui échoue, et les deux contournements possibles.

Exercice de code

Comptez les instances par un membre de classe, résolvez une surcharge, puis constatez l'échec de echanger.

Point de départ

// ── 1. Une case pour la classe ────────────────────────────────────────────
class Compte {
  static nombreDeComptes = 0;      // UNE case, pour toute la classe
  #solde;                          // une case PAR objet

  constructor(titulaire, depot) {
    this.titulaire = titulaire;
    this.#solde = depot;
    // ← à écrire : incrémenter le compteur DE CLASSE
  }
  deposer(m) { this.#solde += m; }
  getSolde() { return this.#solde; }
  static getNombreDeComptes() { return Compte.nombreDeComptes; }
}

// ── 2. Résolution de surcharge ────────────────────────────────────────────
// JavaScript n'a pas de surcharge : on la SIMULE en inspectant les types,
// ce qui rend visible ce que le compilateur Java fait à notre place.
function afficher(...args) {
  const signature = args.map((a) =>
    typeof a === "number" ? (Number.isInteger(a) ? "int" : "double") : typeof a
  ).join(", ");

  const CANDIDATES = {
    "int":            (n) => "afficher(int) -> " + n,
    "double":         (x) => "afficher(double) -> " + x.toFixed(2),
    "string":         (s) => "afficher(String) -> " + s,
    "string, int":    (s, n) => "afficher(String, int) -> " + s.repeat(n),
  };

  // ← à écrire : choisir la candidate exacte ; si aucune, tenter une
  //   conversion élargissante int -> double, comme le fait Java.
  return "aucune surcharge applicable pour (" + signature + ")";
}

// ── 3. Passage de paramètres ──────────────────────────────────────────────
function modifier(c) { c.deposer(100); }        // agit sur l'objet désigné
function remplacer(c) { c = new Compte("X", 0); } // réaffecte une locale
function incrementer(n) { n = n + 1; }           // type primitif

function echanger(a, b) {
  const t = a; a = b; b = t;    // échange les COPIES de références
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Incrémentez le compteur de classe, créez trois comptes, et vérifiez
//    qu'un compteur d'instance à la place vaudrait toujours 1.
// 2. Complétez la résolution de surcharge, y compris la conversion.
// 3. Lancez les trois cas de passage, puis echanger. Pourquoi échoue-t-il,
//    et par quoi le remplacer ?

const a = new Compte("Ana", 100);
const b = new Compte("Bo", 50);
console.log("   comptes créés :", Compte.getNombreDeComptes());

Solution

class Compte {
  static nombreDeComptes = 0;
  #solde;
  compteurPropre = 0;              // pour la comparaison du point 1

  constructor(titulaire, depot) {
    this.titulaire = titulaire;
    this.#solde = depot;
    Compte.nombreDeComptes++;      // par la CLASSE : plus clair que « nombre++ »
    this.compteurPropre++;         // une case par objet : vaudra toujours 1
  }
  deposer(m) { this.#solde += m; }
  getSolde() { return this.#solde; }
  toString() { return this.titulaire + " : " + this.getSolde(); }
  static getNombreDeComptes() { return Compte.nombreDeComptes; }
}

console.log("— 1. variable de classe contre variable d'instance —");
const a = new Compte("Ana", 100);
const b = new Compte("Bo", 50);
const c = new Compte("Cy", 20);
console.log("   Compte.nombreDeComptes = " + Compte.getNombreDeComptes() + "   (une seule case)");
console.log("   a.compteurPropre = " + a.compteurPropre +
            ", b = " + b.compteurPropre + ", c = " + c.compteurPropre +
            "   (une case chacun : toujours 1)");

console.log("");
console.log("— 2. résolution de surcharge —");
function afficher(...args) {
  const signature = args.map((x) =>
    typeof x === "number" ? (Number.isInteger(x) ? "int" : "double") : typeof x
  ).join(", ");

  const CANDIDATES = {
    "int":         (n) => "afficher(int) -> " + n,
    "double":      (x) => "afficher(double) -> " + x.toFixed(2),
    "string":      (s) => "afficher(String) -> " + s,
    "string, int": (s, n) => "afficher(String, int) -> " + s.repeat(n),
  };

  // 1) Correspondance EXACTE : toujours préférée.
  if (CANDIDATES[signature]) return CANDIDATES[signature](...args);
  // 2) À défaut, conversion élargissante int -> double, comme en Java.
  const elargie = signature.replace(/\bint\b/g, "double");
  if (CANDIDATES[elargie]) return CANDIDATES[elargie](...args) + "   (int élargi en double)";
  return "aucune surcharge applicable pour (" + signature + ")";
}

for (const appel of [[5], [5.5], ["ab"], ["ab", 3], [true]]) {
  console.log("   afficher(" + appel.map((x) => JSON.stringify(x)).join(", ") + ")".padEnd(12) +
              " -> " + afficher(...appel));
}
console.log("   Le littéral 5 est un int : la correspondance exacte l'emporte.");
console.log("   Si afficher(int) n'existait pas, 5 serait élargi en double.");
console.log("   Tout cela se décide À LA COMPILATION, sur les types DÉCLARÉS.");

console.log("");
console.log("— 3. passage de paramètres —");
function modifier(x) { x.deposer(100); }
function remplacer(x) { x = new Compte("Remplacé", 0); }
function incrementer(n) { n = n + 1; }
function echanger(x, y) { const t = x; x = y; y = t; }

console.log("   avant           : " + a.toString() + "   |   " + b.toString());
modifier(a);
console.log("   après modifier  : " + a.toString() + "   (le dépôt est VISIBLE)");
remplacer(a);
console.log("   après remplacer : " + a.toString() + "   (rien n'a changé)");

let n = 10;
incrementer(n);
console.log("   après incrementer(n) : n = " + n + "   (primitif : copie directe)");

let x = a, y = b;
echanger(x, y);
console.log("   après echanger  : x = " + x.toString() + ", y = " + y.toString());
console.log("   Rien n'a bougé : la méthode a échangé ses deux COPIES de");
console.log("   références, et l'appelant garde les siennes. On ne peut pas");
console.log("   écrire echanger en Java — il faut un conteneur, un tableau,");
console.log("   ou rendre un résultat :");
[x, y] = [y, x];             // l'appelant fait l'échange lui-même
console.log("   échange chez l'appelant : x = " + x.toString() + ", y = " + y.toString());

En travaux pratiques

Travaux pratiques 4 · 3 h

Ce qui appartient à la classe

Distinguer par l'expérience ce qui appartient à l'objet de ce qui appartient à la classe, et voir le compilateur choisir entre plusieurs méthodes de même nom.

Avant de commencer

  • Les TP 2 et 3

Énoncé

  1. Compter les instancesAjoutez à Document un compteur du nombre d'objets créés. Créez-en cinq et affichez le compteur, depuis un objet puis depuis la classe.
  2. L'erreur symétriqueTentez d'accéder à un champ d'instance depuis une méthode statique. Lisez le message et expliquez-le en une phrase avec le mot this.
  3. La constanteAjoutez une durée d'emprunt maximale, partagée et non modifiable. Testez ce que le compilateur refuse.
  4. La fabrique statiqueAjoutez une méthode statique qui construit un Document à partir d'une ligne de fichier, et qui refuse une ligne mal formée. Comparez avec un constructeur.
  5. SurchargerÉcrivez trois méthodes chercher : par titre, par titre et auteur, par année. Appelez les trois et vérifiez laquelle est choisie.
  6. L'ambiguïtéÉcrivez deux surcharges, l'une prenant un entier long, l'autre un flottant double. Appelez avec un entier ordinaire, puis avec null sur deux surcharges d'objets. Notez ce qui compile et ce qui ne compile pas. Indice : La résolution se fait à la COMPILATION, sur les types déclarés.
  7. Le piège du nombre variable d'argumentsAjoutez une surcharge à nombre variable d'arguments à côté d'une surcharge à un argument. Appelez avec un seul argument et déterminez laquelle est retenue.
  8. Au fil rougeAjoutez à Mediatheque un identifiant unique généré automatiquement pour chaque document. Vérifiez qu'il reste unique après création de mille documents.

C'est réussi quand

  • Votre compteur donne 5 sans qu'aucun objet ne le stocke
  • Vous expliquez l'erreur de la méthode statique en parlant de this
  • Vous prédisez correctement quelle surcharge est appelée dans les trois cas

Correction

Ce qui appartient à la classe
public class Document {
  private static int nombreCreés = 0;      /* UNE seule copie */
  private final String titre;              /* une copie par objet */

  public Document(String titre) {
      this.titre = titre;
      nombreCreés++;                        /* partagé */
  }
  public static int getNombreCreés() { return nombreCreés; }
}

Document.getNombreCreés()    → 5     /* la bonne façon */
unDocument.getNombreCreés()  → 5     /* compile, mais TROMPEUR */

Un champ statique existe une fois, quel que soit le nombre d'objets — y compris zéro. L'appeler sur une instance compile et laisse croire que la valeur dépend de l'objet : on écrit toujours Document.getNombreCreés(). À noter pour plus tard : ce compteur n'est pas sûr en présence de plusieurs fils d'exécution, exactement comme le compteur du TP 5 de Systèmes.

Statique et this
public static void afficher() {
  System.out.println(titre);   /* ERREUR : non-static variable titre
                                  cannot be referenced from a static
                                  context */
}

Une méthode statique n'a pas de this : elle n'est appelée sur AUCUN objet, donc la question « le titre de qui ? » n'a pas de réponse. Cette seule phrase explique l'erreur, et aussi pourquoi main est statique — au démarrage, aucun objet n'existe encore.

La fabrique statique
public static Optional<Document> depuisLigne(String ligne) {
  String[] champs = ligne.split(";");
  if (champs.length != 3) return Optional.empty();   /* refus PROPRE */
  try {
      return Optional.of(new Document(champs[0], champs[1],
                                      Integer.parseInt(champs[2])));
  } catch (NumberFormatException e) {
      return Optional.empty();
  }
}

Trois choses qu'un constructeur ne peut pas faire : porter un nom explicite, ne RIEN renvoyer en cas d'échec, et rendre un objet déjà existant plutôt qu'un neuf. Un constructeur doit soit réussir soit lever une exception — il ne peut pas renvoyer « rien ». C'est pourquoi les bibliothèques modernes exposent des fabriques nommées : Optional.of, List.of, Integer.valueOf.

La surcharge, résolue à la compilation
void afficher(long x)   { System.out.println("long"); }
void afficher(double x) { System.out.println("double"); }

afficher(5);        → « long »   /* int s'élargit d'abord en long */

void traiter(String s) { }
void traiter(Object o) { }
traiter(null);      → « String » /* le type le plus SPÉCIFIQUE gagne */

void f(Integer i) { }
void f(String s)  { }
f(null);            → ERREUR : reference to f is ambiguous

Le compilateur choisit selon les types DÉCLARÉS, à la compilation, en préférant la conversion la moins large puis la surcharge la plus spécifique. Quand deux candidats sont incomparables, il refuse. C'est la différence fondamentale avec la redéfinition du TP 6, qui se résout à l'EXÉCUTION sur le type réel — confondre les deux est le troisième point qui coince du cours.

Le nombre variable d'arguments
void chercher(String titre)       { System.out.println("un"); }
void chercher(String... criteres)  { System.out.println("plusieurs"); }

chercher("Dune");   → « un »

/* la forme à nombre variable est le DERNIER recours :
 le compilateur essaie d'abord sans conversion,
 puis avec élargissement, puis avec autoboxing,
 et seulement ensuite avec varargs */

Cet ordre en trois phases explique la plupart des surprises de surcharge. En pratique : évitez de surcharger une méthode à nombre variable d'arguments, et surtout évitez de surcharger sur des types que l'autoboxing peut relier — int et Integer dans la même famille de surcharges produit du code que personne ne sait lire.

Ce que la suite en fait

Le bloc III ouvre le cœur du cours. L'héritage du chapitre 5 permettra à une classe d'en réutiliser une autre, et posera la question qui coince : quand a-t-on le droit d'hériter ?

Le chapitre 6, lui, reprendra directement une phrase de ce chapitre-ci : la surcharge se résout à la compilation, sur le type déclaré. La redéfinition fera exactement l'inverse, et tout le polymorphisme tient dans cette opposition.

À retenir

Flashcards · 5 cartes

Que signifie static, et pourquoi le main l'est-il ?
Appartenir à la CLASSE et non à un objet : une seule case, créée au chargement de la classe, quel que soit le nombre d'instances. Le main est static parce que la machine virtuelle doit l'appeler AVANT qu'aucun objet n'existe — elle n'a aucune instance sur laquelle l'invoquer. Conséquence : une méthode statique n'a pas de this et ne peut pas lire un attribut d'instance, d'où l'erreur « non-static variable cannot be referenced from a static context » du premier programme.
Quels sont les trois emplois légitimes de static, et le contre-emploi ?
1) Les CONSTANTES, en static final et en majuscules. 2) Les COMPTEURS et registres partagés. 3) Les MÉTHODES UTILITAIRES sans état (Math.sqrt, Integer.parseInt), qui ne dépendent d'aucun objet. Contre-emploi : rendre statique PAR COMMODITÉ, pour éviter de créer un objet — on obtient un état global modifiable de partout, c'est-à-dire le défaut du procédural sous un autre nom.
Que garantit final sur une référence, et que ne garantit-il pas ?
Il interdit de RÉAFFECTER la référence, pas de MODIFIER l'objet qu'elle désigne : sur une liste déclarée static final, l'affectation d'une nouvelle liste est refusée mais add() est autorisé. C'est la même distinction qu'entre la référence et l'objet, et c'est pourquoi final ne suffit pas à garantir l'immuabilité.
Comment la surcharge est-elle résolue, et quelles règles la gouvernent ?
À LA COMPILATION, d'après les TYPES DÉCLARÉS des arguments : le compilateur choisit la méthode la plus spécifique, en préférant une correspondance exacte à une conversion élargissante. Le TYPE DE RETOUR ne fait pas partie de la signature — deux méthodes n'en différant que par là ne compilent pas. Retenir la phrase « à la compilation, sur le type déclaré » : le chapitre 6 présentera la redéfinition, qui fait exactement l'inverse.
Comment Java passe-t-il les paramètres, et quelle en est la conséquence ?
TOUJOURS PAR VALEUR — il n'existe aucun passage par référence. Ce qui trompe : la valeur d'une variable objet EST une référence, donc la copier donne un second nom pour le même objet. Résultat : modifier l'objet désigné (c.deposer(100)) est visible par l'appelant ; réaffecter le paramètre (c = new Compte(0)) ne change qu'une variable locale. C'est le chapitre 7 du cours de programmation, sans étoile ni esperluette. Conséquence : on ne peut pas écrire echanger(a, b) en Java. Les types primitifs, eux, sont copiés directement.