↑
CHAPITRE 2

Classes et objets

Modéliser, encapsuler, relier des objets

Au chapitre 1, vous avez écrit du code dans une classe main. Maintenant, vous allez concevoir vos propres types : une classe décrit un modèle, un objet est une instance concrète de ce modèle. L’objectif : savoir créer une classe, l’encapsuler, la relier à d’autres objets, et comparer correctement vos instances.

Points essentiels
  • Classe vs objet (analogie, conception)
  • Attributs, mĂ©thodes, new, rĂ©fĂ©rence
  • Constructeurs, surcharge, this / this(...)
  • Encapsulation (private, default, public, getters / setters)
  • static, static final, instance vs classe
  • RĂ©fĂ©rences, null, NullPointerException
  • Association et composition
  • toString, equals, hashCode (principe)
  • enum avec attributs et mĂ©thodes

2.1Classe et objet

Concevoir une classe et un objet

Avant d’ouvrir l’éditeur, posez-vous une question simple : qu’est-ce qu’une classe, et qu’est-ce qu’un objet ? Pour l’instant, pas de syntaxe Java : seulement des idées tirées de la vie réelle.

Étape 1 — Le plan et la réalisation

Imaginez le plan d’un architecte pour une maison. Le plan décrit ce que toute maison de ce type doit avoir : des pièces, des portes, des fenêtres. Mais le plan n’est pas la maison.

Quand on construit une maison à Casablanca, puis une autre à Rabat, on obtient deux maisons concrètes. Elles suivent le même plan, pourtant chacune a sa propre adresse, sa propre couleur, ses propres habitants.

En programmation orientée objet, on retrouve exactement cette distinction :

  • la classe, c’est le plan (le modèle) ;
  • l’objet, c’est une rĂ©alisation concrète de ce plan.

Étape 2 — Analogie du véhicule

Dans la vie quotidienne, le mot « véhicule » désigne une catégorie. Vous savez qu’un véhicule a en général une marque, une immatriculation, une vitesse, et qu’il peut démarrer ou s’arrêter. À ce stade, vous ne parlez encore d’aucune voiture précise.

Les objets, ce sont les véhicules concrets : la voiture de Youssef Alaoui, le taxi de Fatima Benjelloun, un bus à Casablanca… Tous appartiennent à la même catégorie « véhicule », mais chacun a ses propres caractéristiques.

Progression : d’abord la catégorie (le modèle), ensuite les exemplaires (les objets). Une catégorie, plusieurs exemplaires.

Étape 3 — Analogie de la personne

Même raisonnement avec les personnes. L’idée de « personne » regroupe ce qui est commun : un nom, un prénom, un âge… Ce n’est pas encore Amine Tazi, ni Sara Idrissi.

Les objets, ce sont les personnes concrètes de votre application : Amine Tazi (22 ans), Sara Idrissi (20 ans), Omar Chakir (28 ans)… Chacune est un exemplaire du même modèle « personne », avec ses propres valeurs.

Classe Personne (modèle avec nom, prénom, âge) et trois objets concrets : Amine Tazi, Sara Idrissi, Omar Chakir
À retenir avant le code : la classe répond à « quoi ? » (quel type de chose). L’objet répond à « qui / lequel ? » (quel exemplaire précis).

Étape 4 — Passer à Java

Maintenant que l’idée est claire, vous allez l’écrire en Java. Dans la suite du chapitre, nous prendrons surtout le modèle Personne (nom, prénom, âge), avec des exemples concrets. Commencez par déclarer la classe : c’est l’équivalent du plan.

2.1.1 – Déclaration d’une classe

En Java, vous déclarez une classe avec le mot-clé class. C’est le moment où le modèle devient du code.

public class Personne {
    // contenu de la classe
}
Rappel : une classe public se place dans un fichier du mĂŞme nom (Personne.java), en PascalCase.

2.1.2 – Attributs et méthodes

À l’intérieur de la classe, vous placez deux familles d’éléments :

  • les attributs : les donnĂ©es de chaque objet (nom, prĂ©nom, âge…) ;
  • les mĂ©thodes : les actions que l’objet peut rĂ©aliser (afficher, vieillir…).
public class Personne {
    String nom;
    String prenom;
    int age;

    void afficher() {
        System.out.println(prenom + " " + nom + ", " + age + " ans");
    }
}

Pour l’instant, les attributs sont encore visibles de l’extérieur. Nous les rendrons private un peu plus loin (encapsulation).

