🔝 Retour au Sommaire
Les classes décrivent ; les objets existent. Cette section pose la brique élémentaire de toute application Java : définir un type avec son état (champs) et son comportement (méthodes), en créer des instances, et les initialiser correctement.
Une classe est un modèle : elle décrit à quoi ressemble une catégorie d'objets — quelles données ils portent et quelles opérations on peut leur appliquer. Un objet est une instance concrète de ce modèle, créée à l'exécution et vivant en mémoire.
L'analogie classique : la classe est le plan d'architecte, les objets sont les maisons construites d'après ce plan. Un seul plan, autant de maisons que l'on veut — chacune avec sa propre adresse, sa propre couleur, son propre état.
Voici la classe la plus simple possible :
public class Point {
int x;
int y;
}Elle décrit un point à deux coordonnées. On en crée des instances avec new :
Point a = new Point();
Point b = new Point();a et b sont deux objets distincts : modifier l'un n'affecte pas l'autre. La classe (Point) est unique ; les objets (a, b) peuvent se compter par milliers.
Les champs (fields, ou variables d'instance) définissent l'état que chaque objet transporte. Dans la classe Point, x et y sont des champs : chaque instance possède sa propre copie.
var a = new Point();
a.x = 3;
a.y = 4; // l'état de "a" est maintenant (3, 4)Deux propriétés importantes des champs :
- Ils reçoivent une valeur par défaut si on ne les initialise pas :
0pour les types numériques,falsepourboolean,'\u0000'pourchar, etnullpour toute référence d'objet. (Les variables locales, elles, n'ont aucune valeur par défaut et doivent être initialisées avant usage — le compilateur l'exige.) - On les déclare
privatepar convention. Exposer directement les champs (public) brise l'encapsulation, le principe qui consiste à protéger l'état interne d'un objet. Nous y consacrons la section 3.2 ; à partir d'ici, nos champs serontprivate.
Les méthodes définissent ce qu'un objet sait faire. Une méthode possède un type de retour, un nom, une liste de paramètres et un corps :
public class CompteBancaire {
private String titulaire;
private double solde;
public void deposer(double montant) { // ne retourne rien -> void
if (montant <= 0) {
throw new IllegalArgumentException("Le montant doit être positif.");
}
solde += montant;
}
public double getSolde() { // retourne un double
return solde;
}
}On appelle une méthode sur un objet, via l'opérateur . :
var compte = new CompteBancaire();
compte.deposer(50.0);
double s = compte.getSolde(); // 50.0Les arguments sont passés par valeur. C'est un point souvent mal compris. Java copie toujours la valeur passée. Pour un type primitif, c'est la valeur elle-même ; réaffecter le paramètre dans la méthode n'a aucun effet à l'extérieur :
void doubler(int n) { n = n * 2; } // ne modifie que la copie locale
var x = 5;
doubler(x);
System.out.println(x); // 5 — inchangéPour un objet, c'est la référence qui est copiée. La méthode peut donc modifier l'objet pointé, mais pas réaffecter la variable de l'appelant :
void crediter(CompteBancaire c) { c.deposer(10.0); } // modifie l'objet pointé
crediter(compte);
System.out.println(compte.getSolde()); // 60.0 — l'objet a bien changéEn revanche, si crediter exécutait c = new CompteBancaire(), la variable compte de l'appelant continuerait de pointer vers l'objet d'origine : on n'a réaffecté que la copie de la référence.
Surcharge (overloading). Plusieurs méthodes peuvent porter le même nom si leurs paramètres diffèrent (en nombre ou en type).
System.out.printlnen est l'exemple le plus connu : il existe une version pourint, une pourString, une pourdouble… Le compilateur choisit la bonne selon les arguments fournis.
L'opérateur new fait trois choses : il alloue l'objet en mémoire (sur le tas — voir chapitre 14), exécute un constructeur pour l'initialiser, puis renvoie une référence vers cet objet.
var compte = new CompteBancaire(); // "compte" est une référence vers l'objetUne variable d'objet ne contient pas l'objet : elle pointe vers lui. Deux variables peuvent donc référencer le même objet (on parle d'aliasing) :
var c1 = new CompteBancaire();
var c2 = c1; // c2 pointe vers le MÊME objet que c1
c1.deposer(100.0);
System.out.println(c2.getSolde()); // 100.0 — c1 et c2 voient le même étatUne référence qui ne pointe vers rien vaut null. Tenter d'appeler une méthode sur null déclenche une NullPointerException :
CompteBancaire vide = null;
vide.getSolde(); // NullPointerException à l'exécutionLe Java moderne (par défaut depuis Java 15) fournit des messages de
NullPointerExceptiondétaillés : ils indiquent précisément quelle variable étaitnull, ce qui accélère beaucoup le débogage.
Deux remarques utiles, approfondies plus loin :
- Comparer deux objets avec
==compare leurs références (pointent-ils vers le même objet ?), pas leur contenu. Pour comparer le contenu, on redéfinitequals— voir 3.10. - Afficher un objet sans avoir défini
toStringproduit une sortie technique du typeCompteBancaire@1b6d3586. Lui donner une représentation lisible relève aussi de 3.10.
Un constructeur initialise un objet au moment de sa création. Il porte le nom de la classe et n'a aucun type de retour (pas même void).
Si vous ne déclarez aucun constructeur, le compilateur en fournit un implicite, sans argument, qui ne fait rien de particulier. C'est lui qui permettait d'écrire new Point() plus haut.
On écrit le plus souvent un ou plusieurs constructeurs pour exiger les données nécessaires à un objet valide :
public class CompteBancaire {
private String titulaire;
private double solde;
public CompteBancaire(String titulaire, double soldeInitial) {
this.titulaire = titulaire;
this.solde = soldeInitial;
}
}var compte = new CompteBancaire("Awa", 100.0);Imposer ce constructeur garantit qu'aucun compte ne peut exister sans titulaire : c'est une première façon de protéger les invariants de votre type.
Comme les méthodes, les constructeurs peuvent être surchargés. Plutôt que de dupliquer le code d'initialisation, un constructeur peut en appeler un autre avec this(...) :
public CompteBancaire(String titulaire) {
this(titulaire, 0.0); // délègue à l'autre constructeur : solde initial = 0
}L'appel this(...) doit être la première instruction du constructeur. (Les versions récentes de Java assouplissent cette contrainte pour des cas avancés — les « corps de constructeur flexibles » — mais la règle « this(...) / super(...) en premier » reste la référence à connaître.)
this est une référence vers l'objet courant : l'instance sur laquelle la méthode ou le constructeur s'exécute. Il a quatre usages, du plus courant au plus accessoire.
1. Lever une ambiguïté entre un champ et un paramètre. C'est son usage quotidien. Lorsqu'un paramètre porte le même nom qu'un champ, this.champ désigne le champ, le nom seul désigne le paramètre :
public CompteBancaire(String titulaire, double soldeInitial) {
this.titulaire = titulaire; // champ <- paramètre
this.solde = soldeInitial;
}Sans le this., titulaire = titulaire se contenterait d'affecter le paramètre à lui-même, et le champ resterait null.
2. Appeler un autre constructeur de la même classe — le chaînage this(...) vu ci-dessus.
3. Retourner l'objet courant pour enchaîner les appels (style fluide). Par exemple, si deposer renvoyait l'objet au lieu de void :
public CompteBancaire deposer(double montant) {
solde += montant;
return this;
}
// compte.deposer(10).deposer(20).deposer(5);4. Se passer soi-même en argument à une autre méthode : journal.enregistrer(this);.
this.est implicite quand il n'y a pas d'ambiguïté : à l'intérieur d'une méthode,soldeetthis.soldedésignent la même chose tant qu'aucune variable locale ne porte ce nom. Certains développeurs écriventthis.systématiquement par souci de clarté ; c'est un choix de style.
Jusqu'ici, champs et méthodes appartenaient à chaque objet. Un membre déclaré static appartient au contraire à la classe elle-même, et il est partagé par toutes les instances.
public class CompteBancaire {
public static final double TAUX_INTERET = 0.02; // constante de classe
private static int nombreDeComptes = 0; // compteur partagé
private final String numero; // propre à chaque compte
private double solde;
public CompteBancaire(double soldeInitial) {
nombreDeComptes++; // partagé : +1 à chaque création
this.numero = "CB-" + nombreDeComptes; // figé pour la vie de l'objet
this.solde = soldeInitial;
}
public static int getNombreDeComptes() { // méthode de classe
return nombreDeComptes;
}
}On accède à un membre static via le nom de la classe :
new CompteBancaire(100);
new CompteBancaire(50);
System.out.println(CompteBancaire.getNombreDeComptes()); // 2
System.out.println(CompteBancaire.TAUX_INTERET); // 0.02Trois repères :
- Les constantes se déclarent
static finalet se nomment enMAJUSCULES_AVEC_UNDERSCORES. - Une méthode
staticn'a pas dethis: elle ne peut donc pas accéder directement aux champs d'instance, puisqu'il n'existe aucune instance « courante ». - C'est précisément pour cela que
public static void main(String[] args)eststatic: la JVM peut l'appeler sans avoir créé d'objet au préalable. Vouloir appeler une méthode d'instance directement depuismain, sans objet, est l'une des erreurs les plus fréquentes des débuts.
J'ai aussi glissé un champ final (numero) : un champ final ne peut être affecté qu'une seule fois — à sa déclaration ou dans le constructeur — puis devient immuable. C'est une première étape vers les objets immuables, que les records (section 3.5) portent à leur aboutissement.
Les blocs d'initialisation. Lorsqu'un champ
staticne peut pas être initialisé en une seule expression, un bloc statiquestatic { … }s'en charge — il s'exécute une seule fois, au chargement de la classe :private static final int[] CARRES = new int[10]; static { // exécuté une fois, au chargement de la classe for (int i = 0; i < CARRES.length; i++) { CARRES[i] = i * i; // 0, 1, 4, 9, … } }Il existe aussi le bloc d'instance
{ … }, exécuté avant chaque constructeur ; en pratique on lui préfère l'initialisation à la déclaration ou le constructeur lui-même, plus lisibles. Le blocstatic, lui, reste la façon idiomatique de préparer un champ de classe qui demande plusieurs étapes.
Écrire à la main les constructeurs, getters et setters est fastidieux et source d'erreurs. Eclipse les génère pour vous : menu Source (ou Alt+Shift+S), puis Generate Constructor using Fields… et Generate Getters and Setters…. Vous déclarez vos champs, l'IDE produit le reste. Ces automatismes — ainsi que la génération de equals/hashCode/toString — sont détaillés à la section 5.7. Retenez l'idée : votre travail est de concevoir l'état et le comportement, pas de taper du code mécanique.
- Une classe est un modèle ; un objet est une instance créée avec
newet vivant en mémoire. Une variable d'objet est une référence vers cet objet, pas l'objet lui-même. - Les champs portent l'état (un exemplaire par objet) ; ils reçoivent une valeur par défaut et se déclarent
private. - Les méthodes portent le comportement ; les arguments sont passés par valeur (pour un objet, c'est la référence qui est copiée).
- Un constructeur (même nom que la classe, sans type de retour) initialise l'objet et protège ses invariants ; le constructeur par défaut implicite disparaît dès qu'on en déclare un.
thisréférence l'objet courant, surtout pour distinguer un champ d'un paramètre et pour chaîner les constructeurs viathis(...).- Un membre
staticappartient à la classe et est partagé par toutes les instances ; une méthodestaticn'a pas dethis.
La section suivante, 3.2 Encapsulation, explique pourquoi les champs sont private et comment exposer un objet via une interface maîtrisée.
⏭ Encapsulation et modificateurs d'accès (public/private/protected/package)