Cours 2 · EncapsulationLeçon 2 sur 2
Membres de classe et surcharge
5 h de lecture8 sections Version PDF
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.
Une classe déclare afficher(double) et afficher(int). On appelle afficher(5). Quelle méthode est choisie, et quand ?
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 primitifs — int, double, boolean, char — ne sont pas des objets : leur
valeur est copiée directement, et aucune modification n'est jamais visible de l'appelant.
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 ?
À 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.
Comptez les instances par un membre de classe, résolvez une surcharge, puis constatez l'échec de echanger.
// ── 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());
En travaux pratiques
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.
- Les TP 2 et 3
- 1. Compter les instances
Ajoutez à 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étrique
Tentez 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 constante
Ajoutez une durée d'emprunt maximale, partagée et non modifiable. Testez ce que le compilateur refuse.
- 4. La fabrique statique
Ajoutez 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.
- 7. Le piège du nombre variable d'arguments
Ajoutez 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 rouge
Ajoutez à Mediatheque un identifiant unique généré automatiquement pour chaque document. Vérifiez qu'il reste unique après création de mille documents.
- 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
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
Vous avez parcouru les 8 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.