Skip to content

Latest commit

 

History

History
277 lines (191 loc) · 13.9 KB

01-classes-objets.md

File metadata and controls

277 lines (191 loc) · 13.9 KB

🔝 Retour au Sommaire

3.1 Classes et objets

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.

Classe et objet : le modèle et l'instance

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 : l'état de l'objet

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 : 0 pour les types numériques, false pour boolean, '\u0000' pour char, et null pour 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 private par 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 seront private.

Les méthodes : le comportement de l'objet

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.0

Les 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.println en est l'exemple le plus connu : il existe une version pour int, une pour String, une pour double… Le compilateur choisit la bonne selon les arguments fournis.

Créer et manipuler des objets

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'objet

Une 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 état

Une 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écution

Le Java moderne (par défaut depuis Java 15) fournit des messages de NullPointerException détaillés : ils indiquent précisément quelle variable était null, 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éfinit equals — voir 3.10.
  • Afficher un objet sans avoir défini toString produit une sortie technique du type CompteBancaire@1b6d3586. Lui donner une représentation lisible relève aussi de 3.10.

Les constructeurs

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).

Le constructeur par défaut

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.

⚠️ Dès que vous déclarez un constructeur, ce constructeur par défaut disparaît. Si vous tenez à conserver la possibilité de construire sans argument, vous devez le redéclarer explicitement.

Constructeurs personnalisés

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.

Plusieurs constructeurs (et leur chaînage)

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.)

Le mot-clé this

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, solde et this.solde désignent la même chose tant qu'aucune variable locale ne porte ce nom. Certains développeurs écrivent this. systématiquement par souci de clarté ; c'est un choix de style.

Membres d'instance vs membres static

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.02

Trois repères :

  • Les constantes se déclarent static final et se nomment en MAJUSCULES_AVEC_UNDERSCORES.
  • Une méthode static n'a pas de this : 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) est static : la JVM peut l'appeler sans avoir créé d'objet au préalable. Vouloir appeler une méthode d'instance directement depuis main, 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 static ne peut pas être initialisé en une seule expression, un bloc statique static { … } 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 bloc static, lui, reste la façon idiomatique de préparer un champ de classe qui demande plusieurs étapes.

Eclipse : générer le code répétitif 🔧

É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.

À retenir

  • Une classe est un modèle ; un objet est une instance créée avec new et 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.
  • this référence l'objet courant, surtout pour distinguer un champ d'un paramètre et pour chaîner les constructeurs via this(...).
  • Un membre static appartient à la classe et est partagé par toutes les instances ; une méthode static n'a pas de this.

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)