2.1.3 – Instanciation avec new

Une instance est un objet concret créé à partir d’une classe. En d’autres termes : la classe est le modèle ; l’instance est l’exemplaire réel obtenu à partir de ce modèle. On dit aussi « créer une instance » pour dire « créer un objet ».

La classe seule ne crée aucune personne. Pour obtenir une instance, vous utilisez new : Java réserve de la mémoire et construit l’objet. Cette opération s’appelle l’instanciation.

public class MainClass {
    public static void main(String[] args) {
        Personne p = new Personne();
        p.nom = "Tazi";
        p.prenom = "Amine";
        p.age = 22;
        p.afficher();
    }
}
Définition : p est une instance de la classe Personne (ici : Amine Tazi, 22 ans). new Personne() réalise l’instanciation.
À retenir : new Personne() crée l’objet ; la variable p contient une référence vers cet objet.

2.1.4 – Référence et objet

Ne confondez pas la variable et l’objet. La variable stocke l’adresse (la référence). L’objet, lui, vit en mémoire.

public class MainClass {
    public static void main(String[] args) {
        Personne a = new Personne();
        Personne b = a;          // b pointe vers LE MĂŠME objet que a
        b.prenom = "Sara";
        b.nom = "Idrissi";
        System.out.println(a.prenom); // Sara
    }
}

Ici, vous n’avez qu’un seul objet, et deux variables qui le désignent. Nous reviendrons sur ce point en 2.6.

2.2Attributs et méthodes

2.2.1 – Attributs d’instance

Un attribut d’instance appartient à chaque objet. Deux personnes peuvent avoir des noms différents, car chaque instance a sa propre copie des attributs.

Chaque objet Personne a sa propre copie des attributs : Youssef Alaoui et Fatima Benjelloun sont indépendants
public class MainClass {
    public static void main(String[] args) {
        Personne a = new Personne();
        Personne b = new Personne();
        a.prenom = "Youssef";
        a.nom = "Alaoui";
        b.prenom = "Fatima";
        b.nom = "Benjelloun";
        // a et b sont indépendants
    }
}

Rappel du chapitre 1 : un attribut reçoit une valeur par défaut (0, false, null…) s’il n’est pas initialisé.

2.2.2 – Méthodes d’instance

Une méthode d’instance s’appelle sur un objet : a.afficher(). À l’intérieur de la méthode, vous accédez directement aux attributs de cet objet.

public class Personne {
    String nom;
    String prenom;
    int age;

    void afficher() {
        System.out.println(prenom + " " + nom + ", " + age + " ans");
    }
}

2.2.3 – Paramètres et valeurs de retour

Une méthode peut recevoir des paramètres et renvoyer une valeur avec return. Si elle ne renvoie rien, le type de retour est void.

public class Personne {
    String nom;
    String prenom;
    int age;

    boolean estMajeur() {
        return age >= 18;
    }

    void vieillir(int annees) {
        age = age + annees;
    }
}

2.2.4 – Passage des arguments : par valeur et par adresse

Quand vous appelez une méthode, Java transmet les arguments au paramètre. Selon le type, on parle de passage par valeur ou de passage par adresse.

  • Passage par valeur : pour un primitif, vous copiez la valeur (int, double…). La mĂ©thode travaille sur une copie : modifier le paramètre ne change pas la variable d’origine chez l’appelant.
  • Passage par adresse : pour un objet, vous copiez la rĂ©fĂ©rence (l’adresse), pas l’objet entier. La mĂ©thode reçoit donc l’adresse du mĂŞme objet : elle peut modifier son contenu (ses attributs).
public class MainClass {
    // Passage par valeur : la copie locale change, pas la variable d’origine
    static void essayerDeChanger(int age) {
        age = 30;
    }

    // Passage par adresse : on modifie l’objet via son adresse
    static void changerAge(Personne p) {
        p.age = 30;
    }

    public static void main(String[] args) {
        int a = 22;
        essayerDeChanger(a);
        System.out.println(a); // 22

        Personne amine = new Personne();
        amine.prenom = "Amine";
        amine.nom = "Tazi";
        amine.age = 22;

        changerAge(amine);
        System.out.println(amine.age); // 30
    }
}

2.2.5 – this

Le mot-clé this désigne l’objet courant : celui sur lequel vous avez appelé la méthode. Il sert surtout à lever une ambiguïté quand un paramètre porte le même nom qu’un attribut.

