C1 — Du procédural à l'objetDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Licence 2 · Programmation orientée objet

Cours 1Du procédural à l'objet

Comprendre ce qui casse dans un programme procédural qui grossit, puis écrire sa première classe et savoir ce qu'un objet contient réellement.

2 chapitres · 8 h de travail estimé

  1. 1. Pourquoi l'objet3 h
  2. 2. Classes et objets5 h

Chapitre 1 · 3 h

Pourquoi l'objet

Ce qui casse dans un programme procédural qui grossit ; regrouper données et traitements, responsabilité et réutilisation ; panorama des langages objet.

Écrivons une gestion de bibliothèque avec les outils d'Algorithmique 1. Trois tableaux :

String[]  titres      = {"Dune", "Fondation", "Solaris"};String[]  auteurs     = {"Herbert", "Asimov", "Lem"};boolean[] disponibles = {true, false, true};

Le livre numéro 1, c'est titres[1], auteurs[1] et disponibles[1]. Rien ne le dit, rien ne le garantit : c'est une convention, tenue à la main, dans chaque fonction qui touche à ces tableaux.

Retirons maintenant « Fondation ». Il faut décaler les trois tableaux. Si une seule des trois opérations est oubliée — ou faite dans le mauvais ordre, ou faite deux fois — les titres, les auteurs et les disponibilités cessent de correspondre. Le programme continue, sans la moindre erreur, et affiche que « Solaris » est d'Asimov.

Ce chapitre part de ce genre de panne. L'objet n'est pas une façon plus élégante d'écrire la même chose : c'est une réponse à un problème précis, et il vaut mieux avoir vu le problème.

Ce qui casse quand un programme grossit

Quatre symptômes reviennent, et ils sont liés.

La cohérence n'est garantie par rien. Les tableaux parallèles en sont le cas d'école, mais le problème est général : dès que plusieurs variables doivent rester d'accord entre elles, et que rien dans le langage ne l'impose, la moindre distraction les désynchronise. Et le symptôme apparaît loin de la faute.

Les fonctions enflent. Une fonction qui doit connaître le titre, l'auteur, la disponibilité, l'emprunteur et la date prend cinq paramètres, puis huit. L'ordre des arguments devient une source d'erreurs — deux String consécutifs s'intervertissent sans que le compilateur bronche.

Tout le monde peut tout modifier. Rien n'empêche une fonction d'écrire disponibles[i] = true sans vérifier que le livre a bien été rendu. Il n'existe aucun endroit où faire respecter une règle : la règle est éparpillée dans tous les appelants, ou nulle part.

Un changement de structure se propage partout. Ajouter l'ISBN, c'est un quatrième tableau, et toutes les fonctions qui insèrent ou suppriment sont à relire. Sur un programme de mille lignes, c'est une journée ; sur cinquante mille, c'est un refus.

Le point commun de ces quatre symptômes : les données et les traitements qui les concernent sont séparés. Les données sont dans des tableaux globaux, les traitements dans des fonctions qui les prennent en paramètre, et rien ne les relie sinon la discipline du programmeur.

L'idée : regrouper

La programmation orientée objet propose une réponse tenant en une phrase : mettre ensemble les données et les opérations qui les concernent.

class Livre {    titre    auteur    disponible     emprunter()    rendre()    estDisponible()}

Un Livre n'est plus trois cases dans trois tableaux, c'est une chose. La désynchronisation disparaît par construction : il n'y a plus trois tableaux à maintenir d'accord, il y a une liste de livres, chacun portant ses propres données. Supprimer un livre, c'est le retirer d'une liste, et il emporte tout ce qui le concerne.

Deux conséquences suivent immédiatement.

Les fonctions maigrissent. emprunter(livre) devient livre.emprunter() : le premier paramètre n'est plus un paramètre, c'est l'objet lui-même. Une méthode qui prenait cinq arguments n'en prend souvent plus aucun.

Il existe enfin un endroit où faire respecter une règle. « On n'emprunte pas un livre déjà emprunté » s'écrit une fois, dans emprunter(). Tout le code qui emprunte passe par là. Le chapitre 3 en fera le principe central du semestre.

Responsabilité et réutilisation

Deux notions se dégagent, et elles guideront tout le cours.

La responsabilité est la question à se poser devant chaque classe : de quoi cet objet est-il responsable, et de quoi ne l'est-il pas ? Un Livre est responsable de son état d'emprunt. Il n'est pas responsable de l'affichage à l'écran, ni de l'enregistrement dans un fichier, ni de la politique de retard de l'établissement. Une classe qui fait tout est une fonction géante déguisée, et l'on n'aura rien gagné.

Le critère opérationnel, qu'on emploiera tout le semestre : une classe doit pouvoir se décrire en une phrase sans « et ». « Un livre connaît son titre, son auteur, et sait s'il est empruntable » va bien. « Un livre connaît son titre et gère l'affichage du catalogue et sauvegarde le fichier » signale trois classes.

La réutilisation vient ensuite, presque gratuitement. Une classe bien délimitée s'emploie dans un autre programme sans rien emporter avec elle. Ce n'est pas la promesse de tout réutiliser — cette promesse des années 1990 n'a jamais été tenue — mais un effet secondaire réel d'une bonne délimitation.

Quiz · 1 question

Trois tableaux parallèles titres, auteurs et disponibles décrivent des livres. Quel est le défaut principal de cette organisation ?

  • Elle consomme trois fois plus de mémoire qu'un tableau uniqueconsommation mémoire
  • La correspondance entre les trois tableaux n'est garantie par RIEN : c'est une convention tenue à la main dans chaque fonction, et une suppression mal faite désynchronise les données sans provoquer la moindre erreurcohérence non garantie
  • Les tableaux ne peuvent pas contenir de types différents, ce qui interdit d'ajouter une datetypes hétérogènes

Réponse : La mémoire n'est pas en cause — trois tableaux de n éléments occupent autant qu'un tableau de n objets à trois champs, à peu de chose près. Le défaut est que l'indice i est une CONVENTION : rien dans le langage n'exprime que titres[i] et auteurs[i] parlent du même livre, donc rien ne le vérifie. Une suppression qui décale deux tableaux sur trois produit un programme qui continue de fonctionner, sans erreur, en attribuant Solaris à Asimov — et le symptôme apparaît loin de la faute. Regrouper les trois champs dans un objet fait disparaître le problème PAR CONSTRUCTION : il n'y a plus de correspondance à maintenir, il y a une chose qui porte ses données.

Panorama

L'objet n'est pas récent, et connaître ses variantes évite de croire que Java est la programmation objet.

