Précédent Remonter Suivant

Chapitre 4  Programmation objet

The ``global'' view of systems can be likened to that of the creator of a system who must know all interfaces and system types. In contrast, objects in a system are like Plato's cave-dwellers who can interact with the universe in which they live only in terms of observable communications, represented by the shadows on the walls of their cave. Creators of a system are concerned with classification while objects that inhabit a system are concerned primarily with communication. Peter Wegner
L'école de programmation Algol (cf. chapitre A) a été la première à proposer une approche de la programmation qui est devenue classique : un programme est considéré comme un ensemble de procédures et un ensemble de données, séparé, sur lequel agissent ces procédures. Ce principe a parfois été résumé par une équation célèbre : Programmes = Algorithmes + Structures de données. Les méthodes d'analyse qui vont de pair consistent à diviser pour régner, c'est-à-dire à découper la tâche à effectuer en un ensemble de modules indépendants, considérés comme des boîtes noires.

Cette technique a fait ses preuves depuis longtemps et continue à être à la base de la programmation structurée. Mais elle atteint ses limites lorsque l'univers sur lequel opèrent les programmes évolue, ce qui est en fait la régle générale plutôt que l'exception : de nouveaux types de données doivent être pris en compte, le contexte applicatif dans lequel s'inscrit le programme change, les utilisateurs du programme demandent de nouvelles fonctionnalités et l'interopérabilité du programme avec d'autres programmes, etc. Or dans un langage comme Pascal ou C, les applications sont découpées en procédures et en fonctions ; cela permet une bonne décomposition des traitements à effectuer, mais le moindre changement de la structuration des données est difficile à mettre en oeuvre, et peut entraîner de profonds bouleversements dans l'organisation de ces procédures et fonctions.

C'est ici que la programmation objet apporte un "plus" fondamental, grâce à la notion d'encapsulation : les données et les procédures qui manipulent ces données sont regroupées dans une même entité, appelée l'objet. Les détails d'implantation de l'objet restent cachés : le monde extérieur n'a accès à ses données que par l'intermédiaire d'un ensemble d'opérations qui constituent l'interface de l'objet. Le programmeur qui utilise un objet dans son programme n'a donc pas à se soucier de sa représentation physique ; il peut raisonner en termes d'abstractions.

Java est l'un des principaux représentants actuels de la famille des langages à objets. Dans ce chapitre, nous allons nous familiariser progressivement avec les principales caractéristiques de cette famille, en nous appuyant comme dans les chapitres précédents sur le langage Java pour illustrer notre propos. Mais il faut savoir qu'à certains choix conceptuels près, on retrouvera le même style de programmation dans d'autres langages de la famille.

4.1  Retour sur la classe

Nous avons déjà introduit la classe comme permettant de regrouper des variables pour former une même entité. Mais en fait, la classe est plus que cela : elle est la description d'une famille d'objets ayant même structure et même comportement. Elle permet donc non seulement de regrouper un ensemble de données, mais aussi les procédures et fonctions qui agissent sur ces données. Dans le vocabulaire de la programmation objet, ces procédures et fonctions sont appelées les méthodes ; elles représentent le comportement commun de tous les objets appartenant à la classe.

Revenons à notre exemple bancaire pour illustrer notre propos. Nous avions défini au § 3.1 une classe CompteBancaire qui contient quatre variables. Par ailleurs, nous avions défini au § 3.2.3 une procédure étatCompte dans la classe Banque ; mais celle-ci accède directement aux variables de la classe CompteBancaire, ce qui est contraire au principe d'encapsulation précédemment énoncé. De même, si on veut vraiment considérer CompteBancaire comme une "boîte noire", dans laquelle les détails d'implantation sont cachés, il n'est pas judicieux de laisser le programme de la classe Banque accéder directement à la variable interne solde, pour l'incrémenter ou la décrémenter. Plus généralement, on souhaite avoir le contrôle sur les opérations autorisées sur les attributs internes d'une classe, ce qui est impossible à réaliser si les programmes du "monde extérieur à la classe" peuvent accéder directement à ces attributs.

Nous aboutissons donc à la spécification de l'interface souhaitable pour un objet de type compte bancaire : D'autres méthodes pourront bien entendu venir compléter cette première interface rudimentaire. À partir de là, écrivons une nouvelle version de la classe CompteBancaire ; notez bien la manière de "penser" quand on écrit une méthode, par exemple créditer : "je" reçois en argument un montant, et j'incrémente "mon" solde de ce montant. Notez aussi que pour interdire l'accès direct aux variables internes, on fait précéder leur déclaration du mot clé private, les méthodes, elles, étant caractérisées par le mot clé public, qui indique qu'elles sont accessibles de l'extérieur de l'objet.
// Classe CompteBancaire - version 2.0
/**
 * Classe représentant un compte bancaire et les méthodes qui lui
 * sont associées.
 * @author Karl Tombre
 */

public class CompteBancaire {
    private String nom;       // le nom du client
    private String adresse;   // son adresse
    private int numéro;       // numéro du compte
    private int solde;        // solde du compte
    
    // Les méthodes
    public void créditer(int montant) {
        solde += montant;
    }
    public void débiter(int montant) {
        solde -= montant;
    }
    public void afficherEtat() {
        System.out.println("Compte numéro " + numéro + 
                           " ouvert au nom de " + nom);
        System.out.println("Adresse du titulaire : " + adresse);
        System.out.println("Le solde actuel du compte est de " +
                           solde + " euros.");
        System.out.println("************************************************");
    }
}
Nous avons déjà vu qu'il y a un lien fort entre les notions de classe et de type de données. Le type en lui-même peut être vu comme l'interface de la classe, c'est-à-dire la spécification -- ou la signature -- des opérations valides sur les objets de la classe, alors que la classe elle-même est une mise en oeuvre concrète de cette interface.

4.1.1  Fonctions d'accès

Une conséquence de l'encapsulation est que les variables d'instance nom, adresse, numéro et solde, sont maintenant cachées au monde extérieur. Malgré tous les avantages de cet état de fait, on peut avoir besoin de rendre certaines de ces informations accessibles, ne serait-ce qu'en lecture. Faudrait-il pour autant les rendre à nouveau publiques dans ce cas ? Non ! La solution est alors de munir l'interface de la classe de fonctions d'accès en lecture et/ou en écriture. Ainsi, si je veux permettre aux utilisateurs de la classe de consulter en lecture uniquement le nom et l'adresse du titulaire du compte, je pourrais définir dans la classe CompteBancaire les méthodes suivantes :
    public String getNom() {
        return nom;
    }
    public String getAdresse() {
        return adresse;
    }
L'avantage, par rapport au fait de rendre les variables publiques, est qu'on ne permet que l'accès en lecture, pas en écriture (c'est-à-dire en modification). Et même si on voulait également permettre l'accès en écriture à l'adresse, par exemple, il faut définir la fonction d'accès
    public void setAdresse(String nouvelleAdresse) {
        adresse = nouvelleAdresse;
    }
plutôt que de rendre la variable publique. En effet, dans une vraie application, on souhaitera probablement vérifier la cohérence de l'adresse, mettre à jour certaines tables statistiques de l'agence, voire activer l'impression d'un nouveau chéquier, par exemple, toutes choses qui sont possibles dans une méthode telle que setAdresse, mais qui sont impossibles à contrôler si le "monde extérieur" peut directement modifier la valeur de la variable adresse.

Le fait de munir ses classes de fonctions d'accès, même triviales, au lieu de rendre certaines variables publiques, est donc une "bonne pratique" en programmation qu'il vaut mieux acquérir tout de suite, même si cela peut parfois sembler fastidieux ou inutile sur les exemples simplistes que nous utilisons en cours. Vous pouvez voir illustré l'ajout de telles fonctions à la classe CompteBancaire au § 6.6, quand nous en aurons besoin pour définir une interface utilisateur. Jusqu'à ce moment-là, nous nous en passerons pour ne pas alourdir les exemples...

4.2  Les objets