Sans this, le nom age désigne le paramètre, pas l’attribut. L’affectation age = age ne changerait donc rien à l’objet.

public class Personne {
    String nom;
    String prenom;
    int age;

    void setAge(int age) {
        this.age = age;   // this.age = attribut de l’objet ; age = paramètre
    }

    void afficher() {
        System.out.println(prenom + " " + nom + ", " + age + " ans");
    }
}

public class MainClass {
    public static void main(String[] args) {
        Personne p = new Personne();
        p.nom = "Tazi";
        p.prenom = "Amine";
        p.setAge(22);   // this désigne l’objet p
        p.afficher();   // Amine Tazi, 22 ans
    }
}
Lecture : quand vous écrivez p.setAge(22), à l’intérieur de setAge, this représente p. Donc this.age = age signifie : « range 22 dans l’attribut age de cet objet ».

2.3Constructeurs

2.3.1 – Rôle du constructeur

Un constructeur initialise un objet au moment du new. Il porte le même nom que la classe et n’a pas de type de retour (pas même void).

public class Personne {
    private String nom;
    private String prenom;
    private int age;

    public Personne(String nom, String prenom, int age) {
        this.nom = nom;
        this.prenom = prenom;
        this.age = age;
    }
}

public class MainClass {
    public static void main(String[] args) {
        Personne p = new Personne("Alaoui", "Youssef", 25);
    }
}

2.3.2 – Constructeur par défaut

Règle : Si vous n’écrivez aucun constructeur, Java en fournit un automatiquement : le constructeur par défaut, sans paramètre. Dès que vous écrivez un constructeur, ce constructeur par défaut disparaît.

Tant que vous n’avez écrit aucun constructeur, ceci fonctionne :

public class Personne {
    String nom;
    String prenom;
    int age;
    // aucun constructeur écrit → Java fournit Personne()
}

public class MainClass {
    public static void main(String[] args) {
        Personne p = new Personne(); // OK : constructeur par défaut
        p.nom = "Tazi";
        p.prenom = "Amine";
        p.age = 22;
    }
}

Dès que vous ajoutez un constructeur avec paramètres, le constructeur par défaut disparaît. new Personne() ne compile plus, sauf si vous l’écrivez vous-même :

public class Personne {
    String nom;
    String prenom;
    int age;

    public Personne(String nom, String prenom, int age) {
        this.nom = nom;
        this.prenom = prenom;
        this.age = age;
    }
}

public class MainClass {
    public static void main(String[] args) {
        // Personne a = new Personne();  // ❌ erreur : pas de constructeur sans paramètre
        Personne b = new Personne("Alaoui", "Youssef", 25); // OK
    }
}

2.3.3 – Surcharge de constructeurs

Vous pouvez proposer plusieurs constructeurs, avec des listes de paramètres différentes. C’est la surcharge : Java choisit le bon selon les arguments du new.

public class Personne {
    private String nom;
    private String prenom;
    private int age;

    public Personne(String nom, String prenom) {
        this.nom = nom;
        this.prenom = prenom;
        this.age = 0;
    }

    public Personne(String nom, String prenom, int age) {
        this.nom = nom;
        this.prenom = prenom;
        this.age = age;
    }
}

2.3.4 – this(...)

Vous avez parfois plusieurs constructeurs qui font presque le même travail. Pour ne pas répéter le même code, un constructeur peut appeler un autre constructeur grâce à this(...).

Règle : cet appel doit être la première instruction du constructeur. Rien ne peut être écrit avant.
public class Personne {
    private String nom;
    private String prenom;
    private int age;

    public Personne(String nom, String prenom) {
        this(nom, prenom, 0);   // OK : première instruction
    }

    public Personne(String nom, String prenom, int age) {
        this.nom = nom;
        this.prenom = prenom;
        this.age = age;
    }

    // ❌ erreur : this(...) n’est pas en première position
    // public Personne(String nom) {
    //     this.nom = nom;
    //     this(nom, "Inconnu", 0);
    // }
}

2.4Encapsulation

Jusqu’ici, n’importe qui pouvait écrire directement p.age = -5. Ce n’est pas très sûr, n’est-ce pas ? L’encapsulation répond à ce problème : vous protégez les données de l’objet et vous contrôlez la façon dont on les lit ou on les modifie.

2.4.1 – private, default et public