Simula 67 invente les classes, pour écrire des simulations — d'où son nom, et d'où le vocabulaire : on simulait des guichets, des files, des clients, c'est-à-dire des choses.

Smalltalk (années 1970) pousse l'idée jusqu'au bout : tout y est objet, y compris les nombres et les classes elles-mêmes, et le seul mécanisme est l'envoi de message.

C++ (1983) greffe l'objet sur le C, en gardant la compatibilité et la performance — d'où l'héritage multiple, et une complexité considérable.

Java (1995) simplifie délibérément : pas d'héritage multiple de classes, ramasse-miettes, machine virtuelle. C'est le langage de ce cours, parce que ses notions y sont explicitesprivate, abstract, interface sont des mots-clés, là où Python s'en remet à des conventions.

Python et JavaScript relèvent d'autres traditions : typage dynamique pour le premier, objets à prototypes pour le second, où un objet hérite directement d'un autre objet sans qu'aucune classe n'intervienne.

Deux lignes de partage traversent ce paysage, et l'on y reviendra. Le typage — statique comme en Java, où le compilateur vérifie les types, ou dynamique comme en Python, où l'on vérifie à l'exécution ; c'est ce qui fera tout l'intérêt du chapitre 6. Et l'héritage multiple, autorisé en C++ et en Python, interdit en Java pour les classes — le chapitre 6 expliquera ce qui le remplace.

Ce que l'objet ne résout pas

Une mise en garde, pour ne pas transformer ce cours en apologie.

L'objet n'est pas toujours le bon outil. Un programme de calcul numérique, un filtre de texte de trente lignes, un pilote de périphérique ne gagnent rien à être découpés en classes — et le C du cours précédent reste le bon choix pour le dernier.

L'objet ne rend pas un mauvais découpage bon. Une hiérarchie de classes mal pensée est plus difficile à corriger qu'un mauvais découpage en fonctions, parce que les dépendances y sont plus profondes. C'est d'ailleurs le sujet du point qui coince du chapitre 5.

Et l'objet coûte : plus de fichiers, plus de vocabulaire, une indirection à l'exécution. Sur un programme de cent lignes, ce coût n'est pas amorti. Le seuil se situe à peu près là où l'introduction de ce chapitre commence à faire mal — quand plusieurs personnes travaillent sur le même code, et quand celui-ci va vivre plusieurs années.

Quiz · 1 question

Une classe Livre gère son état d'emprunt, son affichage à l'écran et son enregistrement dans un fichier. Que dit le critère de responsabilité ?

  • C'est une bonne conception : tout ce qui concerne un livre est au même endroit, ce qui est précisément le but de l'objetbonne conception
  • La classe se décrit avec des « et » : elle porte trois responsabilités indépendantes, qui changeront pour des raisons différentes — il y a là trois classestrois responsabilités
  • Peu importe, tant que les attributs sont privés : c'est l'encapsulation qui compte, pas le découpageseule l'encapsulation compte

Réponse : Regrouper données et traitements ne signifie pas tout regrouper : le critère est la RESPONSABILITÉ, pas le sujet. Un livre est responsable de son état d'emprunt — cela, personne d'autre ne peut le savoir à sa place. L'affichage relève de l'interface utilisateur, l'enregistrement du stockage, et ces trois choses changent pour des raisons indépendantes : passer d'une console à une interface graphique ne devrait pas toucher aux règles d'emprunt. Le critère opérationnel est la description en UNE PHRASE SANS « ET » : dès qu'il en faut un, on tient probablement deux classes. Et l'encapsulation du chapitre 3 ne sauve rien ici — on peut parfaitement encapsuler une classe qui fait trois métiers, elle restera difficile à faire évoluer.

À vous

L'exercice part du code de l'introduction et le fait échouer, avant de le réparer.

D'abord la version à tableaux parallèles : vous écrivez la suppression d'un livre, et le squelette contient volontairement l'oubli d'un des trois décalages. Un vérificateur de cohérence vous dira ce que le programme, lui, ne dirait jamais.

Ensuite la version objet : une liste de livres, chacun portant ses champs. Vous réécrirez la même suppression et constaterez qu'il n'y a plus rien à synchroniser — non pas parce que vous avez été plus attentif, mais parce que le problème n'existe plus.

La dernière partie compte les paramètres : la même opération écrite en procédural et en objet, côte à côte.

Exercice de code

Faites désynchroniser des tableaux parallèles, puis réécrivez la même bibliothèque en objets.

Point de départ

// ── 1. Version procédurale : trois tableaux parallèles ────────────────────
let titres      = ["Dune", "Fondation", "Solaris", "Ubik"];
let auteurs     = ["Herbert", "Asimov", "Lem", "Dick"];
let disponibles = [true, false, true, true];

// La correspondance entre les trois tableaux n'est écrite NULLE PART : on la
// vérifie ici avec une copie de référence, ce qu'aucun programme réel n'a.
const REFERENCE = { Dune: "Herbert", Fondation: "Asimov", Solaris: "Lem", Ubik: "Dick" };
function verifierCoherence(etiquette) {
  const fautes = [];
  for (let i = 0; i < titres.length; i++) {
    if (REFERENCE[titres[i]] !== auteurs[i]) {
      fautes.push(titres[i] + " attribué à " + auteurs[i] + " au lieu de " + REFERENCE[titres[i]]);
    }
  }
  const tailles = [titres.length, auteurs.length, disponibles.length];
  console.log("   " + etiquette.padEnd(24) + "tailles [" + tailles.join(",") + "]" +
    (fautes.length ? "   INCOHÉRENT : " + fautes.join(" ; ") : "   cohérent"));
}

function supprimerProcedural(i) {
  titres.splice(i, 1);
  auteurs.splice(i, 1);
  // ← il manque le troisième décalage. Le programme ne dira rien.
}

// ── 2. Version objet ──────────────────────────────────────────────────────
function creerLivre(titre, auteur, disponible) {
  return {
    titre, auteur, disponible,
    // La règle vit ICI, en un seul endroit, et tout le monde y passe.
    emprunter() { return false; },   // ← à écrire
    rendre() { this.disponible = true; },
    toString() { return this.titre + " (" + this.auteur + ")" +
                        (this.disponible ? "" : " — emprunté"); },
  };
}

let bibliotheque = [
  creerLivre("Dune", "Herbert", true),
  creerLivre("Fondation", "Asimov", false),
  creerLivre("Solaris", "Lem", true),
  creerLivre("Ubik", "Dick", true),
];