Comme nous l'avons vu au § 3.1.1, une classe peut être considérée comme un "moule", et on crée des objets par "moulage" à partir du modèle donné par la classe. Cette opération, qui se fait grâce à l'instruction new déjà vue, s'appelle l'instanciation, car l'objet créé est dit être une instance de sa classe. Celle-ci permet donc de reproduire autant d'exemplaires -- d'instances -- que nécessaire.

Chaque instance ainsi créée possède son propre exemplaire de chacune des variables définies dans la classe ; on les appelle ses variables d'instance. Ainsi, si je crée deux objets de type CompteBancaire, ils auront chacun leur propre variable solde, leur propre variable nom, etc.

Se pose maintenant le problème de l'initialisation de ces variables, au moment de la création. Jusqu'à maintenant, dans notre programme bancaire, nous initialisions ces variables après la création par new, par simple affectation de leur valeur. Mais le principe d'encapsulation que nous avons adopté, et la protection que nous avons donnée aux variables par le mot clé private, nous interdit maintenant d'accéder directement à ces variables pour leur affecter une valeur.

Il nous faut donc un mécanisme spécifique d'initialisation ; en Java, ce mécanisme s'appelle un constructeur. Un constructeur est une procédure d'initialisation ; il porte toujours le même nom que la classe pour laquelle il est défini -- sans attributs de type --, et on peut lui donner les paramètres que l'on veut. Modifions donc notre classe CompteBancaire pour lui ajouter un constructeur :
// Classe CompteBancaire - version 2.1

/**
 * Classe représentant un compte bancaire et les méthodes qui lui
 * sont associées.
 * @author Karl Tombre
 */

public class CompteBancaire {
    private String nom;       // le nom du client
    private String adresse;   // son adresse
    private int numéro;       // numéro du compte
    private int solde;        // solde du compte
    
    // Constructeur : on reçoit en paramètres les valeurs du nom,
    // de l'adresse et du numéro, et on met le solde à 0 par défaut
    CompteBancaire(String unNom, String uneAdresse, int unNuméro) {
        nom = unNom;
        adresse = uneAdresse;
        numéro = unNuméro;
        solde = 0;
    }

    // Les méthodes
    public void créditer(int montant) {
        solde += montant;
    }
    public void débiter(int montant) {
        solde -= montant;
    }
    public void afficherEtat() {
        System.out.println("Compte numéro " + numéro + 
                           " ouvert au nom de " + nom);
        System.out.println("Adresse du titulaire : " + adresse);
        System.out.println("Le solde actuel du compte est de " +
                           solde + " euros.");
        System.out.println("************************************************");
    }
}
Il est temps d'utiliser des objets de cette classe dans notre programme bancaire, que nous devons bien évidemment modifier pour tenir compte de l'approche objet que nous avons prise. Vous pouvez noter plusieurs choses dans l'exemple qui suit :
// Classe Banque - version 3.0

/**
 * Classe contenant un programme de gestion bancaire, utilisant
 * un tableau de comptes bancaires
 * @author Karl Tombre
 * @see    CompteBancaire, Utils
 */

public class Banque {
    public static void main(String[] args) {
        CompteBancaire[] tabComptes = new CompteBancaire[10];

        // Initialisation des comptes
        for (int i = 0 ; i < tabComptes.length ; i++) {
            // Lire les valeurs d'initialisation
            String monNom = Utils.lireChaine("Nom du titulaire = ");
            String monAdresse = Utils.lireChaine("Son adresse = ");
            int monNuméro = Utils.lireEntier("Numéro du compte = ");
            // Créer le compte -- notez la syntaxe avec new
            tabComptes[i] = new CompteBancaire(monNom, monAdresse, monNuméro);
        }

        boolean fin = false;     // variable vraie si on veut s'arrêter
        while(true) {  // boucle infinie dont on sort par un break
            String choix = Utils.lireChaine("Votre choix : [D]ébit, [C]rédit, [F]in ? ");
            boolean credit = false;   // variable vraie si c'est un crédit

            // Récupérer la première lettre de la chaîne saisie
            char monChoix = choix.charAt(0);
            switch(monChoix) {
            case 'C':
            case 'c':       // Même chose pour majuscule et minuscule
                credit = true; 
                fin = false;
                break;     // Pour ne pas continuer en séquence
            case 'd':
            case 'D':
                credit = false;
                fin = false;
                break;
            case 'f':
            case 'F':
                fin = true;
                break;
            default:
                fin = true;  // On va considérer que par défaut on s'arrête
            }

            if (fin) {
                break;    // sortir de la boucle ici
            }
            else {
                int indice = Utils.lireEntier("Indice dans le tableau des comptes = ");
                int montant = Utils.lireEntier("Montant à " + 
                                               (credit ? "créditer" : "débiter") + 
                                               " = ");
                if (credit) {
                    tabComptes[indice].créditer(montant);
                }
                else {
                    tabComptes[indice].débiter(montant);
                }
                // Afficher le nouveau solde
                tabComptes[indice].afficherEtat();
            }
        }
    }
}
Pour conclure ce paragraphe, insistons à nouveau sur le point suivant :
Exercice 6   On souhaite écrire un programme Java qui gère un panier d'emplettes pour un site de commerce électronique. On représentera un item des emplettes par une classe Item dont un embryon est donné ci-dessous ; compléter cette classe en écrivant le corps du constructeur et de la méthode prix.

public class Item {
    private String nom;
    private int quantité;
    private double prixUnitaire;

    Item(String n, int q, double pu) {
        // À faire
    }

    public double prix() {
        // À faire
    }
}

4.3  Méthodes et variables de classe

Nous avons vu que les méthodes et les variables d'instance sont propres à chaque instance ; ainsi, chaque objet de type CompteBancaire possède son propre exemplaire de la variable solde -- ce qui permet fort heureusement à chaque titulaire de compte de disposer de son propre solde et non d'un solde commun !

Mais il peut être parfois nécessaire de disposer de méthodes et de variables qui soient propres à la classe, et non aux instances, et qui n'existent donc qu'en exemplaire unique, quel que soit le nombre d'instances. On les appelle des méthodes et variables de classe, et elles sont introduites par le mot clé static.

Illustrons tout de suite notre propos. Nous souhaitons modifier la classe CompteBancaire de manière à attribuer automatiquement le numéro de compte, par incrémentation d'une variable de classe, que nous appellerons premierNuméroDisponible. Celle-ci est initialisée à 11, et chaque fois qu'on crée un nouveau compte -- concrètement, dans le constructeur de la classe -- elle est incrémentée :
// Classe CompteBancaire - version 2.2

/**
 * Classe représentant un compte bancaire et les méthodes qui lui
 * sont associées.
 * @author Karl Tombre
 */

public class CompteBancaire {
    private String nom;       // le nom du client
    private String adresse;   // son adresse
    private int numéro;       // numéro du compte
    private int solde;        // solde du compte

    // Variable de classe
    public static int premierNuméroDisponible = 1;
    
    // Constructeur : on reçoit en paramètres les valeurs du nom et
    // de l'adresse, on met le solde à 0 par défaut, et on récupère
    // automatiquement un numéro de compte
    CompteBancaire(String unNom, String uneAdresse) {
        nom = unNom;
        adresse = uneAdresse;
        numéro = premierNuméroDisponible;
        solde = 0;
        // Incrémenter la variable de classe
        premierNuméroDisponible++;
    }

    // Les méthodes
    public void créditer(int montant) {
        solde += montant;
    }
    public void débiter(int montant) {
        solde -= montant;
    }
    public void afficherEtat() {
        System.out.println("Compte numéro " + numéro + 
                           " ouvert au nom de " + nom);
        System.out.println("Adresse du titulaire : " + adresse);
        System.out.println("Le solde actuel du compte est de " +
                           solde + " euros.");
        System.out.println("************************************************");
    }
}
Comme nous avons modifié la signature du constructeur, nous devons aussi modifier légèrement notre programme bancaire ; je ne donne ici que les premières lignes de la nouvelle mouture, le reste ne changeant pas. Notez que l'on ne demande plus que le nom et l'adresse :
// Classe Banque - version 3.1

/**
 * Classe contenant un programme de gestion bancaire, utilisant
 * un tableau de comptes bancaires
 * @author Karl Tombre
 * @see    CompteBancaire, Utils
 */