L’idée est simple. Vous cachez l’intérieur de l’objet, et vous n’ouvrez que ce qui est utile à l’extérieur. Pour cela, Java propose plusieurs niveaux d’accès. Pour l’instant, retenez-en trois :

  • private : accessible uniquement dans la classe elle-mĂŞme ;
  • default (aucun mot-clĂ©) : accessible dans le mĂŞme package, mais pas depuis un autre package ;
  • public : accessible depuis n’importe oĂą.

Il existe aussi protected : nous le verrons au chapitre sur l’héritage.

En pratique, vous rendez souvent les attributs private. Ainsi, personne ne peut les modifier directement depuis l’extérieur.

public class Personne {
    private String nom;      // private : seulement dans Personne
    private String prenom;
    private int age;

    String codeInterne;      // default : visible dans le mĂŞme package

    public Personne(String nom, String prenom, int age) {
        this.nom = nom;
        this.prenom = prenom;
        this.age = age;
    }
}

public class MainClass {
    public static void main(String[] args) {
        Personne p = new Personne("Tazi", "Amine", 22);
        // p.age = -5;  // ❌ erreur : age est private
    }
}

2.4.2 – Getters et setters

Si les attributs sont privés, comment les lire ou les modifier ? Vous passez par des méthodes publiques : les getters et les setters.

Un getter renvoie la valeur d’un attribut. Un setter la change — et c’est le bon endroit pour vérifier que la valeur est correcte.

public class Personne {
    private String nom;
    private String prenom;
    private int age;

    public Personne(String nom, String prenom, int age) {
        this.nom = nom;
        this.prenom = prenom;
        this.age = age;
    }

    public String getNom() {
        return nom;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age < 0) {
            System.err.println("Âge négatif");
            return;
        }
        this.age = age;
    }
}

public class MainClass {
    public static void main(String[] args) {
        Personne p = new Personne("Tazi", "Amine", 22);
        System.out.println(p.getAge()); // 22
        p.setAge(23);                   // OK
        p.setAge(-1);                   // message d’erreur, age inchangé
    }
}
Convention de nommage : pour un attribut nom, vous écrivez getNom() et setNom(...). Pour un booléen, on préfère souvent is au lieu de get : par exemple, si l’attribut s’appelle actif, la méthode de lecture s’écrit isActif().

2.4.3 – Protection de l’état interne d’un objet

Pourquoi tout cela ? Pour protéger l’état de l’objet. Sans encapsulation, un autre programmeur (ou vous-même plus tard) pourrait laisser un âge négatif, un nom vide, ou toute autre valeur absurde.

Avec l’encapsulation, les règles vivent dans la classe. Si demain la règle change, vous la corrigez à un seul endroit : dans le setter ou dans le constructeur.

Encapsulation : sans contrôle l’âge peut être invalide ; avec attributs privés et setters, les valeurs sont protégées
En pratique, retenez ceci : vous rendez les attributs private pour les protéger. Vous exposez ensuite des méthodes public utiles (getters, setters, et éventuellement d’autres actions). Enfin, vous placez les contrôles dans les setters et les constructeurs : c’est là que vous refusez une valeur incorrecte, par exemple un âge négatif.

2.5static et final

Jusqu’ici, chaque objet avait ses propres données : le nom d’Amine, l’âge de Sara… Mais parfois, une information concerne toutes les personnes à la fois. C’est le rôle de static : appartenir à la classe, et non à une seule instance.

2.5.1 – Attributs static

Un attribut static n’est pas copié dans chaque objet. Il existe une seule fois, partagée par toutes les instances. Imaginez un compteur : chaque fois que vous créez une personne, le compteur augmente. Ce compteur ne « vit » pas dans Amine ou dans Sara : il vit au niveau de la classe Personne.

Variable static compteur partagée par tous les objets Personne ; chaque objet garde ses propres nom, prénom et âge
public class Personne {
    private String nom;
    private String prenom;
    private int age;
    private static int compteur = 0;

    public Personne(String nom, String prenom, int age) {
        this.nom = nom;
        this.prenom = prenom;
        this.age = age;
        compteur++;
    }

    public static int getCompteur() {
        return compteur;
    }
}

public class MainClass {
    public static void main(String[] args) {
        Personne a = new Personne("Tazi", "Amine", 22);
        Personne b = new Personne("Idrissi", "Sara", 20);
        System.out.println(Personne.getCompteur()); // 2
    }
}