function supprimerObjet(i) {
  bibliotheque.splice(i, 1);   // et c'est tout : le livre emporte ses données
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Lancez la suppression procédurale et lisez le rapport de cohérence.
// 2. Réparez supprimerProcedural, puis demandez-vous ce qui se passera le
//    jour où l'on ajoutera un quatrième tableau (les ISBN).
// 3. Écrivez emprunter() : il doit REFUSER si le livre est déjà emprunté.
//    Combien d'endroits faut-il modifier pour changer cette règle ?

verifierCoherence("au départ");
supprimerProcedural(1);
verifierCoherence("après suppression");

Solution

let titres      = ["Dune", "Fondation", "Solaris", "Ubik"];
let auteurs     = ["Herbert", "Asimov", "Lem", "Dick"];
let disponibles = [true, false, true, true];

const REFERENCE = { Dune: "Herbert", Fondation: "Asimov", Solaris: "Lem", Ubik: "Dick" };
function verifierCoherence(etiquette) {
  const fautes = [];
  for (let i = 0; i < titres.length; i++) {
    if (REFERENCE[titres[i]] !== auteurs[i]) {
      fautes.push(titres[i] + " attribué à " + auteurs[i] + " au lieu de " + REFERENCE[titres[i]]);
    }
  }
  const tailles = [titres.length, auteurs.length, disponibles.length];
  console.log("   " + etiquette.padEnd(26) + "tailles [" + tailles.join(",") + "]" +
    (fautes.length ? "   INCOHÉRENT : " + fautes.join(" ; ") : "   cohérent"));
}

function supprimerFautif(i) { titres.splice(i, 1); auteurs.splice(i, 1); }
function supprimerCorrige(i) {
  titres.splice(i, 1); auteurs.splice(i, 1); disponibles.splice(i, 1);
}

console.log("— 1. version procédurale —");
verifierCoherence("au départ");
supprimerFautif(1);
verifierCoherence("après suppression fautive");
console.log("   Les titres et les auteurs restent d'accord — la faute porte sur le");
console.log("   troisième tableau, donc « Solaris » hérite de la disponibilité de");
console.log("   « Fondation ». Aucune erreur, aucun avertissement, et un livre");
console.log("   emprunté qui se déclare disponible.");

titres = ["Dune", "Fondation", "Solaris", "Ubik"];
auteurs = ["Herbert", "Asimov", "Lem", "Dick"];
disponibles = [true, false, true, true];
supprimerCorrige(1);
verifierCoherence("après suppression correcte");
console.log("   Réparé — jusqu'au jour où l'on ajoutera les ISBN : il faudra");
console.log("   retrouver TOUTES les fonctions qui insèrent ou suppriment.");

console.log("");
console.log("— 2. version objet —");
function creerLivre(titre, auteur, disponible) {
  return {
    titre, auteur, disponible,
    // La règle « on n'emprunte pas ce qui est déjà emprunté » est écrite UNE
    // fois. Tout le code qui emprunte passe forcément par ici.
    emprunter() {
      if (!this.disponible) return false;
      this.disponible = false;
      return true;
    },
    rendre() { this.disponible = true; },
    toString() {
      return this.titre + " (" + this.auteur + ")" + (this.disponible ? "" : " — emprunté");
    },
  };
}

let bibliotheque = [
  creerLivre("Dune", "Herbert", true),
  creerLivre("Fondation", "Asimov", false),
  creerLivre("Solaris", "Lem", true),
  creerLivre("Ubik", "Dick", true),
];

console.log("   avant : " + bibliotheque.map((l) => l.toString()).join(" | "));
bibliotheque.splice(1, 1);        // une seule opération, rien à synchroniser
console.log("   après : " + bibliotheque.map((l) => l.toString()).join(" | "));
console.log("   Il n'y a plus de correspondance à maintenir : le livre a emporté");
console.log("   ses trois champs avec lui. Ajouter l'ISBN ne toucherait AUCUNE");
console.log("   fonction de suppression.");

console.log("");
console.log("— 3. la règle vit dans la méthode —");
const dune = bibliotheque[0];
console.log("   premier emprunt : " + dune.emprunter() + "  -> " + dune.toString());
console.log("   second emprunt  : " + dune.emprunter() + "  -> refusé, et personne");
console.log("                     n'a eu à y penser à l'extérieur");

console.log("");
console.log("— 4. le même service, deux écritures —");
console.log("   procédural : emprunter(titres, auteurs, disponibles, i)   4 paramètres");
console.log("   objet      : livre.emprunter()                            0 paramètre");
console.log("   Et pour changer la règle : un seul endroit contre autant d'appelants.");

En travaux pratiques

Travaux pratiques 1 · 2 h

Partir du code procédural, et le faire craquer

Écrire d'abord la version sans objets, la faire évoluer jusqu'à ce qu'elle devienne pénible, puis mesurer précisément ce que l'objet répare.

Avant de commencer

  • Un JDK et de quoi compiler en ligne de commande
  • Une expérience de la programmation impérative

Énoncé

  1. La version procéduraleÉcrivez une gestion de médiathèque avec des tableaux parallèles : un pour les titres, un pour les auteurs, un pour les années, un pour la disponibilité. Ajoutez, affichez, empruntez.
  2. Le premier accrocAjoutez un champ : le nombre d'exemplaires. Comptez les endroits du code que vous devez modifier.
  3. Le second accrocÉcrivez une fonction qui trie les documents par année. Faites-la tourner et vérifiez les auteurs après le tri. Indice : Si vous ne déplacez pas TOUS les tableaux exactement de la même façon, les données se désolidarisent.
  4. Le troisième accrocAjoutez une règle : un document indisponible ne peut pas être emprunté. Comptez combien d'endroits du programme peuvent violer cette règle.
  5. RegrouperCréez une classe Document rassemblant les quatre champs, et remplacez les tableaux parallèles par un tableau de documents. Refaites les étapes 2 et 3.
  6. ProtégerRendez la disponibilité modifiable uniquement par des méthodes emprunter et rendre. Recomptez les endroits qui peuvent violer la règle.
  7. ComparerDressez le tableau : nombre de lignes, nombre d'endroits à modifier pour ajouter un champ, nombre d'endroits pouvant casser l'invariant. Avant et après.

C'est réussi quand

  • Vous constatez, sur votre propre code, le désalignement des tableaux après tri
  • Ajouter un champ ne touche qu'un seul fichier après refonte
  • Un seul endroit du programme peut désormais modifier la disponibilité

Correction

Les tableaux parallèles, et ce qui les casseMediatheque.java
String[] titres    = new String[100];
String[] auteurs   = new String[100];
int[]    annees    = new int[100];
boolean[] dispo    = new boolean[100];

/* le tri qui casse tout : on trie UN tableau */
Arrays.sort(annees);
/* titres, auteurs et dispo n'ont pas bougé :
 « 1984 » est maintenant associé à l'auteur de « Dune » */

Rien dans le langage ne dit que ces quatre tableaux sont liés : la relation n'existe que dans la tête du programmeur, donc elle se perd. C'est le premier des trois problèmes que l'objet résout — non pas par élégance, mais parce que la cohérence devient impossible à rompre par accident.

L'invariant sans protection
/* la règle : un document indisponible ne s'emprunte pas */

/* mais N'IMPORTE OÙ dans le programme, on peut écrire : */
dispo[i] = false;    /* dans emprunter()   */
dispo[i] = false;    /* dans inventaire()  — par erreur */
dispo[i] = true;     /* dans importer()    — sans vérifier */

11 endroits touchent dispo, 11 endroits peuvent casser la règle

Une règle métier écrite dans un commentaire n'est pas une règle : c'est un espoir. Tant que le champ est accessible partout, la vérifier partout est le seul moyen — et il suffit d'un oubli. C'est le second problème : rien ne PROTÈGE l'invariant.

La classeDocument.java
public class Document {
  private String titre;
  private String auteur;
  private int    annee;
  private boolean disponible = true;

  public Document(String titre, String auteur, int annee) {
      this.titre = titre;
      this.auteur = auteur;
      this.annee = annee;
  }

  public boolean emprunter() {
      if (!disponible) return false;    /* la règle, à UN seul endroit */
      disponible = false;
      return true;
  }
  public void rendre() { disponible = true; }
  public boolean estDisponible() { return disponible; }
}

Les données et les opérations qui les gouvernent sont au même endroit, et l'extérieur ne peut plus toucher disponible directement. La règle métier n'est plus un commentaire : elle est le seul chemin possible. Trier un tableau de Document déplace chaque objet entier — le désalignement de l'étape 3 devient inexprimable.

Le tableau comparatif
                                    procédural   objet
lignes                                  180        210
fichiers à modifier pour un champ         4          1
endroits pouvant casser l'invariant      11          1
désalignement possible au tri            oui        non

L'objet coûte trente lignes de plus. Il rend en échange trois choses qui ne se mesurent qu'à la maintenance : un seul endroit à modifier, un seul endroit à surveiller, et une classe d'erreurs qui n'existe plus. Sur un programme de deux cents lignes le gain est discutable ; sur dix mille il n'est plus discuté.

Ce que l'objet n'apporte PAS

Il ne rend pas le programme plus rapide — il ajoute souvent une indirection. Il ne rend pas le code plus court. Et il ne dispense pas de savoir programmer : une classe avec quinze champs publics et aucune méthode est un tableau parallèle déguisé. L'objet n'apporte que ce que vous en faites : encapsuler un invariant, nommer un concept du domaine, et permettre de remplacer une implémentation sans toucher aux appelants.

Ce que la suite en fait

Le chapitre 2 donne la syntaxe de ce que ce chapitre a esquissé : ce qu'est réellement une classe, ce qu'est un objet, ce que new fait en mémoire — et la référence this, qui est le premier point qui coince du semestre.

Le chapitre 3 reprendra la troisième panne de l'introduction, celle où n'importe qui peut écrire n'importe quoi dans les données : c'est l'encapsulation, et c'est ce qui transforme le regroupement de ce chapitre en véritable garantie.

À retenir

Flashcards · 5 cartes

Quels sont les quatre symptômes d'un programme procédural qui grossit ?
1) La COHÉRENCE n'est garantie par rien : les tableaux parallèles se désynchronisent sans la moindre erreur, et le symptôme apparaît loin de la faute. 2) Les FONCTIONS ENFLENT : cinq puis huit paramètres, dont l'ordre devient une source d'erreurs. 3) TOUT LE MONDE PEUT TOUT MODIFIER : aucune règle ne peut être imposée en un seul endroit. 4) UN CHANGEMENT DE STRUCTURE se propage à tout le code. Cause commune : données et traitements sont séparés.
Quelle est l'idée centrale de l'objet, et ses deux conséquences immédiates ?
Mettre ENSEMBLE les données et les opérations qui les concernent. Conséquences : les fonctions maigrissent — emprunter(livre) devient livre.emprunter(), le premier paramètre étant l'objet lui-même ; et il existe enfin UN ENDROIT où faire respecter une règle — « on n'emprunte pas un livre déjà emprunté » s'écrit une fois, dans la méthode, et tout le code passe par là.
Quel est le critère opérationnel de la responsabilité d'une classe ?
Une classe doit pouvoir se décrire EN UNE PHRASE SANS « ET ». « Un livre connaît son titre, son auteur, et sait s'il est empruntable » va bien. « Un livre connaît son titre ET gère l'affichage ET sauvegarde le fichier » signale trois classes : trois responsabilités indépendantes, qui changeront pour des raisons différentes. La réutilisation vient ensuite, comme effet secondaire d'une bonne délimitation.
Que faut-il retenir du panorama des langages objet ?
Simula 67 invente les classes pour la simulation, d'où le vocabulaire. Smalltalk pousse l'idée à son terme : tout est objet. C++ greffe l'objet sur le C, avec héritage multiple et complexité. Java simplifie délibérément et rend les notions EXPLICITES (private, abstract, interface sont des mots-clés), d'où son choix ici. Python et JavaScript relèvent d'autres traditions — typage dynamique, objets à prototypes. Deux lignes de partage : le typage statique ou dynamique, et l'héritage multiple, interdit en Java pour les classes.
Que l'objet ne résout-il pas ?
Il n'est pas toujours le bon outil : calcul numérique, filtre de trente lignes, pilote de périphérique n'y gagnent rien. Il ne rend pas un mauvais découpage bon — une hiérarchie mal pensée est plus difficile à corriger qu'un mauvais découpage en fonctions, les dépendances y étant plus profondes. Et il COÛTE : plus de fichiers, plus de vocabulaire, une indirection. Le seuil de rentabilité est là où plusieurs personnes travaillent sur un code qui va vivre des années.