public class Banque {
    public static void main(String[] args) {
        CompteBancaire[] tabComptes = new CompteBancaire[10];

        // Initialisation des comptes
        for (int i = 0 ; i < tabComptes.length ; i++) {
            // Lire les valeurs d'initialisation
            String monNom = Utils.lireChaine("Nom du titulaire = ");
            String monAdresse = Utils.lireChaine("Son adresse = ");
            // Créer le compte -- notez la syntaxe avec new
            tabComptes[i] = new CompteBancaire(monNom, monAdresse);
        }

        // etc.
À part la modification sur le nombre de questions posées à l'utilisateur en phase de création du tableau, le programme a le même comportement qu'avant :
Nom du titulaire = Karl
Son adresse = Sornéville
Nom du titulaire = Luigi
Son adresse = Rome

\emph{... Je vous passe un certain nombre de lignes - c'est fastidieux}

Nom du titulaire = Robert
Son adresse = Vandoeuvre
Votre choix : [D]ébit, [C]rédit, [F]in ? D
Indice dans le tableau des comptes = 3
Montant à débiter = 123
Compte numéro 4 ouvert au nom de François
Adresse du titulaire : Strasbourg
Le solde actuel du compte est de -123 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? C
Indice dans le tableau des comptes = 0
Montant à créditer = 9800
Compte numéro 1 ouvert au nom de Karl
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 9800 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? F

4.3.1  Retour sur la procédure main

Vous avez maintenant les éléments vous permettant de mieux comprendre la syntaxe a priori rébarbative que nous vous avons imposée dès le premier programme, à savoir public static void main(String[] args) :

4.3.2  Comment accéder aux variables et méthodes de classe ?

Nous ne nous sommes pas posés pour l'instant la question de l'accès aux variables et méthodes de classe, car jusqu'à maintenant nous ne les avons utilisées qu'au sein de la classe dans laquelle elles sont définies, et la notation directe s'applique donc, comme pour les méthodes et variables d'instance. Mais la variable de classe premierNuméroDisponible ayant été déclarée publique, on pourrait imaginer que l'on souhaite y accéder directement depuis un programme ou une autre classe. Dans un programme extérieur, il n'est pas pour autant conseillé d'écrire2 :
CompteBancaire c = new CompteBancaire("Capitaine Haddock", "Boucherie Sanzot");
System.out.println("Le premier numéro disponible est : " + c.premierNuméroDisponible);
car premierNuméroDisponible appartient à la classe, et non à l'instance c. Il vaut donc mieux utiliser la notation pointée en précisant que c'est à la classe qu'appartient la variable :
System.out.println("Le premier numéro disponible est : " + 
                   CompteBancaire.premierNuméroDisponible);
C'est d'ailleurs ce que nous ferons lorsque nous aurons besoin de sauvegarder cette variable dans un fichier (§ 6.3.2).

Vous avez d'ailleurs déjà utilisé -- sans le savoir -- des variables de classe dans les programmes vus jusqu'à maintenant. Par exemple, System.out désigne la variable de classe out de la classe System, comme nous le verrons au § 6.3.1.

Les fonctions lireChaine et lireEntier sont également des méthodes de classe que nous avons définies dans la classe Utils, et que nous avons utilisées en employant la syntaxe que nous venons de voir, à savoir Utils.lireChaine.

4.4  Exemple d'une classe prédéfinie : String

La chaîne de caractères est une structure de données très fréquente dans les langages de programmation. Dans beaucoup de langages, elle est définie comme un tableau de caractères. Mais en Java, c'est l'approche objet qui est employée, et les bibliothèques standards de Java fournissent la classe String, que nous avons déjà eu l'occasion d'utiliser.

String fournit les opérations courantes sur les chaînes, comme nous allons le voir dans ce paragraphe. Ces opérations sont disponibles sous forme de méthodes définies dans la classe String.

Mais String a aussi une particularité. C'est la seule classe pour laquelle un opérateur est redéfini3 : nous avons déjà eu l'occasion d'utiliser l'opérateur + sur des chaînes de caractères, et nous avons vu qu'il permet la concaténation de deux chaînes, comme dans :
"Adresse du titulaire : " + adresse.

Par ailleurs, Java prévoit une autre facilité syntaxique pour les chaînes de caractères, à savoir l'emploi des guillemets ("). Quand nous écrivons String salutation = "bonjour"; nous créons en fait une constante de type String, qui vaut "bonjour". L'affectation de celle-ci à la variable salutation revient tout simplement à faire "pointer" l'adresse contenue dans celle-ci sur la "case" où est stockée cette constante -- souvenez-vous que les variables de types définis par des classes sont toujours des références (cf. § 3.1.1). On pourrait bien sûr recourir plus classiquement à un constructeur, comme pour toute autre classe, en écrivant String salutation = new String("bonjour"); mais ce serait moins efficace, puisqu'on crée dans ce cas deux objets de type String, la constante "bonjour" puis la nouvelle chaîne via le constructeur.

Sans chercher à être exhaustifs, nous donnons ici une liste très partielle des méthodes disponibles dans la classe String ; comme pour toutes les autres classes des bibliothèques de base de Java, la documentation complète est disponible à partir de ma page web à l'école (rubrique APIs de Java). J'indique à chaque fois la signature des méthodes ; pour compareTo par exemple, cette signature est int compareTo(String), ce qui signifie qu'on l'utilisera de la manière suivante :
String s = ....;
String t = ...;
if (s.compareTo(t) > 0) {
  System.out.println("La chaîne " + s + " est supérieure à la chaîne " + t);
}

Méthode Description
char charAt(int) Rend le caractère à la position indiquée. Nous avons déjà utilisé cette méthode.
int compareTo(String) Comparaison lexicographique avec la chaîne donnée en paramètre. Rend 0 si les deux chaînes sont égales, un nombre négatif si la chaîne est inférieure à celle donnée en paramètre, un nombre positif si elle est supérieure.
boolean endsWith(String) Teste si la chaîne se termine par le suffixe donné en paramètre.
boolean equals(Object) Teste l'égalité avec un autre objet. Nous avons déjà utilisé cette méthode.
boolean equalsIgnoreCase(String) Teste l'égalité avec une autre chaîne sans tenir compte des majuscules/minuscules.
int lastIndexOf(String) Rend l'index dans la chaîne de la dernière position de la sous-chaîne donnée en paramètre.
int length() Rend la longueur de la chaîne.
String toLowerCase() Conversion de la chaîne en minuscules.
String toUpperCase() Conversion de la chaîne en majuscules.

4.4.1  Application : recherche plus conviviale du compte

Nous allons appliquer l'interface que nous venons de voir à notre programme bancaire. En effet, jusqu'à maintenant, nous demandions à l'utilisateur de donner l'indice dans le tableau des comptes, ce qui est peu naturel. Nous allons maintenant retrouver le compte dans le tableau par simple indication du nom du titulaire. Il faudra alors parcourir le tableau pour chercher un compte dont le titulaire correspond au nom cherché. Nous permettrons à l'utilisateur de ne pas tenir compte des majuscules ou des minuscules, en utilisant la méthode equalsIgnoreCase

En préalable, notons que nous n'avons pas pour l'instant de moyen d'accéder au nom du titulaire du compte, puisque la variable nom est privée. Nous pourrions la déclarer publique, mais ce serait contraire au principe d'encapsulation ; préférons donc définir des méthodes d'accès en lecture au nom et à l'adresse du titulaire du compte. Notez bien que si j'avais déclaré ces variables publiques, je pourrais les lire, mais aussi les modifier depuis l'extérieur de la classe, alors qu'avec la solution préconisée, je ne permets que l'accès en lecture :
// Classe CompteBancaire - version 2.3

/**
 * Classe représentant un compte bancaire et les méthodes qui lui
 * sont associées.
 * @author Karl Tombre
 */

public class CompteBancaire {
    private String nom;       // le nom du client
    private String adresse;   // son adresse
    private int numéro;       // numéro du compte
    private int solde;        // solde du compte

    // Variable de classe
    public static int premierNuméroDisponible = 1;
    
    // Constructeur : on reçoit en paramètres les valeurs du nom et
    // de l'adresse, on met le solde à 0 par défaut, et on récupère
    // automatiquement un numéro de compte
    CompteBancaire(String unNom, String uneAdresse) {
        nom = unNom;
        adresse = uneAdresse;
        numéro = premierNuméroDisponible;
        solde = 0;
        // Incrémenter la variable de classe
        premierNuméroDisponible++;
    }

    // Les méthodes
    public void créditer(int montant) {
        solde += montant;
    }
    public void débiter(int montant) {
        solde -= montant;
    }
    public void afficherEtat() {
        System.out.println("Compte numéro " + numéro + 
                           " ouvert au nom de " + nom);
        System.out.println("Adresse du titulaire : " + adresse);
        System.out.println("Le solde actuel du compte est de " +
                           solde + " euros.");
        System.out.println("************************************************");
    }
    // Accès en lecture
    public String nom() {
        return this.nom;
    }
    public String adresse() {
        return this.adresse;
    }
}
Vous remarquerez que j'ai donné à ces méthodes les mêmes noms qu'aux variables auxquelles elles accèdent. Dans le corps des méthodes, j'utilise le mot clé this, qui référencie toujours l'objet courant -- "moi-même", qui possède les variables et les instances4 -- et la notation this.nom indique donc "ma variable privée nom". Ceci n'était pas stricto sensu nécessaire dans le cas présent, car il n'y a pas ambiguité, mais cela augmente la lisibilité du programme quand il y a deux attributs de même nom.

Voici le programme bancaire modifié ; vous noterez l'emploi de la méthode equalsIgnoreCase de la classe String :
// Classe Banque - version 3.2

/**
 * Classe contenant un programme de gestion bancaire, utilisant
 * un tableau de comptes bancaires
 * @author Karl Tombre
 * @see    CompteBancaire, Utils
 */

public class Banque {
    public static void main(String[] args) {
        CompteBancaire[] tabComptes = new CompteBancaire[10];

        // Initialisation des comptes
        for (int i = 0 ; i < tabComptes.length ; i++) {
            // Lire les valeurs d'initialisation
            String monNom = Utils.lireChaine("Nom du titulaire = ");
            String monAdresse = Utils.lireChaine("Son adresse = ");
            // Créer le compte -- notez la syntaxe avec new
            tabComptes[i] = new CompteBancaire(monNom, monAdresse);
        }

        boolean fin = false;     // variable vraie si on veut s'arrêter
        while(true) {  // boucle infinie dont on sort par un break
            String choix = Utils.lireChaine("Votre choix : [D]ébit, [C]rédit, [F]in ? ");
            boolean credit = false;   // variable vraie si c'est un crédit

            // Récupérer la première lettre de la chaîne saisie
            char monChoix = choix.charAt(0);
            switch(monChoix) {
            case 'C':
            case 'c':       // Même chose pour majuscule et minuscule
                credit = true; 
                fin = false;
                break;     // Pour ne pas continuer en séquence
            case 'd':
            case 'D':
                credit = false;
                fin = false;
                break;
            case 'f':
            case 'F':
                fin = true;
                break;
            default:
                fin = true;  // On va considérer que par défaut on s'arrête
            }

            if (fin) {
                break;    // sortir de la boucle ici
            }
            else {
                // Demander un nom
                String nomAChercher = Utils.lireChaine("Nom du client = ");
                boolean trouvé = false;  // rien trouvé pour l'instant
                int indice = 0;
                for (int i = 0 ; i < tabComptes.length ; i++) {
                    if (nomAChercher.equalsIgnoreCase(tabComptes[i].nom())) {
                        trouvé = true; // j'ai trouvé
                        indice = i;    // mémoriser l'indice
                        break;         // plus besoin de continuer la recherche
                    }
                }

                if (trouvé) { // Si j'ai trouvé quelque chose
                    int montant = Utils.lireEntier("Montant à " + 
                                                   (credit ? "créditer" : "débiter") + 
                                                   " = ");
                    if (credit) {
                        tabComptes[indice].créditer(montant);
                    }
                    else {
                        tabComptes[indice].débiter(montant);
                    }
                    // Afficher le nouveau solde
                    tabComptes[indice].afficherEtat();
                }
                else {
                    System.out.println("Désolé, " + nomAChercher + 
                                       " ne fait pas (encore) partie de nos clients.");
                }
            }
        }
    }
}
Et voici une trace d'exécution de cette nouvelle version :
Nom du titulaire = Karl Tombre
Son adresse = Sornéville
Nom du titulaire = Luigi Liquori
Son adresse = Rome

\emph{... Je vous passe un certain nombre de lignes - c'est fastidieux}

Nom du titulaire = Bill Clinton
Son adresse = Washington
Votre choix : [D]ébit, [C]rédit, [F]in ? c
Nom du client = Karl Tombre
Montant à créditer = 4500
Compte numéro 1 ouvert au nom de Karl Tombre
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 4500 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? D
Nom du client = Jacques Chirac
Désolé, Jacques Chirac ne fait pas (encore) partie de nos clients.
Votre choix : [D]ébit, [C]rédit, [F]in ? F
Ce programme souffre encore de plusieurs faiblesses, ou disons de manques que je vous laisse compléter en exercice5 :

4.5  La composition : des objets dans d'autres objets

Le programme bancaire commence à devenir complexe ; on peut relever qu'il mèle dans la même procédure main des éléments d'interface homme--machine (les dialogues avec l'utilisateur) et la gestion du tableau des comptes. De plus, il est assez rigide, car il fixe à exactement 10 le nombre de comptes à gérer. Il est donc temps de modéliser par une classe à part entière la notion d'agence bancaire, qui pour l'instant est juste représenté par le tableau. Cela nous donnera l'occasion de composer un objet (l'agence bancaire) à partir d'autres objets (les comptes individuels)6.

Spécifions une version sommaire de l'interface d'une agence bancaire. Il faut être capable de : Les choix de représentation interne sont les suivants : Ceci nous conduit à la classe AgenceBancaire suivante :
// Classe AgenceBancaire - version 1.0

/**
 * Classe représentant une agence bancaire et les méthodes qui lui
 * sont associées.
 * @author Karl Tombre
 */

public class AgenceBancaire {
    private CompteBancaire[] tabComptes;  // le tableau des comptes
    private int capacitéCourante;         // la capacité du tableau
    private int nbComptes;                // le nombre effectif de comptes

    // Constructeur -- au moment de créer une agence, on crée un tableau
    // de capacité initiale 10, mais qui ne contient encore aucun compte
    AgenceBancaire() {
        tabComptes = new CompteBancaire[10];
        capacitéCourante = 10;
        nbComptes = 0;
    }

    // Méthode privée utilisée pour incrémenter la capacité
    private void augmenterCapacité() {
        // Incrémenter la capacité de 10
        capacitéCourante += 10;
        // Créer un nouveau tableau plus grand que l'ancien
        CompteBancaire[] tab = new CompteBancaire[capacitéCourante];
        // Recopier les comptes existants dans ce nouveau tableau
        for (int i = 0 ; i < nbComptes ; i++) {
            tab[i] = tabComptes[i];
        }
        // C'est le nouveau tableau qui devient le tableau des comptes
        // (l'ancien sera récupéré par le ramasse-miettes)
        tabComptes = tab;
    }

    // Les méthodes de l'interface
    // Ajout d'un nouveau compte
    public void ajout(CompteBancaire c) {
        if (nbComptes == capacitéCourante) { // on a atteint la capacité max
            augmenterCapacité();
        }
        // Maintenant je suis sûr que j'ai de la place
        // Ajouter le nouveau compte dans la première case vide
        // qui porte le numéro nbComptes !
        tabComptes[nbComptes] = c;
        // On prend note qu'il y a un compte de plus
        nbComptes++;
    }
    // Récupérer un compte à partir d'un nom donné
    public CompteBancaire trouverCompte(String nom) {
        boolean trouvé = false;  // rien trouvé pour l'instant
        int indice = 0;
        for (int i = 0 ; i < nbComptes ; i++) {
            if (nom.equalsIgnoreCase(tabComptes[i].nom())) {
                trouvé = true; // j'ai trouvé
                indice = i;    // mémoriser l'indice
                break;         // plus besoin de continuer la recherche
            }
        }
        if (trouvé) {
            return tabComptes[indice];
        }
        else {
            return null;  // si rien trouvé, je rend la référence nulle
        }
    }
    // Afficher l'état de tous les comptes
    public void afficherEtat() {
        // Il suffit d'afficher l'état de tous les comptes de l'agence
        for (int i = 0 ; i < nbComptes ; i++) { 
            tabComptes[i].afficherEtat();
        }
    }
}
Nous allons maintenant construire une nouvelle version de notre programme bancaire. Profitons de cette "mise à plat" pour modifier un peu les dialogues et leur traitement. Vous remarquerez que maintenant, ce programme ne contient plus que des instructions de dialogue d'un côté, et des appels à l'interface des classes CompteBancaire et GestionBancaire de l'autre ; nous avons donc bien séparé ces deux aspects, ce qui rend le programme plus lisible :
// Classe Banque - version 4.0

/**
 * Classe contenant un programme de gestion bancaire, utilisant
 * un objet de type AgenceBancaire
 * @author Karl Tombre
 * @see    AgenceBancaire, CompteBancaire, Utils
 */

public class Banque {
    public static void main(String[] args) {
        AgenceBancaire monAgence = new AgenceBancaire();

        while (true) { // boucle infinie dont on sort par un break
            String monNom = Utils.lireChaine("Donnez le nom du client (rien=exit) : ");
            if (monNom.equals("")) {
                break;  // si on ne donne aucun nom on quitte la boucle
            }
            else {
                // Vérifier si le compte existe
                CompteBancaire monCompte = monAgence.trouverCompte(monNom);
                if (monCompte == null) {
                    // rien trouvé, on le crée
                    System.out.println("Ce compte n'existe pas, nous allons le créer");
                    String monAdresse = Utils.lireChaine("Adresse = ");
                    // Créer le compte
                    monCompte = new CompteBancaire(monNom, monAdresse);
                    // L'ajouter aux comptes de l'agence
                    monAgence.ajout(monCompte);
                }

                String choix = Utils.lireChaine("Votre choix : [D]ébit, [C]rédit ? ");
                boolean credit = false;   // variable vraie si c'est un crédit

                // Récupérer la première lettre de la chaîne saisie
                char monChoix = choix.charAt(0);
                if (monChoix == 'c' || monChoix == 'C') {
                    int montant = Utils.lireEntier("Montant à créditer = ");
                    monCompte.créditer(montant);
                }
                else if (monChoix == 'd' || monChoix == 'D') {
                    int montant = Utils.lireEntier("Montant à débiter = ");
                    monCompte.débiter(montant);
                }
                else {
                    System.out.println("Choix invalide");
                }
                // Dans tous les cas, afficher l'état du compte
                monCompte.afficherEtat();
            }
        }
        // Quand on sort de la boucle, afficher l'état global de l'agence
        System.out.println("Voici le nouvel état des comptes de l'agence");
        monAgence.afficherEtat();
    }
}
Voici une trace d'exécution de ce programme :
Donnez le nom du client (rien=exit) : \emph{Karl Tombre}
Ce compte n'existe pas, nous allons le créer
Adresse = \emph{Sornéville}
Votre choix : [D]ébit, [C]rédit ? \emph{D}
Montant à débiter = \emph{1000}
Compte numéro 1 ouvert au nom de Karl Tombre
Adresse du titulaire : Sornéville
Le solde actuel du compte est de -1000 euros.
************************************************
Donnez le nom du client (rien=exit) : \emph{Karl Tombre}
Votre choix : [D]ébit, [C]rédit ? \emph{C} 
Montant à créditer = \emph{2000}
Compte numéro 1 ouvert au nom de Karl Tombre
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 1000 euros.
************************************************
Donnez le nom du client (rien=exit) : \emph{Luigi Liquori}
Ce compte n'existe pas, nous allons le créer
Adresse = \emph{Rome}
Votre choix : [D]ébit, [C]rédit ? \emph{c}
Montant à créditer = \emph{8900}
Compte numéro 2 ouvert au nom de Luigi Liquori
Adresse du titulaire : Rome
Le solde actuel du compte est de 8900 euros.
************************************************
Donnez le nom du client (rien=exit) : \emph{Jacques Jaray}
Ce compte n'existe pas, nous allons le créer
Adresse = \emph{Laxou}
Votre choix : [D]ébit, [C]rédit ? \emph{F}
Choix invalide
Compte numéro 3 ouvert au nom de Jacques Jaray
Adresse du titulaire : Laxou
Le solde actuel du compte est de 0 euros.
************************************************
Donnez le nom du client (rien=exit) : \emph{kArL toMbre}
Votre choix : [D]ébit, [C]rédit ? \emph{c}
Montant à créditer = \emph{2400}
Compte numéro 1 ouvert au nom de Karl Tombre
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 3400 euros.
************************************************
Donnez le nom du client (rien=exit) : 
Voici le nouvel état des comptes de l'agence
Compte numéro 1 ouvert au nom de Karl Tombre
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 3400 euros.
************************************************
Compte numéro 2 ouvert au nom de Luigi Liquori
Adresse du titulaire : Rome
Le solde actuel du compte est de 8900 euros.
************************************************
Compte numéro 3 ouvert au nom de Jacques Jaray
Adresse du titulaire : Laxou
Le solde actuel du compte est de 0 euros.
************************************************

Exercice 7   Voici une classe Point rudimentaire :

public class Point {
    private double x;
    private double y;

    Point(double unX, double unY) {
        x = unX;
        y = unY;
    }

    public double getX() { return x; }
    public double getY() { return y; }
}
  
On souhaite définir une classe décrivant des rectangles à partir de leurs deux coins supérieur gauche et inférieur droit (dans le repère habituel en graphisme sur ordinateur, c'est à dire origine en haut à gauche, x croissant vers la droite, y croissant vers le bas). L'embryon de cette classe est donné ci-dessous :

public class Rectangle {
    private Point coinSupGauche;
    private Point coinInfDroit;

    Rectangle(Point p1, Point p2) {
        coinSupGauche = new Point(Math.min(p1.getX(), p2.getX()),
                                  Math.min(p1.getY(), p2.getY()));
        coinInfDroit = new Point(Math.max(p1.getX(), p2.getX()),
                                 Math.max(p1.getY(), p2.getY()));
    }
}  
Ajouter à cette classe les deux méthodes suivantes :

4.6  L'héritage

Quand le cahier des charges d'une application devient important, il vient un moment où il n'est plus ni pratique, ni économique de gérer tous les cas possibles dans une seule classe. Ainsi, supposons qu'il y a deux types de comptes bancaires : les comptes de dépôt et les comptes d'épargne. Pour les premiers, on permet au solde d'être négatif, mais des agios sont déduits chaque jour si le solde est négatif. Pour les seconds, le solde doit toujours rester positif, mais on ajoute des intérêts calculés chaque jour8.

On pourrait bien entendu gérer ces différents cas de figure dans une seule et même classe CompteBancaire, mais celle-ci deviendrait complexe et peu évolutive, puisqu'un grand nombre de ses méthodes devraient prévoir les différences de traitement entre comptes de dépôt et comptes d'épargne. On pourrait aussi choisir de faire deux classes indépendantes, CompteDepot et CompteEpargne, mais on est alors condamné à dupliquer un grand nombre d'informations et de traitements communs aux deux types de comptes.

C'est dans de telles situations que le recours à l'héritage est particulièrement approprié. En effet, l'héritage est un mécanisme qui permet de partager et de réutiliser des données et des méthodes. L'héritage permet d'étendre une classe existante, au lieu d'en créer une nouvelle ex nihilo, l'idée étant de tendre vers une hiérarchie de classes réutilisables.

Il s'agit en fait d'un problème de partage efficace de ressources. La classe peut en effet être considérée comme un réservoir de ressources, à partir duquel il est possible de définir d'autres classes plus spécifiques, complétant les ressources de leur "classe mère", dont elles héritent. Les ressources les plus générales sont donc mises en commun dans des classes qui sont ensuite spécialisées par la définition de sous-classes.

Une sous-classe est en effet une spécialisation de la description d'une classe, appelée sa superclasse, dont elle partage -- on dit aussi qu'elle hérite -- les variables et les méthodes. La spécialisation d'une classe peut être réalisée selon deux techniques. La première est l'enrichissement : la sous-classe est dotée de nouvelles variables et/ou de nouvelles méthodes, représentant les caractéristiques propres au sous-ensemble d'objets décrit par la sous-classe. La seconde technique est la substitution, qui consiste à donner une nouvelle définition à une méthode héritée, lorsque celle-ci se révèle inadéquate ou trop générale pour l'ensemble des objets décrits par la sous-classe.

La notion d'héritage peut être vue sous deux angles complémentaires. Quand une classe B hérite de la classe A (on dit aussi que B est dérivée de A), l'ensemble des instances de A contient celui des instances de B. Du point de vue extensionnel, la sous-classe B peut donc être considérée comme un sous-ensemble de la classe A. Mais en même temps, du fait des possibilités d'enrichissement, l'ensemble des propriétés de A est un sous-ensemble des propriétés de B ! Le mot clé utilisé en Java pour marquer l'héritage, extends, reflète d'ailleurs bien cet état de fait.

Reprenons notre nouveau cahier des charges pour illustrer cette notion. Tout d'abord, nous allons décider de faire de CompteBancaire une classe abstraite, c'est-à-dire une classe qu'il est interdit d'instancier directement. En effet, nous ne voulons avoir que des comptes de dépôt et des comptes d'épargne, et aucun compte dont la catégorie n'est pas définie. Il faut donc dans notre cas interdire à qui que ce soit de créer un objet par instanciation de CompteBancaire. Les classes abstraites sont souvent utilisées dans les langages à objets pour factoriser toutes les caractéristiques communes à un ensemble de classes, qui deviennent ensuite des sous-classes de la classe abstraite.

Le fait d'avoir une classe abstraite nous permet aussi de définir des méthodes abstraites, c'est-à-dire des méthodes dont nous définissons la signature, mais dont nous ne donnons aucune implantation dans la classe abstraite. Pour ne pas être à son tour abstraite, une sous-classe doit alors obligatoirement donner une définition de cette méthode. Aussi bien la classe abstraite que la méthode abstraite sont introduites par le mot clé abstract. Dans l'exemple donné ci-après, nous avons ajouté à CompteBancaire la méthode abstraite traitementQuotidien, qui correspond au traitement qui est supposé être effectué tous les jours -- ou plutôt toutes les nuits -- sur tous les comptes par un hypothétique programme de gestion des comptes.

Une autre modification que nous sommes amenés à effectuer concerne la protection des variables d'instance. Jusqu'à maintenant, elles étaient toutes déclarées private. Mais nous allons avoir besoin d'accéder à la variable solde dans les classes héritées ; or une variable privée n'est même pas visible dans les sous-classes qui en héritent ! D'un autre côté, nous ne souhaitons pas que solde devienne une variable publique. Java prévoit un niveau de protection intermédiaire, qui correspond à ce que nous cherchons : une variable ou une méthode protégée (mot clé protected) est privée sauf pour les sous-classes de la classe où elle est définie, ainsi que pour les classes appartenant au même package (cf. § 6.2). Les autres variables de CompteBancaire n'ont quant à elles aucune raison de ne pas rester privées.

Voici donc la nouvelle version de la classe CompteBancaire :
// Classe CompteBancaire - version 3.0

/**
 * Classe abstraite représentant un compte bancaire et les méthodes qui lui
 * sont associées.
 * @author Karl Tombre
 */

public abstract class CompteBancaire {
    private String nom;       // le nom du client
    private String adresse;   // son adresse
    private int numéro;       // numéro du compte
    protected int solde;      // solde du compte -- variable protégée

    // Variable de classe
    public static int premierNuméroDisponible = 1;
    
    // Constructeur : on reçoit en paramètres les valeurs du nom et
    // de l'adresse, on met le solde à 0 par défaut, et on récupère
    // automatiquement un numéro de compte
    CompteBancaire(String unNom, String uneAdresse) {
        nom = unNom;
        adresse = uneAdresse;
        numéro = premierNuméroDisponible;
        solde = 0;
        // Incrémenter la variable de classe
        premierNuméroDisponible++;
    }

    // Les méthodes
    public void créditer(int montant) {
        solde += montant;
    }
    public void débiter(int montant) {
        solde -= montant;
    }
    public void afficherEtat() {
        System.out.println("Compte numéro " + numéro + 
                           " ouvert au nom de " + nom);
        System.out.println("Adresse du titulaire : " + adresse);
        System.out.println("Le solde actuel du compte est de " +
                           solde + " euros.");
        System.out.println("************************************************");
    }
    // Accès en lecture
    public String nom() {
        return this.nom;
    }
    public String adresse() {
        return this.adresse;
    }

    // méthode abstraite, doit être implantée dans les sous-classes
    // traitement quotidien appliqué au compte par un gestionnaire de comptes
    public abstract void traitementQuotidien();
}
Nous allons maintenant créer les deux sous-classes CompteDepot et CompteEpargne. Examinons en détail la première :
// Classe CompteDepot - version 1.0

/**
 * Classe représentant un compte de dépôt, sous-classe de CompteBancaire.
 * @author Karl Tombre
 */

public class CompteDepot extends CompteBancaire {

    private double tauxAgios;   // taux quotidien des agios
Vous avez sûrement noté l'emploi du mot clé extends, qui permet d'indiquer la relation d'héritage entre CompteDepot et CompteBancaire.

Le premier problème qui se pose à nous est la manière d'initialiser une instance de cette nouvelle classe. Un compte de dépôt est un compte bancaire avec des propriétés en plus, mais pour l'initialiser, le constructeur de CompteDepot ne doit pas omettre d'initialiser la partie héritée, c'est-à-dire d'appeler le constructeur de CompteBancaire. Il doit en fait y avoir une chaîne d'appels aux constructeurs des superclasses. Ici, nous utilisons le mot clé super, qui permet dans un constructeur d'appeler le constructeur de la superclasse. Cet appel doit être la première instruction donnée dans le constructeur de la classe :
    // Constructeur
    CompteDepot(String unNom, String uneAdresse, double unTaux) {
        // on crée déjà la partie commune
        super(unNom, uneAdresse);
        // puis on initialise le taux des agios
        tauxAgios = unTaux;
    }
La seule méthode à redéfinir dans la classe CompteDepot est afficherEtat, pour laquelle on souhaite afficher une ligne de plus, indiquant qu'on a bien un compte de dépôts. Nous trouvons ici un deuxième emploi du mot clé super : en conjonction avec la notation pointée, il permet d'appeler une méthode masquée par l'héritage, en l'occurrence la méthode afficherEtat de CompteBancaire, que nous sommes justement en train de redéfinir :
    // Méthode redéfinie : l'affichage
    public void afficherEtat() {
        System.out.println("Compte de dépôts");
        super.afficherEtat(); // appeler la méthode de même nom dans la superclasse
    }
Il nous reste à définir la méthode traitementQuotidien, qui était définie comme abstraite dans la superclasse. Notez la double conversion de solde en double pour effectuer les opérations internes en réel double précision, puis du résultat à débiter en int pour rester dans des calculs entiers9. Cette conversion utilise l'opération de cast, notée par un type entre parenthèses. Notez aussi l'appel direct à la méthode débiter, héritée de CompteBancaire :
    // Définition de la méthode de traitementQuotidien
    public void traitementQuotidien() {
        if (solde < 0) {
            débiter((int) (-1.0 * (double) solde * tauxAgios));
        }
    }
}
Donnons plus rapidement la deuxième sous-classe, CompteEpargne. On notera juste la redéfinition en plus de la méthode débiter, pour vérifier que le solde ne devient pas négatif :
// Classe CompteEpargne - version 1.0

/**
 * Classe représentant un compte d'épargne, sous-classe de CompteBancaire.
 * @author Karl Tombre
 */

public class CompteEpargne extends CompteBancaire {
    private double tauxIntérêts;   // taux d'intérêts par jour

    // Constructeur
    CompteEpargne(String unNom, String uneAdresse, double unTaux) {
        // on crée déjà la partie commune
        super(unNom, uneAdresse);
        // puis on initialise le taux d'intérêt
        tauxIntérêts = unTaux;
    }

    // Méthode redéfinie : l'affichage
    public void afficherEtat() {
        System.out.println("Compte d'épargne");
        super.afficherEtat(); // appeler la méthode de même nom dans la superclasse
    }

    // Méthode redéfinie : débiter -- interdit de passer en-dessous de 0
    public void débiter(int montant) {
        if (montant <= solde) {
            solde -= montant;
        }
        else {
            System.out.println("Débit non autorisé");
        }
    }
    
    // Définition de la méthode de traitementQuotidien
    public void traitementQuotidien() {
        créditer((int) ((double) solde * tauxIntérêts));
    }
}

4.6.1  Héritage et typage

Nous avons vu qu'une classe peut être assimilée à un type, dans la mesure où elle sert à définir des objets auxquels s'applique un ensemble d'opérations. La règle usuelle en programmation est de vérifier la correction des programmes avant l'exécution, c'est-à-dire de garantir avant l'exécution du programme que les méthodes appelées existent bien, que les variables sont du bon type, etc. C'est déjà pour cette raison que Java, comme beaucoup d'autres langages, est fortement typé (cf. § 2.2) et qu'il faut connaître d'avance -- c'est-à-dire au moment de la compilation (cf. § 1.2) -- le type de toutes les variables et la signature de toutes les méthodes utilisées.

Comment cette notion de typage fort s'articule-t-elle avec l'héritage ? D'une certaine manière -- bien que cela soit un peu réducteur du point de vue formel -- une sous-classe peut être considérée comme définissant un sous-type. Il y a donc compatibilité de types ; en reprenant notre exemple, une variable déclarée de type CompteBancaire peut référencer un objet instance de CompteDepot ou de CompteEpargne. Attention, le contraire n'est pas vrai a priori ! Bien entendu, quand on manipule une variable de type CompteBancaire, c'est l'interface définie par cette classe qui est accessible, même si l'objet référencé possède d'autres attributs de par son "appartenance" à une sous-classe de CompteBancaire.

Nous allons mettre cette propriété à profit pour proposer une version légèrement modifiée de la classe AgenceBancaire :
// Classe AgenceBancaire - version 1.1

/**
 * Classe représentant une agence bancaire et les méthodes qui lui
 * sont associées.
 * @author Karl Tombre
 */

public class AgenceBancaire {
    private CompteBancaire[] tabComptes;  // le tableau des comptes
    private int capacitéCourante;         // la capacité du tableau
    private int nbComptes;                // le nombre effectif de comptes

    // Constructeur -- au moment de créer une agence, on crée un tableau
    // de capacité initiale 10, mais qui ne contient encore aucun compte
    AgenceBancaire() {
        tabComptes = new CompteBancaire[10];
        capacitéCourante = 10;
        nbComptes = 0;
    }

    // Méthode privée utilisée pour incrémenter la capacité
    private void augmenterCapacité() {
        // Incrémenter la capacité de 10
        capacitéCourante += 10;
        // Créer un nouveau tableau plus grand que l'ancien
        CompteBancaire[] tab = new CompteBancaire[capacitéCourante];
        // Recopier les comptes existants dans ce nouveau tableau
        for (int i = 0 ; i < nbComptes ; i++) {
            tab[i] = tabComptes[i];
        }
        // C'est le nouveau tableau qui devient le tableau des comptes
        // (l'ancien sera récupéré par le ramasse-miettes)
        tabComptes = tab;
    }

    // Les méthodes de l'interface
    // Ajout d'un nouveau compte
    public void ajout(CompteBancaire c) {
        if (nbComptes == capacitéCourante) { // on a atteint la capacité max
            augmenterCapacité();
        }
        // Maintenant je suis sûr que j'ai de la place
        // Ajouter le nouveau compte dans la première case vide
        // qui porte le numéro nbComptes !
        tabComptes[nbComptes] = c;
        // On prend note qu'il y a un compte de plus
        nbComptes++;
    }

    // Récupérer un compte à partir d'un nom donné
    public CompteBancaire trouverCompte(String nom) {
        boolean trouvé = false;  // rien trouvé pour l'instant
        int indice = 0;
        for (int i = 0 ; i < nbComptes ; i++) {
            if (nom.equalsIgnoreCase(tabComptes[i].nom())) {
                trouvé = true; // j'ai trouvé
                indice = i;    // mémoriser l'indice
                break;         // plus besoin de continuer la recherche
            }
        }
        if (trouvé) {
            return tabComptes[indice];
        }
        else {
            return null;  // si rien trouvé, je rend la référence nulle
        }
    }

    // Afficher l'état de tous les comptes
    public void afficherEtat() {
        // Il suffit d'afficher l'état de tous les comptes de l'agence
        for (int i = 0 ; i < nbComptes ; i++) { 
            tabComptes[i].afficherEtat();
        }
    }

    // Traitement quotidien de tous les comptes
    public void traitementQuotidien() {
        for (int i = 0 ; i < nbComptes ;i++) {
            tabComptes[i].traitementQuotidien();
        }
    }
}
En fait, la seule différence est l'ajout de la méthode traitementQuotidien, qui permet d'appliquer la méthode traitementQuotidien à tous les comptes. Mais il y a une autre différence qui n'apparaît pas dans le code : maintenant, il est impossible de créer des instances de la classe CompteBancaire, puisque c'est une classe abstraite. Le tableau tabComptes est un tableau d'objets de type CompteBancaire, qui contiendra des références à des instances soit de CompteDepot, soit de CompteEpargne. Grâce à la propriété de sous-typage, ces deux types d'objets peuvent être regroupés dans un même tableau -- à condition bien entendu d'y accéder via l'interface qu'ils ont en commun, à savoir l'interface de la classe abstraite CompteBancaire. C'est ce qui nous permet dans les méthodes afficherEtat et traitementQuotidien de parcourir le tableau en demandant à chaque objet d'effectuer l'opération afficherEtat qui lui est propre.

4.6.2  Liaison dynamique

L'approche objet induit en fait une programmation par requêtes adressées aux interfaces des classes : on n'appelle pas directement une fonction, mais on demande à un objet d'exécuter une méthode de son interface. Se pose alors la question de savoir quand il faut déterminer quelle fonction physique il faut concrètement appeler, c'est-à-dire quand il faut réaliser la liaison entre la requête et la méthode de la classe appropriée.

S'il fallait que le compilateur connaisse précisément d'avance quelle méthode de quelle classe doit être appelée, on perdrait le bénéfice de l'héritage. En effet, il serait alors impossible de garantir un comportement correct des méthodes afficherEtat et traitementQuotidien de la classe AgenceBancaire, puisque ce n'est qu'au moment de l'exécution que l'on saura lequel des comptes du tableau est un compte de dépôts et lequel est un compte d'épargne. C'est pourquoi, dans les langages à objets, la liaison entre la requête et la méthode effectivement activée est dynamique. Le compilateur est capable d'établir que la méthode existe bien -- puisque afficherEtat et traitementQuotidien appartiennent bien toutes deux à l'interface de la classe CompteBancaire, et que les instructions tabComptes[i].afficherEtat() et tabComptes[i].traitementQuotidien() sont donc bien valides -- mais le choix de la méthode qui s'exécutera est différé jusqu'au moment de l'exécution, quand on constate qu'à l'indice i, le tableau comporte soit une instance de CompteDepot, soit une instance de CompteEpargne.

Nous pouvons donc écrire une nouvelle version de notre programme de gestion bancaire ; à la création d'un nouveau compte, nous demandons maintenant à l'utilisateur quel type de compte doit être créé. Dans la version actuelle, nous avons fixé dans le programme le taux d'intérêt des comptes d'épargne et le taux des agios ; bien entendu, une version plus complète devrait prévoir de saisir ces taux, et éventuellement de pouvoir les modifier. Le reste du programme ne change pas, la liaison dynamique se chargeant d'activer les méthodes appropriées. Nous terminons le programme par l'appel de la méthode traitementQuotidien, suivi d'un nouvel affichage de l'état des comptes de l'agence, pour illustrer le traitement différencié qui est fait :
// Classe Banque - version 4.1

/**
 * Classe contenant un programme de gestion bancaire, utilisant
 * un objet de type AgenceBancaire
 * @author Karl Tombre
 * @see    AgenceBancaire, CompteBancaire, Utils
 */

public class Banque {
    public static void main(String[] args) {
        AgenceBancaire monAgence = new AgenceBancaire();

        while (true) { // boucle infinie dont on sort par un break
            String monNom = Utils.lireChaine("Donnez le nom du client (rien=exit) : ");
            if (monNom.equals("")) {
                break;  // si on ne donne aucun nom on quitte la boucle
            }
            else {
                // Vérifier si le compte existe
                CompteBancaire monCompte = monAgence.trouverCompte(monNom);
                if (monCompte == null) {
                    // rien trouvé, on le crée
                    System.out.println("Ce compte n'existe pas, nous allons le créer");
                    String monAdresse = Utils.lireChaine("Adresse = ");
                    // Choisir le type de compte
                    String type = 
                        Utils.lireChaine("Compte de [D]épôt (défaut) ou d'[E]pargne ? ");
                    char choixType = type.charAt(0);
                    // Créer le compte de type demandé
                    // On autorise les lettres accentuées ou non
                    if (choixType == 'e' || choixType == 'E' || 
                        choixType == 'é' || choixType == 'É') {
                        monCompte = new CompteEpargne(monNom, monAdresse, 0.00015);
                    }
                    else {
                        monCompte = new CompteDepot(monNom, monAdresse, 0.00041);
                    }
                    // L'ajouter aux comptes de l'agence
                    monAgence.ajout(monCompte);
                }

                String choix = Utils.lireChaine("Votre choix : [D]ébit, [C]rédit ? ");
                boolean credit = false;   // variable vraie si c'est un crédit

                // Récupérer la première lettre de la chaîne saisie
                char monChoix = choix.charAt(0);
                if (monChoix == 'c' || monChoix == 'C') {
                    int montant = Utils.lireEntier("Montant à créditer = ");
                    monCompte.créditer(montant);
                }
                else if (monChoix == 'd' || monChoix == 'D') {
                    int montant = Utils.lireEntier("Montant à débiter = ");
                    monCompte.débiter(montant);
                }
                else {
                    System.out.println("Choix invalide");
                }
                // Dans tous les cas, afficher l'état du compte
                monCompte.afficherEtat();
            }
        }
        // Quand on sort de la boucle, afficher l'état global de l'agence
        System.out.println("Voici le nouvel état des comptes de l'agence");
        monAgence.afficherEtat();
        System.out.println("-----------------------------------");
        System.out.println("On applique un traitement quotidien");
        System.out.println("-----------------------------------");
        monAgence.traitementQuotidien();
        monAgence.afficherEtat();
    }
}
Voici une trace de l'exécution de ce programme ; vous noterez le débit non autorisé du compte d'épargne -- puisqu'on passerait en-dessous de 0 --, le traitement différencié des deux comptes dans l'affichage des états, et le résultat différent du traitement quotidien :
Donnez le nom du client (rien=exit) : \emph{Karl}
Ce compte n'existe pas, nous allons le créer
Adresse = \emph{Sornéville}
Compte de [D]épôt (défaut) ou d'[E]pargne ? \emph{D}
Votre choix : [D]ébit, [C]rédit ? \emph{D}
Montant à débiter = \emph{50000}
Compte de dépôts
Compte numéro 1 ouvert au nom de Karl
Adresse du titulaire : Sornéville
Le solde actuel du compte est de -50000 euros.
************************************************
Donnez le nom du client (rien=exit) : \emph{Luigi}
Ce compte n'existe pas, nous allons le créer
Adresse = \emph{Rome}
Compte de [D]épôt (défaut) ou d'[E]pargne ? \emph{é}
Votre choix : [D]ébit, [C]rédit ? \emph{C}
Montant à créditer = \emph{50000}
Compte d'épargne
Compte numéro 2 ouvert au nom de Luigi
Adresse du titulaire : Rome
Le solde actuel du compte est de 50000 euros.
************************************************
Donnez le nom du client (rien=exit) : \emph{Luigi}
Votre choix : [D]ébit, [C]rédit ? \emph{D}
Montant à débiter = \emph{60000}
Débit non autorisé
Compte d'épargne
Compte numéro 2 ouvert au nom de Luigi
Adresse du titulaire : Rome
Le solde actuel du compte est de 50000 euros.
************************************************
Donnez le nom du client (rien=exit) : 
Voici le nouvel état des comptes de l'agence
Compte de dépôts
Compte numéro 1 ouvert au nom de Karl
Adresse du titulaire : Sornéville
Le solde actuel du compte est de -50000 euros.
************************************************
Compte d'épargne
Compte numéro 2 ouvert au nom de Luigi
Adresse du titulaire : Rome
Le solde actuel du compte est de 50000 euros.
************************************************
-----------------------------------
On applique un traitement quotidien
-----------------------------------
Compte de dépôts
Compte numéro 1 ouvert au nom de Karl
Adresse du titulaire : Sornéville
Le solde actuel du compte est de -50020 euros.
************************************************
Compte d'épargne
Compte numéro 2 ouvert au nom de Luigi
Adresse du titulaire : Rome
Le solde actuel du compte est de 50007 euros.
************************************************

Et si je veux interdire la définition de sous-classes ?

Il arrive que l'on souhaite qu'une classe donnée ne puisse pas servir de superclasse à des sous-classes. La raison principale est souvent une question de sécurité : la compatibilité de type entre une classe et ses sous-classes pourrait permettre à des programmeurs mal intentionnés de définir une sous-classe qui viendrait se substituer à la classe d'origine, en donnant l'apparence d'avoir le même comportement, alors qu'elle peut faire des choses radicalement différentes. Le mot clé final permet d'indiquer qu'une classe ne peut pas avoir de sous-classes. La classe String, par exemple, est tellement vitale pour les programmes que les concepteurs de Java l'ont déclarée finale :
public final class String {
   ...
}

1
Notez bien que l'initialisation de la variable de classe ne se fait pas dans un constructeur, mais directement dans la classe, au moment où elle est déclarée.
2
Bien que Java le permette...
3
En C++, un langage proche de Java par la syntaxe, on peut redéfinir des opérateurs sur toutes les classes.
4
De manière caractéristique, c'est le mot clé self qui est utilisé dans le langage Smalltalk pour désigner l'objet courant.
5
Si je vous donnais les solutions de tous les problèmes dans ce polycopié, je me retrouverais en panne d'idées nouvelles pour les TDs !
6
Il arrive qu'il y ait confusion entre l'emploi de la composition et celui de l'héritage. Une bonne manière de choisir l'outil le plus approprié est de se demander si B est une sorte de A (dans ce cas B hérite de A) ou si B contient des objets de type A (dans ce cas, on définit une variable d'instance de type A dans B).
7
Un vrai programmeur Java ne ferait pas ce genre de gymnastique ; il utiliserait directement la classe Vector, disponible dans la bibliothèque de base Java, qui met justement en oeuvre un tableau de taille variable. Mais il nous semble utile d'avoir fait soi-même ce genre de manipulation au moins une fois, c'est pourquoi nous construisons ici notre propre tableau extensible.
8
Pour garder des programmes illustratifs simples et éviter une gestion lourde de dates et de périodes, nous avons bien entendu simplifié énormément la gestion de comptes bancaires, pour laquelle les intérêts et les agios sont calculés beaucoup moins souvent mais tiennent en revanche compte des dates et des durées. Depuis le début, nous avons aussi simplifié le problème des sommes en ne comptant qu'avec des valeurs entières ; cela nous évite des problèmes d'arrondi à deux chiffres après la virgule, mais va nous compliquer un peu la vie quand il s'agira de calculer des intérêts et des agios.
9
Une conséquence de la formule choisie est que notre banque ne débite rien si les agios quotidiens ne sont que des centimes...

Précédent Remonter Suivant