2.5.2 – Méthodes static

Une méthode static s’appelle directement sur la classe : Personne.getCompteur(). Vous n’avez pas besoin d’un objet pour l’utiliser.

En revanche, une méthode static ne peut pas utiliser this, ni lire un attribut d’instance comme nom ou age. Elle ne « connaît » pas une personne en particulier : elle travaille au niveau de la classe.

Rappel : la méthode main est static pour cette raison. La JVM la lance sans créer d’objet au préalable.

2.5.3 – Constantes avec static final

Quand une valeur ne doit jamais changer et qu’elle est commune à toute la classe, vous combinez static et final. Vous obtenez une constante de classe.

Par convention, son nom s’écrit en MAJUSCULES_AVEC_UNDERSCORES : ainsi, on voit tout de suite qu’il s’agit d’une constante.

public class Personne {
    private int age;
    public static final int AGE_MAJORITE = 18;

    public boolean estMajeur() {
        return age >= AGE_MAJORITE;
    }
}

2.5.4 – Différence entre instance et classe

Le point important à bien comprendre, c’est le contexte d’exécution. Une méthode static appartient à la classe. Une méthode d’instance appartient à un objet précis. Ce n’est donc pas le même cadre de travail.

Comparez d’abord ce qui appartient à un objet et ce qui appartient à la classe :

Instance Classe (static)
Appartenance Chaque objet a sa copie Une seule copie, partagée
Appel amine.afficher() Personne.getCompteur()
Accès à this Oui Non

La règle fondamentale

Cette différence de contexte entraîne une règle simple sur les accès :

Depuis une méthode… Variable d’instance Méthode d’instance Variable static Méthode static
static ❌ Non ❌ Non ✅ Oui ✅ Oui
d’instance ✅ Oui ✅ Oui ✅ Oui ✅ Oui

En d’autres termes : une méthode static ne peut pas utiliser directement les données d’un objet particulier, car elle ne sait pas de quel objet il s’agit. Une méthode d’instance, elle, peut tout utiliser : les attributs de l’objet courant, et aussi les éléments static de la classe.

public class Personne {
    private String nom;
    private static int compteur = 0;

    public void afficher() {
        System.out.println(nom);       // OK : méthode d’instance → attribut d’instance
        System.out.println(compteur);  // OK : méthode d’instance → static aussi
    }

    public static int getCompteur() {
        // System.out.println(nom);    // ❌ erreur : static → pas d’attribut d’instance
        return compteur;               // OK : static → static
    }
}
À retenir : static = niveau classe (pas d’objet précis). Instance = niveau objet (avec this). C’est pour cela qu’une méthode static ne peut pas « voir » nom ou age d’une personne particulière.

2.6Références et null

Une variable de type objet ne contient pas l’objet lui-même. Elle contient une référence : en quelque sorte, l’adresse de l’objet en mémoire. Cette idée explique plusieurs situations fréquentes.

2.6.1 – Plusieurs références vers un même objet

Deux variables peuvent désigner le même objet. Si vous modifiez l’objet via l’une, l’autre voit le même changement — car il n’y a qu’un seul objet en mémoire.

public class MainClass {
    public static void main(String[] args) {
        Personne a = new Personne("Tazi", "Amine", 22);
        Personne b = a;          // b pointe vers le mĂŞme objet que a
        b.setAge(23);
        System.out.println(a.getAge()); // 23
    }
}

2.6.2 – null

Parfois, une variable d’objet n’a encore aucune cible. Dans ce cas, elle vaut null : elle ne pointe vers aucun objet.

public class MainClass {
    public static void main(String[] args) {
        Personne p = null;   // pas encore d’objet
    }
}

2.6.3 – NullPointerException

Si vous demandez quelque chose à une référence null, Java ne peut pas répondre : il n’y a pas d’objet derrière. À l’exécution, vous obtenez une NullPointerException.

public class MainClass {
    public static void main(String[] args) {
        Personne p = null;
        p.afficher(); // ❌ NullPointerException
    }
}

Le même piège apparaît avec un tableau d’objets. new Personne[5] crée un tableau de 5 cases, mais chaque case vaut encore null : aucun objet Personne n’a été créé.

public class MainClass {
    public static void main(String[] args) {
        Personne[] liste = new Personne[5];
        liste[0].setNom("Tazi"); // ❌ NullPointerException : liste[0] vaut null
    }
}