Chapitre 2 · 5 h

Classes et objets

La classe comme moule et l'objet comme instance ; attributs, méthodes, constructeur, référence this, création par new, cycle de vie d'un objet.

Compte a = new Compte(100);Compte b = new Compte(50);a.deposer(30);System.out.println(b.getSolde());     /* 50, pas 80 */

Cette évidence n'en est pas une au premier contact. La question « mais alors, il y a deux soldes ? » revient chaque année, et elle est légitime : le code de deposer est écrit une seule fois, et l'on peine à voir comment une seule ligne peut modifier une case sans toucher à l'autre.

C'est le premier point qui coince du semestre. Il se règle d'une seule façon : en regardant plusieurs objets côte à côte, pas à pas.

Le moule et la gaufre

Une classe décrit ce qu'un objet contiendra et ce qu'il saura faire. Elle n'existe qu'en un exemplaire, et elle ne contient aucune donnée d'utilisateur — c'est un moule.

Un objet, ou instance, est une chose fabriquée avec ce moule. Il possède ses propres attributs, distincts de ceux de tous les autres objets de la même classe.

L'analogie tient si l'on ne l'étire pas : le moule à gaufres ne contient pas de pâte, et deux gaufres issues du même moule ont la même forme et des garnitures différentes.

public class Compte {    private double solde;              // attribut : une case PAR objet    private String titulaire;     public Compte(String titulaire, double depotInitial) {   // constructeur        this.titulaire = titulaire;        this.solde = depotInitial;    }     public void deposer(double montant) {                    // méthode        this.solde += montant;    }     public double getSolde() {        return this.solde;    }}

Trois vocabulaires à fixer. Les attributs — ou champs — sont les variables déclarées dans la classe : chaque objet en reçoit un jeu complet. Les méthodes sont les fonctions déclarées dans la classe : elles existent une seule fois en mémoire, et reçoivent l'objet sur lequel elles travaillent. Le constructeur est la méthode particulière appelée à la création.

Une différence avec le C mérite d'être signalée tout de suite : en Java, les attributs sont initialisés automatiquement — 0 pour les nombres, false pour les booléens, null pour les références. Ce n'est pas le cas des variables locales, qui restent non initialisées comme au chapitre 2 du cours de programmation, et le compilateur refuse de les lire.

Le constructeur

public Compte(String titulaire, double depotInitial) { ... }

Trois règles, et la troisième surprend.

Il porte exactement le nom de la classe, majuscule comprise. Il n'a aucun type de retour, pas même void — écrire void Compte(...) en fait une méthode ordinaire nommée Compte, qui compile et n'est jamais appelée par new. C'est une erreur classique, et elle est silencieuse.

Il sert à établir l'invariant de l'objet : à la sortie du constructeur, l'objet doit être dans un état valide. Un compte sans titulaire ou avec un solde négatif ne doit pas pouvoir exister — et c'est le constructeur qui l'empêche, comme le chapitre 3 le formalisera.

Enfin, une règle de Java qu'il vaut mieux connaître : si l'on n'écrit aucun constructeur, le compilateur en fournit un par défaut, sans paramètre. Dès qu'on en écrit un seul, ce constructeur par défaut disparaît — et new Compte() cesse de compiler. Il faut alors le réécrire si on en veut un.

Ce que fait new

L'instruction Compte a = new Compte("Ana", 100); fait quatre choses, dans cet ordre.

1. ALLOUER   un objet sur le TAS, avec une case par attribut2. INITIALISER  ces cases aux valeurs par défaut (0, false, null)3. EXÉCUTER  le constructeur, qui les remplit vraiment4. RENDRE    une RÉFÉRENCE vers cet objet, qui est rangée dans a

Le mot important est le dernier. a ne contient pas l'objet, mais une référence vers lui — c'est-à-dire, en pratique, une adresse au sens du chapitre 7 du cours de programmation. L'objet vit sur le tas, la variable a vit sur la pile.

D'où une conséquence que le chapitre 4 exploitera :

Compte a = new Compte("Ana", 100);Compte b = a;                          // COPIE LA RÉFÉRENCE, pas l'objetb.deposer(50);a.getSolde();                          // 150 : c'est le MÊME objet

Il n'y a ici qu'un seul compte, désigné par deux noms. C'est exactement le passage par adresse du cours de programmation, et c'est ce qui distingue Java du C sur ce point : en Java, tout objet est manipulé par référence, sans qu'aucune étoile ne l'écrive.

this

Animation · 8 étapes

Deux objets, deux soldes — mais un seul compteur

  1. Avant toute créationLa classe est un MOULE : elle décrit ce qu'un compte contiendra, sans qu'aucun compte existe. Une seule chose existe déjà, la variable de classe `nombre` — elle appartient à la classe, pas à un objet, et il n'y en aura jamais qu'un exemplaire.
  2. new alloue le premier objet`new` fait deux choses : il réserve la mémoire d'un objet — donc une case `solde` bien à lui — puis il appelle le constructeur. Pendant cet appel, `this` désigne cet objet-là, et rien d'autre.
  3. this.solde = depot`solde` sans préfixe désignerait ici le paramètre le plus proche ; `this.solde` lève l'ambiguïté et désigne l'attribut de l'objet en cours de construction. C'est le premier usage de `this`, et le plus fréquent.
  4. nombre++ — sans thisAucun `this` ici, et ce n'est pas un oubli : `nombre` n'appartient à aucun objet. On aurait pu écrire `Compte.nombre++`, ce qui est plus clair et devrait être l'habitude.
  5. Second new : un autre objetUne nouvelle case `solde` est allouée, indépendante de la première. `this` désigne maintenant b — même code, autre objet, et c'est exactement ce que le mot `instance` veut dire.
  6. La MÊME ligne, un AUTRE soldeLa ligne 5 s'exécute pour la seconde fois et écrit 50 — sans toucher au 100 de a. Deux instances, deux jeux d'attributs : c'est le point que la lecture du code seul ne montre pas.
  7. Le compteur, lui, est partagé`nombre` passe à 2. Il n'y a pas « le nombre de a » et « le nombre de b » : il y a UN compteur, que les deux constructeurs ont incrémenté. C'est toute la différence entre variable de classe et variable d'instance.
  8. Modifier un objet ne touche pas l'autrea.solde change, b.solde ne bouge pas, et nombre non plus. Trois cases distinctes, trois durées de vie distinctes — et le constructeur est terminé, donc `this` ne désigne plus rien.

this désigne l'objet sur lequel la méthode s'exécute. Il n'a de sens qu'à l'intérieur d'une méthode d'instance, et il change à chaque appel — c'est ce que l'animation montre.

Trois usages, du plus fréquent au plus rare.

Lever une ambiguïté. Quand un paramètre porte le même nom qu'un attribut, solde désigne le paramètre — le plus proche — et this.solde l'attribut. C'est le cas du constructeur ci-dessus, et c'est l'usage qu'on rencontre partout. Certains l'évitent en nommant les paramètres différemment ; l'écrasante majorité des codes Java fait l'inverse et emploie this.

Se passer soi-même à une autre méthode : journal.enregistrer(this).

Enchaîner les constructeurs : this(titulaire, 0) appelle un autre constructeur de la même classe, ce qui évite de dupliquer l'initialisation. L'appel doit être la première instruction.

Le contre-emploi à connaître : dans une méthode statique, this n'existe pas — il n'y a aucun objet courant. C'est le sujet du chapitre 4, et l'erreur non-static variable this cannot be referenced from a static context est celle que tout étudiant rencontre en écrivant son premier main.

Quiz · 1 question

Après Compte a = new Compte(100); Compte b = a; b.deposer(50); que vaut a.getSolde() ?