Pour que cela fonctionne, créez d’abord l’objet, puis placez-le dans le tableau :

public class MainClass {
    public static void main(String[] args) {
        Personne[] liste = new Personne[5];
        liste[0] = new Personne("Tazi", "Amine", 22);
        liste[0].setNom("Alaoui"); // OK
    }
}
Bon réflexe : avant d’utiliser une référence douteuse, testez if (p != null) { … }.

2.6.4 – Copie d’une référence vs copie d’un objet

Attention à ne pas confondre deux opérations très différentes. L’affectation Personne b = a; copie la référence : vous avez toujours un seul objet, avec deux noms pour le désigner.

Si vous voulez un nouvel objet indépendant, vous devez le créer (par exemple avec un nouveau new). Ensuite, modifier b ne change plus a.

public class MainClass {
    public static void main(String[] args) {
        Personne a = new Personne("Idrissi", "Sara", 20);
        Personne b = new Personne(a.getNom(), a.getPrenom(), a.getAge()); // nouvel objet
        b.setAge(21);
        System.out.println(a.getAge()); // 20
        System.out.println(b.getAge()); // 21
    }
}

2.7Relations entre objets

Dans un vrai programme, les objets ne vivent pas isolés. Une personne a un contact, une adresse, parfois un véhicule… Voyons deux façons simples de relier des objets entre eux.

2.7.1 – Association entre objets

Une association relie deux objets qui peuvent exister l’un sans l’autre. Par exemple, Youssef peut avoir Fatima comme contact. Fatima existe déjà ; Youssef se contente de la référencer.

public class Personne {
    private Personne contact;   // association

    public void setContact(Personne contact) {
        this.contact = contact;
    }
}

public class MainClass {
    public static void main(String[] args) {
        Personne youssef = new Personne("Alaoui", "Youssef", 25);
        Personne fatima = new Personne("Benjelloun", "Fatima", 30);
        youssef.setContact(fatima);
    }
}

2.7.2 – Composition

La composition est une relation entre deux objets dans laquelle un objet est constitué d’un ou plusieurs autres objets.

L’idée peut se résumer ainsi : « un objet possède un autre objet comme partie de lui ».

Exemple simple : une voiture et son moteur. La voiture possède un moteur. Sans voiture, ce moteur n’a pas vraiment de sens dans ce modèle. En général, la voiture crée le moteur dans son constructeur.

public class Moteur {
    private int puissance;

    public Moteur(int puissance) {
        this.puissance = puissance;
    }
}

public class Voiture {
    private String modele;
    private Moteur moteur;   // composition : la voiture possède un moteur

    public Voiture(String modele, int puissance) {
        this.modele = modele;
        this.moteur = new Moteur(puissance); // créé avec la voiture
    }
}
En bref : association = « je pointe vers un objet déjà là ». Composition = « je possède un objet qui fait partie de moi ».

2.7.3 – Exemple : Personne et Adresse

Voici un autre exemple, avec notre fil conducteur. Quand vous créez Omar Chakir, vous créez aussi son adresse au même moment.

public class Adresse {
    private String ville;
    private String rue;

    public Adresse(String ville, String rue) {
        this.ville = ville;
        this.rue = rue;
    }

    public String getVille() {
        return ville;
    }
}

public class Personne {
    private String nom;
    private String prenom;
    private int age;
    private Adresse adresse;   // composition

    public Personne(String nom, String prenom, int age, String ville, String rue) {
        this.nom = nom;
        this.prenom = prenom;
        this.age = age;
        this.adresse = new Adresse(ville, rue); // créée avec la personne
    }

    public void afficher() {
        System.out.println(prenom + " " + nom + " — " + adresse.getVille());
    }
}

public class MainClass {
    public static void main(String[] args) {
        Personne p = new Personne("Chakir", "Omar", 28, "Casablanca", "Bd Zerktouni");
        p.afficher(); // Omar Chakir — Casablanca
    }
}
Ici, l’adresse est créée par et pour la personne. C’est l’idée de la composition, formulée simplement.

2.8Méthodes de Object

En Java, toute classe hérite implicitement de Object. Vous n’avez rien à écrire pour cela. Trois méthodes reviennent très souvent : toString, equals et hashCode. Voyons-les une par une.

2.8.1 – toString()

Quand vous affichez un objet avec System.out.println(p), Java appelle automatiquement toString(). Sans redéfinition, le résultat est peu lisible. En la redéfinissant, vous choisissez un texte clair.