  • 100 : a et b sont deux variables distinctes, donc deux comptes distincts100
  • 150 : b = a copie la RÉFÉRENCE, pas l'objet — il n'y a qu'un seul compte, désigné par deux noms150
  • Une erreur : on ne peut pas affecter un objet à une autre variable sans le clonererreur

Réponse : new fait quatre choses : allouer l'objet sur le TAS, initialiser ses cases aux valeurs par défaut, exécuter le constructeur, et rendre une RÉFÉRENCE. C'est cette référence — une adresse, au sens du cours de programmation — qui est rangée dans a. L'affectation b = a recopie donc l'adresse, pas l'objet : les deux variables désignent la même case sur le tas, et un dépôt fait par l'une est visible par l'autre. Pour obtenir deux comptes indépendants, il faut deux new. C'est la différence entre l'IDENTITÉ (est-ce le même objet ?) et l'ÉGALITÉ (ont-ils le même contenu ?), que le chapitre 6 traitera avec equals — et c'est aussi ce qui rend le passage de paramètres subtil au chapitre 4.

Cycle de vie

Un objet naît avec new, vit tant qu'une référence le désigne, et meurt quand plus aucune ne le désigne.

Compte a = new Compte("Ana", 100);a = new Compte("Bo", 50);              // le premier compte n'est plus atteignable

Le premier objet existe toujours en mémoire, mais plus personne ne peut l'atteindre : c'est exactement la définition de la fuite du chapitre 8 du cours de programmation. La différence est que Java dispose d'un ramasse-miettes (garbage collector), qui repère périodiquement les objets inatteignables et récupère leur mémoire.

Trois conséquences pratiques, et la dernière est une mise en garde.

Il n'y a pas de free, ni de destructeur à écrire. La question « qui libère ? », qui occupait tout le chapitre 8 du C, ne se pose plus.

Le prix est un coût à l'exécution : le ramasse-miettes s'exécute quand il le décide, et peut introduire de brèves pauses. C'est acceptable presque partout, et rédhibitoire en temps réel dur — au sens du chapitre 1 du cours de systèmes.

Enfin, le ramasse-miettes ne supprime pas les fuites, il supprime une de leurs causes. Un objet encore référencé par une liste qu'on a oublié de vider ne sera jamais récupéré. La fuite en Java n'est pas « j'ai oublié de libérer » mais « je garde une référence dont je n'ai plus besoin » — plus subtile, et plus difficile à voir.

Quiz · 1 question

Un étudiant écrit public void Compte(String nom) puis appelle new Compte(« Ana »), et le code ne compile pas. Pourquoi ?

  • Il manque le mot-clé static devant la méthodestatic manquant
  • Un constructeur n'a AUCUN type de retour, pas même void : avec void, c'est une méthode ordinaire nommée Compte, jamais appelée par new — et comme un constructeur a été « écrit », celui par défaut n'existe plusvoid en trop
  • Le nom du paramètre ne doit pas être identique à celui de l'attributconflit de noms

Réponse : C'est l'erreur silencieuse la plus classique du chapitre. Un constructeur se reconnaît à deux signes : il porte exactement le nom de la classe, et il n'a AUCUN type de retour. Ajouter void en fait une méthode ordinaire — qui compile parfaitement, puisque rien n'interdit une méthode nommée Compte — mais que new n'appellera jamais. Le message d'erreur porte alors sur l'appel : « constructor Compte in class Compte cannot be applied to given types », parce que le seul constructeur disponible est celui par défaut, sans paramètre... sauf que celui-ci a disparu dès qu'on a écrit un constructeur — ce qui n'est justement PAS le cas ici, aucun n'ayant été écrit. D'où un message déroutant. Quant au conflit de noms entre paramètre et attribut, il est non seulement légal mais idiomatique : c'est précisément l'usage principal de this.

À vous

L'exercice met deux objets côte à côte et affiche leur état après chaque instruction — c'est l'animation, mais que vous pilotez.

Quatre temps. Créer deux comptes et vérifier qu'un dépôt sur l'un ne touche pas l'autre. Écrire un constructeur qui valide son paramètre, avant même le chapitre 3. Faire l'expérience de l'alias : b = a, un dépôt, et les deux soldes. Enfin, deux pièges de this — un attribut masqué par un paramètre quand on oublie this, et une méthode détachée de son objet, qui montre que this n'est pas fixé une fois pour toutes.

Exercice de code

Mettez deux objets côte à côte, validez dans le constructeur, puis faites tomber les deux pièges de this.

Point de départ

// JavaScript possède class, constructor et this : la transposition depuis
// Java est directe, à la déclaration des types près.

class Compte {
  constructor(titulaire, depotInitial) {
    // ← à écrire : refuser un dépôt initial négatif, puis initialiser
    this.titulaire = titulaire;
    this.solde = depotInitial;
  }
  deposer(montant) { this.solde += montant; }
  getSolde() { return this.solde; }
  toString() { return this.titulaire + " : " + this.solde.toFixed(2); }
}

// Affiche l'état de plusieurs objets CÔTE À CÔTE, comme au tableau.
function etat(etiquette, ...comptes) {
  console.log("   " + etiquette.padEnd(28) +
    comptes.map((c) => c.toString()).join("   |   "));
}

// ── 1. Deux objets, deux jeux d'attributs ─────────────────────────────────
const a = new Compte("Ana", 100);
const b = new Compte("Bo", 50);
etat("après les deux new", a, b);
a.deposer(30);
etat("après a.deposer(30)", a, b);

// ── 2. L'alias ────────────────────────────────────────────────────────────
const c = a;               // COPIE LA RÉFÉRENCE, pas l'objet
c.deposer(20);
etat("après c.deposer(20)", a, b);
console.log("   a et c sont le même objet :", a === c);

// ── 3. L'attribut masqué ──────────────────────────────────────────────────
class CompteFautif {
  constructor(titulaire, solde) {
    titulaire = titulaire;   // ← sans this : on affecte le paramètre à lui-même
    solde = solde;
  }
  toString() { return this.titulaire + " : " + this.solde; }
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Faites refuser un dépôt initial négatif par le constructeur.
// 2. Créez un CompteFautif et affichez-le : que valent ses attributs, et
//    pourquoi le code compile-t-il sans rien dire ?
// 3. Détachez la méthode : const f = a.deposer; f(10). Que se passe-t-il,
//    et qu'est-ce que cela apprend sur this ?

Solution

class Compte {
  constructor(titulaire, depotInitial) {
    // Le constructeur ÉTABLIT L'INVARIANT : à sa sortie, l'objet doit être
    // dans un état valide. Un compte au solde négatif ne doit pas exister.
    if (depotInitial < 0) throw new Error("dépôt initial négatif : " + depotInitial);
    if (!titulaire) throw new Error("un compte doit avoir un titulaire");
    this.titulaire = titulaire;
    this.solde = depotInitial;
  }
  deposer(montant) { this.solde += montant; }
  getSolde() { return this.solde; }
  toString() { return this.titulaire + " : " + this.solde.toFixed(2); }
}

function etat(etiquette, ...comptes) {
  console.log("   " + etiquette.padEnd(28) + comptes.map((c) => c.toString()).join("   |   "));
}

console.log("— 1. deux objets, deux jeux d'attributs —");
const a = new Compte("Ana", 100);
const b = new Compte("Bo", 50);
etat("après les deux new", a, b);
a.deposer(30);
etat("après a.deposer(30)", a, b);
console.log("   Le code de deposer est écrit UNE fois ; il a modifié une case et");
console.log("   pas l'autre, parce que this désignait a et non b.");

console.log("");
console.log("— l'invariant tient dès la construction —");
try { new Compte("Cy", -10); } catch (e) { console.log("   refusé : " + e.message); }

console.log("");
console.log("— 2. l'alias —");
const c = a;
c.deposer(20);
etat("après c.deposer(20)", a, b);
console.log("   a === c :", a === c, "  — un seul objet, deux noms");
const d = new Compte(a.titulaire, a.getSolde());   // là, une vraie copie
d.deposer(1000);
etat("après d.deposer(1000)", a, d);
console.log("   d est un AUTRE objet : il a fallu un second new.");

console.log("");
console.log("— 3. l'attribut masqué —");
class CompteFautif {
  constructor(titulaire, solde) {
    // « solde = solde » affecte le paramètre à lui-même : l'attribut n'est
    // jamais créé. Aucune erreur — c'est du code parfaitement légal, et
    // c'est ce qui le rend dangereux.
    titulaire = titulaire;
    solde = solde;
  }
  toString() { return this.titulaire + " : " + this.solde; }
}
console.log("   " + new CompteFautif("Dee", 500).toString());
console.log("   Les deux attributs valent undefined : le constructeur n'a rien");
console.log("   initialisé. En Java, le code compile aussi — les attributs y");
console.log("   prennent simplement leur valeur par défaut, 0 et null.");

console.log("");
console.log("— 4. la méthode détachée —");
const f = a.deposer;
try { f(10); } catch (e) { console.log("   f(10) échoue : " + e.message.split("\n")[0]); }
const g = a.deposer.bind(a);
g(10);
etat("après g(10), lié à a", a);
console.log("   this n'est pas fixé une fois pour toutes à la définition : il est");
console.log("   déterminé par l'APPEL. En Java le problème ne se pose pas ainsi —");
console.log("   on ne détache pas une méthode de son objet — mais le principe est");
console.log("   le même : une méthode reçoit l'objet sur lequel elle travaille.");

En travaux pratiques

Travaux pratiques 2 · 3 h

Le premier point qui coince : this

Faire disparaître la confusion entre la classe et l'objet, et entre le paramètre et le champ — en provoquant les deux erreurs plutôt qu'en les évitant.

Avant de commencer

  • Le TP 1

Énoncé

  1. Deux objetsCréez deux Document et modifiez le titre du premier. Affichez les deux. Puis affichez ce que vaut une variable statique modifiée sur l'un des deux.
  2. Le constructeur qui ne fait rienÉcrivez un constructeur dont les paramètres portent le même nom que les champs, SANS écrire this. Créez un objet et affichez ses champs. Expliquez. Indice : Le paramètre masque le champ : l'affectation se fait du paramètre vers lui-même.
  3. Le réparer, deux foisCorrigez d'abord en renommant les paramètres, puis avec this. Dites laquelle des deux formes vous garderez et pourquoi.
  4. this dans une méthodeÉcrivez une méthode qui renvoie l'objet lui-même, et enchaînez trois appels sur une seule ligne. Expliquez ce que vaut this à chaque appel.
  5. Plusieurs constructeursÉcrivez trois constructeurs de Document, dont deux qui délèguent au troisième. Vérifiez que la validation n'est écrite qu'une fois.
  6. L'objet non initialiséDéclarez une référence sans l'affecter et appelez une méthode dessus. Lisez le message, et distinguez « la référence est nulle » de « l'objet est vide ».
  7. Comparer deux objetsCréez deux Document de contenu identique et comparez-les avec l'opérateur d'égalité. Expliquez le résultat, et faites de même avec deux chaînes construites de deux façons.
  8. Au fil rougeÉcrivez la classe Mediatheque contenant un tableau de Document, avec ajouter, chercher et lister. Vérifiez qu'aucune méthode n'a besoin de connaître l'intérieur de Document.

C'est réussi quand