public class Personne {
    private String nom;
    private String prenom;
    private int age;

    public Personne(String nom, String prenom, int age) {
        this.nom = nom;
        this.prenom = prenom;
        this.age = age;
    }

    @Override
    public String toString() {
        return prenom + " " + nom + " (" + age + " ans)";
    }
}

public class MainClass {
    public static void main(String[] args) {
        Personne p = new Personne("Tazi", "Amine", 22);
        System.out.println(p); // Amine Tazi (22 ans)
    }
}

2.8.2 – equals()

equals sert à comparer le contenu de deux objets selon la règle que vous choisissez. Par défaut, dans Object, elle se comporte comme == : elle vérifie si c’est exactement la même instance en mémoire.

Pour vos classes, vous la redéfinissez souvent. Par exemple : deux personnes sont égales si elles ont le même nom, le même prénom et le même âge.

public class Personne {
    private String nom;
    private String prenom;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Personne autre = (Personne) o;
        return age == autre.age
            && Objects.equals(nom, autre.nom)
            && Objects.equals(prenom, autre.prenom);
    }
}

2.8.3 – == et equals()

Ne confondez pas ces deux outils :

Opérateur / méthode Compare
== Les références : est-ce le même objet en mémoire ?
equals Le contenu : selon la règle que vous avez définie
Règle pratique : pour comparer la signification de vos objets, utilisez equals. Réservez == au test « est-ce exactement la même instance ? ».

2.8.4 – hashCode() : principe

hashCode() renvoie un entier associé à l’objet. Pour l’instant, retenez surtout le contrat suivant : si deux objets sont égaux avec equals, ils doivent avoir le même hashCode.

Les collections comme HashMap s’appuient sur ce contrat (vous le reverrez au chapitre 4).

public class Personne {
    private String nom;
    private String prenom;
    private int age;

    @Override
    public int hashCode() {
        return Objects.hash(nom, prenom, age);
    }
}
En pratique : dès que vous redéfinissez equals, redéfinissez aussi hashCode de façon cohérente.

2.9enum avancé

Au chapitre 1, vous avez vu l’enum comme une liste de valeurs fixes. Ici, nous allons plus loin : un enum peut aussi porter des données et des méthodes, presque comme une petite classe.

2.9.1 – Attributs et constructeur

Chaque constante de l’enum peut avoir ses propres informations. Par exemple, un statut peut avoir un libellé lisible : « Étudiant », « Salarié »…

Le constructeur de l’enum est implicitement private. Vous ne faites pas de new Statut(...) : les constantes sont créées une fois pour toutes dans l’enum.

public enum Statut {
    ETUDIANT("Étudiant"),
    SALARIE("Salarié"),
    RETRAITE("Retraité");

    private final String libelle;

    Statut(String libelle) {
        this.libelle = libelle;
    }

    public String getLibelle() {
        return libelle;
    }
}

2.9.2 – Méthodes d’un enum

Vous appelez les méthodes sur une constante, comme sur un objet : Statut.ETUDIANT.getLibelle(). Vous pouvez aussi ajouter vos propres méthodes métier.

public enum Statut {
    ETUDIANT("Étudiant"),
    SALARIE("Salarié");

    private final String libelle;

    Statut(String libelle) {
        this.libelle = libelle;
    }

    public String description() {
        return name() + " → " + libelle;
    }
}

public class MainClass {
    public static void main(String[] args) {
        System.out.println(Statut.SALARIE.description());
        // SALARIE → Salarié
    }
}

2.10Synthèse et exercice pratique

2.10.1 – Création d’un modèle simple

Il est temps de rassembler les idées du chapitre. Vous allez construire un petit modèle cohérent : une classe Personne encapsulée, liée à une Adresse, avec toString, equals et hashCode.

2.10.2 à 2.10.5 – Cahier des charges

Exercice de synthèse

  1. Modèle : créez Adresse ((ville, rue) et Personne (nom, prenom, age, adresse).
  2. Objets : instanciez deux personnes (par exemple Youssef Alaoui et Fatima Benjelloun).
  3. Encapsulation : attributs private, getters / setters avec contrôle de l’âge (≥ 0).
  4. Relation : composition Personne → Adresse (l’adresse est créée avec la personne).
  5. Object : redéfinissez toString et equals (deux personnes égales si même nom, prénom et âge). Ajoutez un hashCode cohérent.

En résumé