  • Vous expliquez pourquoi le constructeur sans this laisse les champs à null
  • Vos trois constructeurs ne dupliquent aucune validation
  • Vous savez dire ce que compare l'opérateur d'égalité sur deux objets

Correction

Le constructeur qui s'affecte à lui-même
public Document(String titre, String auteur) {
  titre  = titre;      /* le PARAMÈTRE est affecté au PARAMÈTRE */
  auteur = auteur;     /* le champ n'est jamais touché */
}

new Document("1984", "Orwell").getTitre()   →  null

/* javac avec -Xlint signale : self-assignment */

Le paramètre MASQUE le champ dans toute la portée du constructeur : le nom titre y désigne le paramètre, jamais le champ. C'est l'erreur numéro un des débuts en Java, et elle ne provoque aucune erreur de compilation — seulement des champs à null constatés plus tard.

Ce que this désigne
public Document(String titre, String auteur) {
  this.titre  = titre;    /* this.titre : le CHAMP de l'objet courant */
  this.auteur = auteur;   /* titre      : le PARAMÈTRE */
}

/* this = « l'objet sur lequel la méthode a été appelée » */
Document a = new Document("1984", "Orwell");
Document b = new Document("Dune", "Herbert");
a.getTitre();   /* dans getTitre, this vaut a */
b.getTitre();   /* dans getTitre, this vaut b */

Une seule méthode existe en mémoire, pas une par objet : this est le paramètre implicite qui dit sur QUI elle travaille. Une fois cela compris, l'essentiel du chapitre s'éclaire — y compris pourquoi une méthode static, qui n'a pas de this, ne peut pas accéder aux champs d'instance. Garder les mêmes noms et écrire this est la convention : renommer en titreParam fonctionne, et signale que l'on n'a pas compris le mécanisme.

L'enchaînement
public Document avecAnnee(int annee) {
  this.annee = annee;
  return this;                    /* on rend l'objet lui-même */
}

Document d = new Document("Dune", "Herbert")
               .avecAnnee(1965)
               .avecGenre("science-fiction")
               .avecCote("SF-HER-01");

/* à chaque appel, this est le MÊME objet : un seul Document existe */

L'enchaînement n'est possible que parce que chaque méthode renvoie this. C'est la base du motif constructeur fluide, très employé en Java. Attention toutefois : cet objet est MUTABLE, et le rendre au milieu d'une chaîne le rend modifiable par n'importe qui — une variante renvoie une copie modifiée plutôt que this.

Les constructeurs qui délèguent
public Document(String titre, String auteur, int annee) {
  if (titre == null || titre.isBlank())
      throw new IllegalArgumentException("titre obligatoire");
  if (annee < 1400 || annee > 2100)
      throw new IllegalArgumentException("année invalide : " + annee);
  this.titre = titre; this.auteur = auteur; this.annee = annee;
}

public Document(String titre, String auteur) {
  this(titre, auteur, 0);          /* this(...) : DÉLÉGATION */
}
public Document(String titre) {
  this(titre, "inconnu", 0);
}

this(...) en première instruction appelle un autre constructeur de la même classe. Toute la validation vit alors à UN seul endroit : ajouter une règle la fait respecter par les trois formes. Sans cette délégation, on recopie la validation trois fois, et la troisième copie finit toujours par diverger.

Référence nulle et comparaison
Document d;              /* champ non initialisé → null */
d.getTitre();            /* NullPointerException */

/* null = AUCUN objet. Différent d'un objet aux champs vides. */

Document a = new Document("1984", "Orwell");
Document b = new Document("1984", "Orwell");
a == b            → false   /* deux objets DISTINCTS, deux adresses */
a.equals(b)       → false   /* tant qu'on n'a pas redéfini equals (TP 6) */

String s1 = "abc", s2 = "abc";
s1 == s2          → true    /* littéraux INTERNÉS : même objet */
String s3 = new String("abc");
s1 == s3          → false   /* d'où : TOUJOURS equals sur les chaînes */

L'opérateur d'égalité compare des RÉFÉRENCES — sont-ce le même objet — jamais des contenus. Le cas des chaînes est le plus traître, parce que l'internement des littéraux le fait fonctionner par accident dans les cas de test, et échouer dès que la chaîne vient d'une lecture ou d'une concaténation. La redéfinition d'equals attend au TP 6.

Ce que la suite en fait

Le chapitre 3 ferme l'objet. Rien n'empêche encore d'écrire a.solde = -1000 de l'extérieur, ce qui ruine tout le bénéfice de l'introduction : la règle est dans la méthode, mais on peut passer à côté. L'encapsulation supprime ce contournement.

Le chapitre 4 reprendra la distinction que l'animation a montrée — variable de classe contre variable d'instance — et la traitera pour elle-même, avec les méthodes statiques et le main.

À retenir

Flashcards · 5 cartes

Quelle est la différence entre une classe et un objet ?
La CLASSE décrit ce qu'un objet contiendra et saura faire : elle n'existe qu'en un exemplaire et ne porte aucune donnée d'utilisateur — c'est un moule. L'OBJET, ou instance, est fabriqué avec ce moule et possède ses PROPRES attributs, distincts de ceux de toutes les autres instances. Les méthodes, elles, n'existent qu'une fois en mémoire : elles reçoivent l'objet sur lequel elles travaillent.
Quelles sont les trois règles du constructeur en Java ?
1) Il porte EXACTEMENT le nom de la classe. 2) Il n'a AUCUN type de retour, pas même void — écrire void en fait une méthode ordinaire que new n'appellera jamais, erreur silencieuse classique. 3) Il établit l'INVARIANT : à sa sortie, l'objet doit être dans un état valide. Et une règle de Java : si l'on n'écrit aucun constructeur, le compilateur en fournit un par défaut sans paramètre — mais dès qu'on en écrit un, celui-là disparaît.
Que fait new, et pourquoi b = a ne copie-t-il pas l'objet ?
new ALLOUE l'objet sur le tas (une case par attribut), INITIALISE ces cases aux valeurs par défaut (0, false, null), EXÉCUTE le constructeur, et REND UNE RÉFÉRENCE. C'est cette référence — une adresse — qui est rangée dans la variable, laquelle vit sur la pile. Donc b = a recopie l'adresse : un seul objet, deux noms, et un dépôt par l'un est visible par l'autre. Pour deux objets indépendants, il faut deux new.
Quels sont les trois usages de this, et où n'existe-t-il pas ?
this désigne l'objet sur lequel la méthode s'exécute, et change à chaque appel. 1) LEVER UNE AMBIGUÏTÉ quand un paramètre porte le nom d'un attribut — l'usage principal. 2) SE PASSER SOI-MÊME à une autre méthode. 3) ENCHAÎNER LES CONSTRUCTEURS avec this(...), qui doit être la première instruction. Il n'existe PAS dans une méthode statique, où il n'y a aucun objet courant — d'où l'erreur classique du premier main.
Comment un objet Java meurt-il, et le ramasse-miettes supprime-t-il les fuites ?
Il vit tant qu'une référence le désigne, et devient récupérable quand plus aucune ne le fait. Le ramasse-miettes repère périodiquement les objets inatteignables : il n'y a donc ni free ni destructeur, et la question « qui libère ? » disparaît. Prix : un coût à l'exécution, avec de brèves pauses — rédhibitoire en temps réel dur. Et il NE SUPPRIME PAS les fuites : un objet encore référencé par une liste qu'on a oublié de vider ne sera jamais récupéré. La fuite Java, c'est « je garde une référence inutile ».