Précédent Remonter Suivant

Chapitre 6  Programmation (un peu) plus avancée

Amis, ne creusez pas vos chères rêveries ;
Ne fouillez pas le sol de vos plaines fleuries ;
Et, quand s'offre à vos yeux un océan qui dort,
Nagez à la surface ou jouez sur le bord.
Car la pensée est sombre ! Une pente insensible
Va du monde réel à la sphère invisible ;
La spirale est profonde, et quand on y descend
Sans cesse se prolonge et va s'élargissant,
Et pour avoir touché quelque énigme fatale,
De ce voyage obscur souvent on revient pâle !
Victor Hugo
Comme je ne souhaite absolument pas que vous sortiez complètement pâles et désemparés de ce cours de tronc commun, je me contenterai dans ce chapitre d'aborder quelques-uns des nombreux points complémentaires dont j'aurais encore aimé vous parler, en ce qui concerne la programmation. Il va de soi que la science informatique -- ou faut-il plutôt parler d'un art ? -- recèle bien plus de joies, mais aussi de mystères, que ce que nous pouvons traiter en ces quelque séances. Espérons toutefois qu'à l'issue de ce cours, vous aurez vous-même envie d'en apprendre plus...

6.1  Portée

La portée d'une variable est le bloc de code au sein de laquelle cette variable est accessible ; la portée détermine également le moment où la variable est créée, et quand elle est détruite.

Les variables d'instance et variables de classe sont visibles et accessibles directement à l'intérieur de l'ensemble de la classe. Par exemple, la variable tabComptes est accessible dans l'ensemble de la classe AgenceBancaire, et nous avons bien entendu profité largement de cette visibilité pour l'utiliser dans les méthodes de cette classe. Si une variable de classe ou d'instance est déclarée publique, elle est également accessible de l'extérieur de la classe, mais en employant la notation pointée.

Les variables locales sont définies dans une méthode -- ou dans un bloc interne à une méthode -- et ne sont accessibles et visibles qu'au sein du bloc dans lequel elles ont été définies, ainsi que dans les blocs imbriqués dans ce bloc.

Reprenons par exemple la méthode supprimer de la classe AgenceBancaire :
    public void supprimer(CompteBancaire c) {
        boolean trouvé = false;  // rien trouvé pour l'instant
        int indice = 0;
        for (int i = 0 ; i < nbComptes ; i++) {
            if (tabComptes[i] == c) { // attention comparaison de références
                trouvé = true; // j'ai trouvé
                indice = i;    // mémoriser l'indice
                break;         // plus besoin de continuer la recherche
            }
        }
        if (trouvé) {
            // Décaler le reste du tableau vers la gauche
            // On "écrase" ainsi le compte à supprimer
            for (int i = indice+1 ; i < nbComptes ; i++) {
                tabComptes[i-1] = tabComptes[i];
            }
            // Mettre à jour le nombre de comptes
            nbComptes--;
        }
        else {
            // Message d'erreur si on n'a rien trouvé
            System.out.println("Je n'ai pas trouvé ce compte");
        }
    }
Les paramètres d'appel d'une méthode (comme le paramètre c de la méthode supprimer) sont accessibles tout au long de la méthode.

Dans tous les cas, une variable peut être masquée si une autre variable de même nom est définie dans un bloc imbriqué. Le fait qu'une variable soit masquée signifie juste qu'elle n'est pas directement accessible ; elle n'en continue pas moins d'exister, et on peut même dans certains cas y accéder, en donnant les précisions nécessaires. Si par exemple je déclarais dans la méthode supprimer une variable locale de nom nbComptes, celle-ci masquerait la variable d'instance nbComptes. Pour accéder à cette dernière, je devrais dans ce cas écrire this.nbComptes. En règle générale, c'est une mauvaise idée de masquer une variable significative, car cela rend habituellement la lecture du programme difficile et confuse.

6.2  Espaces de nommage : les packages

On retrouve la notion de bibliothèque dans la grande majorité des langages de programmation. Pour beaucoup d'entre eux, la définition du langage est d'ailleurs accompagnée d'une définition normalisée de la bibliothèque de base, ensemble de fonctions fournissant les services fondamentaux tels que les entrées-sorties, les calculs mathématiques, etc. Viennent s'y ajouter des bibliothèques quasiment "standard" pour l'interface utilisateur, le graphisme, etc. D'autres bibliothèques accompagnent des produits logiciels. Enfin, certaines bibliothèques peuvent elles-mêmes être des produits logiciels, commercialisés en tant que tels.

Avec la notion de package, Java étend et modifie un peu, mais généralise aussi, le concept de bibliothèque.

La problématique du nommage est liée à celle de l'utilisation de bibliothèques. Si on peut théoriquement garantir l'absence de conflits de nommage quand un seul programmeur développe une application, c'est déjà plus difficile quand une équipe travaille ensemble sur un produit logiciel, et cela nécessite des conventions ou constructions explicites quand on utilise des bibliothèques en provenance de tiers.

De ce point de vue, Java systématise l'emploi des packages, qui sont entre autres des espaces de nommage permettant de réduire fortement les conflits de nommage et les ambiguités. Toute classe doit appartenir à un package ; si on n'en indique pas, comme cela a été le cas jusqu'à maintenant pour tous nos programmes, la classe est ajoutée à un package par défaut.

L'organisation hiérarchique en packages est traduite à la compilation en une organisation hiérarchique équivalente des répertoires, sous la racine indiquée comme le "réceptacle" de vos classes Java. La recommandation est faite d'utiliser pour ses noms de packages une hiérarchisation calquée sur celle des domaines Internet. Ainsi, les classes que j'écris pour les corrigés de mon cours devraient être déclarées dans le package fr.inpl-nancy.mines.tombre.ens.cours1A si je décide d'organiser mes programmes d'enseignement dans la catégorie ens.

Pour définir le package d'appartenance d'une classe, on utilise le mot clé package ; si je souhaite regrouper tous mes exemples bancaires dans le package banque, je peux donc écrire :
package fr.inpl-nancy.mines.tombre.ens.cours1A.banque;

public class Banque {
    ...
Les fichiers seront alors organisés de manière symétrique, dans un répertoire du genre :
fr/inpl-nancy/mines/tombre/ens/cours/cours1A/banque/
Pour faciliter l'utilisation des packages, Java fournit le mot clé import, que nous avons déjà utilisé plusieurs fois. Nous avons par exemple écrit :
import java.io.*;

public class Utils {
    public static String lireChaine(String question) {
        InputStreamReader ir = new InputStreamReader(System.in);
        BufferedReader br = new BufferedReader(ir);
        ...
Cela nous a permis d'utiliser directement la classe InputStreamReader, sans devoir donner son nom complet, qui est java.io.InputStreamReader. La directive import permet soit "l'importation" d'une classe (on écrirait alors par exemple import java.io.InputStreamReader;), soit celle de toutes les classes d'un package donné, ce que nous avons fait grâce à la forme java.io.*. Cette directive permet ultérieurement d'utiliser les classes ainsi importées sans donner leur nom complet (préfixé par leur package d'appartenance), autrement dit sous une forme abrégée, plus pratique. Rien ne vous empêche bien entendu d'utiliser une classe ou une interface publique sans l'importer, mais il faudra alors donner son nom complet, par exemple java.awt.event.MouseListener au lieu de MouseListener.

NB : Quand on ne spécifie pas la visibilité d'une variable ou d'une méthode (pas de mot clé public, protected ou private), celle-ci est visible dans toutes les classes de son package. Nous reproduisons ci-après un récapitulatif des règles de visibilité, suivant les cas.
Protection Classe Sous-classe Package Monde entier
private oui non non non
protected oui oui oui non
public oui oui oui oui
package (rien) oui non oui non

6.3  Entrées/sorties

Si les instructions d'entrées/sorties -- c'est-à-dire de communication entre le programme et son environnement extérieur -- sont très rarement définies dans le langage de programmation lui-même, les langages sont toujours accompagnés d'une bibliothèque de fonctions et procédures standards permettant d'effectuer ces opérations. Java n'est pas une exception.

Malgré sa simplicité apparente, la définition d'une bibliothèque générique d'entrées/sorties est une tâche très délicate. En effet, on a de multiples possibilités, qu'il faut de préférence prendre en compte de manière la plus homogène possible : À la base des entrées/sorties en Java se trouve la notion de flot de données (stream en anglais). Pour lire de l'information, un programme ouvre un flot de données connecté à une source d'information (un fichier, une connexion réseau, un clavier, etc.) et lit l'information de manière séquentielle sur ce flot. De même, pour envoyer de l'information vers une destination extérieure, un programme ouvre aussi un flot connecté à la destination et y écrit de manière séquentielle. Les opérations d'entrées/sorties se ressemblent donc toutes : on commence par ouvrir un flot, puis on lit (ou on écrit) dans ce flot tant qu'il y a de l'information à lire (respectivement à écrire), puis enfin on referme le flot.

L'ensemble des fonctionnalités standards d'entrées/sorties en Java se trouvent dans le package java.io. Celui-ci contient notamment 4 classes abstraites : Reader et Writer factorisent le comportement commun de toutes les classes qui lisent (respectivement écrivent) dans des flots de caractères (caractères au codage Unicode sur 16 bits -- cf. § F.1). InputStream et OutputStream, quant à elles, sont les superclasses des classes qui permettent de lire et d'écrire dans des flots d'octets (sur 8 bits).

Le tableau qui suit donne un aperçu de quelques-unes des classes disponibles dans le package java.io. Il faut savoir qu'un certain nombre de fonctionnalités non mentionnées dans ce polycopié sont également disponibles dans ce package, par l'intermédiaire de classes spécialisées.
Superclasse ® Reader InputStream Writer OutputStream
E/S bufferisées BufferedReader BufferedInputStream BufferedWriter BufferedOutputStream
E/S avec la mémoire CharArrayReader ByteArrayInputStream CharArrayWriter ByteArrayOutputStream
Conversion flots d'octets/flots de caractères InputStreamReader   OutputStreamWriter  
Pipelines (la sortie d'un programme est l'entrée d'un autre) PipedReader PipedInputStream PipedWriter PipedOutputStream
Fichiers FileReader FileInputStream FileWriter FileOutputStream

6.3.1  L'explication d'un mystère

Nous avons maintenant des éléments pour expliquer une bonne partie des notations ésotériques que nous vous avons imposées dès le début du cours, en particulier pour lire et écrire des informations.

La classe System permet à vos programmes Java d'accéder aux ressources du système hôte. Cette classe possède plusieurs variables de classe, dont deux vont nous intéresser ici. La variable de classe System.out est de type PrintStream ; elle permet d'écrire sur votre sortie standard (habituellement, la console Java sur votre écran). C'est un flot de sortie, appelé le flot de sortie standard, qui est ouvert d'office et prêt à recevoir des données. Les méthodes de PrintStream que nous avons utilisées tout au long de ce cours sont : Décortiquons maintenant la méthode de classe Utils.lireChaine, qui permet de lire une chaîne de caractères au clavier. La classe System fournit aussi une variable de classe System.in, qui est déclarée comme étant de type InputStream1. Comme son homologue System.out, cette variable désigne un flot ouvert d'office, le flot d'entrée standard. C'est un flot d'octets et non de caractères, ce qui est logique dans la mesure où les systèmes informatiques actuels fonctionnent avec des flots de caractères codés sur 8 bits en entrées/sorties. Mais comme nous voulons lire des chaînes de caractères, la première chose à faire est de créer une instance d'une sous-classe de Reader, pour nous retrouver dans la hiérarchie des flots de lecture de caractères. Il se trouve que la classe InputStreamReader permet justement de faire cette "conversion", comme nous l'avons vu dans le tableau. C'est donc la raison de l'instanciation d'un objet de type InputStreamReader, désigné par la variable ir :
    public static String lireChaine(String question) {
        InputStreamReader ir = new InputStreamReader(System.in);
Mais en fait, nous voulons effectuer des entrées bufferisées, et il nous faut donc opérer une seconde "conversion", en créant une instance de la classe BufferedReader, désignée par la variable br. C'est cette variable que nous allons utiliser par la suite...
        BufferedReader br = new BufferedReader(ir);
On pose la question, sans aller à la ligne, et on force l'affichage du buffer, comme nous l'avons déjà expliqué :
        System.out.print(question);
        System.out.flush();
Il nous reste à appeler la méthode readLine, qui fait partie de l'interface de la classe BufferedReader, pour lire au clavier une ligne (jusqu'à ce que l'utilisateur tape "entrée") :
        String reponse = "";
        try {
            reponse = br.readLine();
        } catch (IOException ioe) {}
        return reponse;
    }

6.3.2  Les fichiers

La gestion de fichiers comprend deux aspects :
  1. Les considérations générales de la gestion des entrées-sorties, dans la mesure où la lecture ou l'écriture dans un fichier n'est en fait qu'un flot parmi d'autres. Nous avons déjà vu que nous disposons dans la hiérarchie des classes du package java.io des classes FileReader et FileWriter pour lire et écrire des flots de caractères dans des fichiers, et de FileInputStream et FileOutputStream pour les flots d'octets.
  2. L'établissement d'un lien avec le système de fichiers sous-jacent, tout en restant indépendant de la plateforme. Cet aspect est pris en compte par la classe File.
Prenons un exemple qui illustre ceci et montre comment lire et écrire dans un fichier. Une faiblesse importante de notre programme de gestion bancaire est bien entendu que nous perdons tout ce que nous avons saisi quand nous quittons le programme. Nous souhaitons donc maintenant être capables de sauvegarder les comptes de l'agence bancaire dans un fichier, au moment de quitter le programme, et de relire la sauvegarde quand on relance le programme.

La première chose à faire est d'ouvrir le fichier. Si le nom du fichier est comptes.dat, on écrira :
File fich = new File("comptes.dat");
Mais il se peut que ce fichier n'existe pas encore ; nous voulons alors le créer. Il se trouve que la classe FileOutputStream, quand elle est instanciée, crée le fichier s'il n'existe pas. On écrira donc :
if (!fich.exists()) {
    FileOutputStream fp = new FileOutputStream("comptes.dat");
    fich = new File("comptes.dat");
}
Vous noterez l'utilisation de la méthode exists de la classe File pour vérifier l'existence du fichier.

À condition que nous ayons le droit d'écrire dans ce fichier, nous passons ensuite à une phase de conversions très analogues à celle que nous avons effectuées à partir de System.in. En effet, nous voulons effectuer des entrées/sorties bufferisées, une fois de plus, et écrivons donc :
BufferedWriter bw = null;
try {
    if (fich.canWrite()) {
        FileWriter fw = new FileWriter(f);
        bw = new BufferedWriter(fw);
    }
}
catch (IOException ioe) {}
Nous avons maintenant une variable de type BufferedWriter, et pouvons écrire des chaînes de caractères. Comme nous souhaitons les séparer par le caractère "retour à la ligne", nous utilisons à la fois les fonctions write et newLine de la classe :
try {
    bw.write("Karl Tombre");
    bw.newLine();
    bw.write("Sornéville");
    bw.newLine();
}
catch (IOException ioe) {}
Pour écrire des entiers, nous choisirons de les convertir en chaînes de caractères, grâce à la méthode toString de la classe Integer :
try {
    Integer i = new Integer(numéro);
    bw.write(i.toString());
    bw.newLine();
    i = new Integer(solde);
    bw.write(i.toString());
    bw.newLine();
}
catch (IOException ioe) {}
Il reste pour finir à fermer le fichier, une fois que tout a été écrit :
bw.close();
Les opérations de lecture dans un fichier se font de manière similaire.

Avant de passer au programme de gestion bancaire, organisons-nous un peu. Nous allons ajouter dans la classe Utils des fonctions permettant de ne pas encombrer nos programmes, en y regroupant les lignes de code nécessaires aux opérations les plus courantes. La nouvelle version de la classe Utils se passe a priori de commentaires ; vous y retrouverez plusieurs des lignes de code que nous venons de voir :
// Classe Utils - version 2.0

/**
 *  Classe regroupant les utilitaires disponibles pour les exercices
 * de 1ere année du tronc commun informatique.
 * @author Karl Tombre
 */

import java.io.*;

public class Utils {
    // Les fonctions lireChaine et lireEntier
    // Exemple d'utilisation dans une autre classe :
    // String nom = Utils.lireChaine("Entrez votre nom : ");
    public static String lireChaine(String question) {
        InputStreamReader ir = new InputStreamReader(System.in);
        BufferedReader br = new BufferedReader(ir);
        
        System.out.print(question);
        System.out.flush();
        return loadString(br);
    }

    // La lecture d'un entier n'est qu'un parseInt de plus !!
    public static int lireEntier(String question) {
        return Integer.parseInt(lireChaine(question));
    }

    // écriture d'une chaîne dans un BufferedWriter
    public static void saveString(BufferedWriter bw, String s) {
        try {
            bw.write(s);
            bw.newLine();
        }
        catch (IOException ioe) {}
    }

    // écriture d'un entier : on convertit en chaîne et on se ramène
    // à la procédure précédente
    public static void saveInt(BufferedWriter bw, int i) {
        Integer q = new Integer(i);
        saveString(bw, q.toString());
    }

    // ouverture en écriture d'un fichier
    // rend un BufferedWriter -- null si pas possible de l'ouvrir
    public static BufferedWriter openWriteFile(File f) {
        BufferedWriter bw = null;
        try {
            if (f.canWrite()) {
                FileWriter fw = new FileWriter(f);
                bw = new BufferedWriter(fw);
            }
        }
        catch (IOException ioe) {}
        return bw;
    }

    // ouverture en lecture d'un fichier
    // rend un BufferedReader -- null si pas possible de l'ouvrir
    public static BufferedReader openReadFile(File f) {
        BufferedReader br = null;
        try {
            if (f.canRead()) {
                FileReader fr = new FileReader(f);
                br = new BufferedReader(fr);
            }
        }
        catch (IOException ioe) {}
        return br;
    }

    // lecture dans un BufferedReader d'une chaîne de caractères
    public static String loadString(BufferedReader br) {
        String s = "";
        try {
            s = br.readLine();
        }
        catch (IOException ioe) {}
        return s;
    }

    // lecture d'un entier dans un BufferedReader
    public static int loadInt(BufferedReader br) {
        return Integer.parseInt(loadString(br));
    }
}
Décidons ensuite comment un compte bancaire va être sauvegardé et rechargé dans un fichier. Nous optons pour la simplicité en écrivant et en lisant des chaînes de caractères. Dans l'ordre, on écrira donc le nom, l'adresse, le numéro et le solde, suivi du taux d'agios pour un compte de dépôts, et du taux d'intérêts pour un compte d'épargne. Nous donnons ci-après la nouvelle version des trois classes CompteBancaire, CompteEpargne et CompteDepot. Notez qu'en plus des deux nouvelles méthodes qui y sont définies, nous avons ajouté un constructeur par défaut (sans paramètres) dont nous aurons besoin au moment du chargement à partir d'un fichier :
// Classe CompteBancaire - version 3.1

import java.io.*;

/**
 * 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 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++;
    }

    // Constructeur par défaut, sans paramètres
    CompteBancaire() {
        nom = "";
        adresse = "";
        numéro = 0;
        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("************************************************");
    }
    // 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();

    // sauvegarde du compte dans un BufferedWriter
    public void save(BufferedWriter bw) {
        Utils.saveString(bw, nom);
        Utils.saveString(bw, adresse);
        Utils.saveInt(bw, numéro);
        Utils.saveInt(bw, solde);
    }

    // chargement du compte à partir d'un BufferedReader
    public void load(BufferedReader br) {
        nom = Utils.loadString(br);
        adresse = Utils.loadString(br);
        numéro = Utils.loadInt(br);
        solde = Utils.loadInt(br);
    }
}
// Classe CompteDepot - version 1.1

import java.io.*;

/**
 * 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

    // 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;
    }

    // Constructeur par défaut
    CompteDepot() {
        super();
        tauxAgios = 0.0;
    }

    // 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
    }
    
    // Définition de la méthode de traitementQuotidien
    public void traitementQuotidien() {
        if (solde < 0) {
            débiter((int) (-1.0 * (double) solde * tauxAgios));
        }
    }
    // sauvegarde du compte dans un BufferedWriter
    public void save(BufferedWriter bw) {
        super.save(bw);
        Double d = new Double(tauxAgios);
        Utils.saveString(bw, d.toString());
    }

    // chargement du compte à partir d'un BufferedReader
    public void load(BufferedReader br) {
        super.load(br);
        tauxAgios = Double.valueOf(Utils.loadString(br)).doubleValue();
    }
}
// Classe CompteEpargne - version 1.1

import java.io.*;

/**
 * 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;
    }

    // Constructeur par défaut
    CompteEpargne() {
        super();
        tauxIntérêts = 0.0;
    }

    // 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));
    }
    // sauvegarde du compte dans un BufferedWriter
    public void save(BufferedWriter bw) {
        super.save(bw);
        Double d = new Double(tauxIntérêts);
        Utils.saveString(bw, d.toString());
    }

    // chargement du compte à partir d'un BufferedReader
    public void load(BufferedReader br) {
        super.load(br);
        tauxIntérêts = Double.valueOf(Utils.loadString(br)).doubleValue();
    }
}
Il nous faut maintenant définir les opérations de sauvegarde et de chargement dans l'interface ListeDeComptes :
// Interface ListeDeComptes - version 1.1

import java.io.*;

/**
 * Interface représentant une liste de comptes et les opérations
 * que l'on souhaite effectuer sur cette liste
 * @author Karl Tombre
 */

public interface ListeDeComptes {
    // Récupérer un compte à partir d'un nom donné
    public CompteBancaire trouverCompte(String nom);
    // Ajout d'un nouveau compte
    public void ajout(CompteBancaire c);
    // Suppression d'un compte 
    public void supprimer(CompteBancaire c);
    // Afficher l'état de tous les comptes
    public void afficherEtat();
    // Traitement quotidien de tous les comptes
    public void traitementQuotidien();
    // Sauvegarder dans un fichier
    public void sauvegarder(File f);
    // Charger à partir d'un fichier
    public void charger(File f);
}
Maintenant, nous devons l'implanter dans les classes qui mettent en oeuvre cette interface. Nous ne donnons ici que la nouvelle version de la classe AgenceBancaire, c'est-à-dire la version avec tableaux extensibles :
// Classe AgenceBancaire - version 1.3

import java.io.*;

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

public class AgenceBancaire implements ListeDeComptes {
    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();
        }
    }

    // Suppression d'un compte
    public void supprimer(CompteBancaire c) {
        boolean trouvé = false;  // rien trouvé pour l'instant
        int indice = 0;
        for (int i = 0 ; i < nbComptes ; i++) {
            if (tabComptes[i] == c) { // attention comparaison de références
                trouvé = true; // j'ai trouvé
                indice = i;    // mémoriser l'indice
                break;         // plus besoin de continuer la recherche
            }
        }
        if (trouvé) {
            // Décaler le reste du tableau vers la gauche
            // On "écrase" ainsi le compte à supprimer
            for (int i = indice+1 ; i < nbComptes ; i++) {
                tabComptes[i-1] = tabComptes[i];
            }
            // Mettre à jour le nombre de comptes
            nbComptes--;
        }
        else {
            // Message d'erreur si on n'a rien trouvé
            System.out.println("Je n'ai pas trouvé ce compte");
        }
    }
Le début de la classe ne change pas, si ce n'est l'importation du package java.io. Passons maintenant aux deux nouvelles méthodes. La méthode sauvegarder est chargée d'écrire le contenu du tableau de comptes dans le fichier. Il faut bien entendu commencer par ouvrir le fichier en écriture ; pour cela, nous avons défini dans la classe Utils la méthode de classe openWriteFile, qui nous rend un objet de type BufferedWriter, que nous allons manipuler dans la suite des opérations :
    public void sauvegarder(File f) {
        BufferedWriter bw = Utils.openWriteFile(f);
À condition bien entendu que l'ouverture ait été possible (variable bw non égale à null), nous pouvons donc commencer à écrire dans le fichier. En y réfléchissant, nous nous rendons compte qu'il peut être judicieux de stocker le nombre de comptes dans le tableau ; cela simplifiera grandement la lecture. Donc la première ligne du fichier va contenir le nombre de comptes, ce que nous écrivons grâce à la méthode de classe Utils.saveInt :
        if (bw != null) {
            Utils.saveInt(bw, nbComptes);
Pour éviter de repartir à 1 dans la numérotation des comptes bancaires, il faut aussi sauvegarder la variable de classe CompteBancaire.premierNuméroDisponible :
            // Sauvegarder la variable de classe
            Utils.saveInt(bw, CompteBancaire.premierNuméroDisponible);
Il nous reste maintenant à parcourir le tableau en écrivant les comptes les uns après les autres. Si tous les comptes avaient été de la même nature, cela serait enfantin. Mais attention : nous avons à la fois des comptes d'épargne et des comptes de dépôt, et à la relecture du fichier il est nécessaire de savoir les distinguer ! Une première solution aurait été d'utiliser l'opérateur instanceof en écrivant quelque chose comme if (tabComptes[i] instanceof CompteDepot) ... else ... , mais cette solution reste très peu évolutive : qu'adviendrait-il du programme si nous ajoutions des comptes d'épargne en actions, des plans épargne logement, des livret d'épargne... ? Je propose donc une solution plus générique, qui fait appel aux propriétés de réflexivité2 du langage Java. L'idée est d'écrire le nom de la classe d'appartenance de l'objet que l'on veut sauvegarder. En Java, il existe une classe Class dont toutes les classes sont instances. Une méthode getClass est définie sur tous les objets (techniquement, elle est définie dans la classe Object, racine de l'arbre d'héritage). Elle rend la classe d'appartenance de l'objet. Nous désignons cette classe par la variable typeDeCompte. La méthode getName de la classe Class rend une chaîne de caractères désignant le nom de la classe. Une fois cette chaîne écrite, nous pouvons déléguer à l'objet le soin de se sauvegarder (tabComptes[i].save(bw)) ; grâce à la liaison dynamique nous sommes sûrs d'appeler la bonne méthode, celle de la classe CompteDepot ou celle de la classe CompteEpargne, suivant les cas. L'avantage de cette solution est d'être extensible : si nous ajoutons ultérieurement d'autres sous-classes à CompteBancaire, la solution reste identique et le code ci-après n'a pas à être modifié :
            for (int i = 0 ; i < nbComptes ; i++) {
                Class typeDeCompte = tabComptes[i].getClass();
                Utils.saveString(bw, typeDeCompte.getName());
                tabComptes[i].save(bw);
            }
Il nous reste à fermer le flot d'écriture et à indiquer par un message comment l'opération s'est déroulée :
            try {
                bw.close();
            }
            catch (IOException ioe) {}
            System.out.println("Sauvegarde effectuée");
        }
        else {
            System.out.println("Impossible de sauvegarder");
        }
    }
La méthode charger est symétrique. On commence par ouvrir le fichier en lecture, et on lit la première ligne qui, comme vous vous en souvenez, contient le nombre de comptes à charger. Cela nous donnera un compteur pour la boucle de lecture. On lit aussi sur la deuxième ligne la valeur à attribuer à la variable de classe CompteBancaire.premierNuméroDisponible :
    public void charger(File f) {
        BufferedReader br = Utils.openReadFile(f);
        if (br != null) {
            nbComptes = Utils.loadInt(br);
            CompteBancaire.premierNuméroDisponible = Utils.loadInt(br);
Il faut maintenant pour chaque compte sauvegardé commencer par lire le nom de sa classe d'appartenance (Utils.loadString), puis "convertir" ce nom en un objet de type Class, grâce à la méthode de classe Class.forName. Ensuite, nous créons une nouvelle instance de cette classe. Mais nous n'avons pas encore les paramètres d'initialisation, d'où l'intérêt de l'ajout de constructeurs par défaut -- sans paramètres d'initialisation -- dans les classes CompteDepot et CompteEpargne. Nous pouvons ainsi demander la création d'une nouvelle instance grâce à la méthode newInstance de la classe Class. Une fois cette instance créée, c'est à elle que nous laissons le soin de charger ses valeurs par la méthode load, qui active la bonne méthode, une fois de plus grâce à la liaison dynamique. Il ne restera plus qu'à refermer le fichier et à afficher un message sur le déroulement du chargement :
            for (int i = 0 ; i < nbComptes ; i++) {
                try {
                    Class typeDeCompte = Class.forName(Utils.loadString(br));
                    tabComptes[i] = (CompteBancaire) typeDeCompte.newInstance();
                    tabComptes[i].load(br);
                }
                catch(ClassNotFoundException cnfe) {}
                catch(IllegalAccessException iae) {}
                catch(InstantiationException ie) {}
            }
            try {
                br.close();
            }
            catch (IOException ioe) {}
            System.out.println("Comptes chargés");
        }
        else {
            System.out.println("Impossible de charger la sauvegarde");
        }
    }
}
Il nous reste simplement à intégrer ces deux opérations dans notre programme de gestion bancaire. Les seules modifications étant en début et en fin de ce programme, je vous passe les lignes intermédiaires, le programme interactif de débit/crédit n'étant affecté en rien par cette nouvelle fonctionnalité :
// Classe Banque - version 4.3

import java.io.*;
/**
 * Classe contenant un programme de gestion bancaire, utilisant
 * un objet de type AgenceBancaire
 * @author Karl Tombre
 * @see    ListeDeComptes, CompteBancaire, Utils
 */

public class Banque {
    public static void main(String[] args) {
        ListeDeComptes monAgence = new AgenceBancaire();
        File fich = new File("comptes.dat");
        // On commence par charger le fichier de sauvegarde éventuel
        monAgence.charger(fich);

        while (true) { // boucle infinie dont on sort par un break
          // ... la même chose qu'avant
        }
        // 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();

        // Et on termine par une sauvegarde
        try {
            fich = new File("comptes.dat");
            if (!fich.exists()) {
                // Si le fichier n'existe pas, le créer
                FileOutputStream fp = new FileOutputStream("comptes.dat");
                fich = new File("comptes.dat");
            }
            monAgence.sauvegarder(fich);
        }
        catch (IOException ioe) {}
    }
}
Une première exécution de ce programme ne permet pas de charger une sauvegarde qui n'existe pas encore ; mais sinon, les choses se déroulent normalement, et la sauvegarde s'effectue à la fin :
Impossible de charger la sauvegarde
Donnez le nom du client (rien=exit) : \emph{Karl Tombre}
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{E}
Votre choix : [D]ébit, [C]rédit, [S]upprimer ? \emph{C}
Montant à créditer = \emph{10000}
Compte d'épargne
Compte numéro 1 ouvert au nom de Karl Tombre
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 10000 euros.
************************************************
Donnez le nom du client (rien=exit) : \emph{Luigi Liquori}
Ce compte n'existe pas, nous allons le créer
Adresse = \emph{Bari}
Compte de [D]épôt (défaut) ou d'[E]pargne ? \emph{D}
Votre choix : [D]ébit, [C]rédit, [S]upprimer ? \emph{D}
Montant à débiter = \emph{20000}
Compte de dépôts
Compte numéro 2 ouvert au nom de Luigi Liquori
Adresse du titulaire : Bari
Le solde actuel du compte est de -20000 euros.
************************************************
Donnez le nom du client (rien=exit) : 
Voici le nouvel état des comptes de l'agence
Compte d'épargne
Compte numéro 1 ouvert au nom de Karl Tombre
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 10000 euros.
************************************************
Compte de dépôts
Compte numéro 2 ouvert au nom de Luigi Liquori
Adresse du titulaire : Bari
Le solde actuel du compte est de -20000 euros.
************************************************
-----------------------------------
On applique un traitement quotidien
-----------------------------------
Compte d'épargne
Compte numéro 1 ouvert au nom de Karl Tombre
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 10001 euros.
************************************************
Compte de dépôts
Compte numéro 2 ouvert au nom de Luigi Liquori
Adresse du titulaire : Bari
Le solde actuel du compte est de -20008 euros.
************************************************
Sauvegarde effectuée
Examinons le contenu du fichier comptes.dat :
2
3
CompteEpargne
Karl Tombre
Sornéville
1
10001
1.5E-4
CompteDepot
Luigi Liquori
Bari
2
-20008
4.1E-4
Lançons maintenant le programme une seconde fois. Cette fois, le chargement se fait bien et nous retrouvons les comptes dans l'état où nous les avions laissés :
Comptes chargés
Donnez le nom du client (rien=exit) : \emph{Karl tOmbre}
Votre choix : [D]ébit, [C]rédit, [S]upprimer ? \emph{d}
Montant à débiter = \emph{10000}
Compte d'épargne
Compte numéro 1 ouvert au nom de Karl Tombre
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 1 euros.
************************************************
Donnez le nom du client (rien=exit) : \emph{Jacques Jaray}
Ce compte n'existe pas, nous allons le créer
Adresse = \emph{Laxou}
Compte de [D]épôt (défaut) ou d'[E]pargne ? \emph{E}
Votre choix : [D]ébit, [C]rédit, [S]upprimer ? \emph{C}
Montant à créditer = \emph{100}
Compte d'épargne
Compte numéro 1 ouvert au nom de Jacques Jaray
Adresse du titulaire : Laxou
Le solde actuel du compte est de 100 euros.
************************************************
Donnez le nom du client (rien=exit) : 
Voici le nouvel état des comptes de l'agence
Compte d'épargne
Compte numéro 1 ouvert au nom de Karl Tombre
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 1 euros.
************************************************
Compte de dépôts
Compte numéro 2 ouvert au nom de Luigi Liquori
Adresse du titulaire : Bari
Le solde actuel du compte est de -20008 euros.
************************************************
Compte d'épargne
Compte numéro 1 ouvert au nom de Jacques Jaray
Adresse du titulaire : Laxou
Le solde actuel du compte est de 100 euros.
************************************************
-----------------------------------
On applique un traitement quotidien
-----------------------------------
Compte d'épargne
Compte numéro 1 ouvert au nom de Karl Tombre
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 1 euros.
************************************************
Compte de dépôts
Compte numéro 2 ouvert au nom de Luigi Liquori
Adresse du titulaire : Bari
Le solde actuel du compte est de -20016 euros.
************************************************
Compte d'épargne
Compte numéro 3 ouvert au nom de Jacques Jaray
Adresse du titulaire : Laxou
Le solde actuel du compte est de 100 euros.
************************************************
Sauvegarde effectuée
Et bien entendu, le fichier comptes.dat est à jour :
3
4
CompteEpargne
Karl Tombre
Sornéville
1
1
1.5E-4
CompteDepot
Luigi Liquori
Bari
2
-20016
4.1E-4
CompteEpargne
Jacques Jaray
Laxou
3
100
1.5E-4

6.4  Exceptions

Dans tous les exemples que nous donnons depuis le début de ce cours, nous avons plus d'une fois employé la construction try ... catch. Celle-ci est liée au traitement des exceptions en Java, c'est-à-dire des conditions d'erreur ou "anormales", pour une raison ou une autre. Presque toutes les erreurs à l'exécution déclenchent une exception, qui peut dans beaucoup de cas être "attrapée".

Java fournit un mécanisme permettant de regrouper le traitement de ces exceptions. Une exception signalée dans un bloc est propagée (instruction throw), d'abord à travers les blocs englobants, puis à travers la pile des appels de méthodes et fonctions, jusqu'à ce qu'elle soit "attrapée" et traitée par une instruction catch. En fait, les exceptions sont des objets, instances de la classe Throwable ou de l'une de ses sous-classes.

Dans beaucoup de langages, le traitement des conditions d'erreur est disséminé à travers le programme, ce qui en rend la compréhension malaisée. Java fournit les mécanismes permettant au contraire de les regrouper, ce qui permet de se concentrer d'un côté sur les instructions à effectuer pour le déroulement normal des opérations, et de l'autre des interventions à faire si quelque chose se passe mal.

Les trois mots clés try, catch et finally permettent de structurer votre code de la manière schématique suivante :
try {
    \emph{// Code qui peut provoquer des exceptions}
}
catch (UneException e1) {
    \emph{// Traitement de ce type d'exception}
}
catch (UneAutreException e2) {
    \emph{// Traitement de l'autre type d'exception}
}
finally {
    \emph{// Choses à faire dans tous les cas, même après une exception}
}
UneException et uneAutreException doivent être des sous-classes de Throwable.

6.4.1  Les derniers éléments du mystère

Ces informations vont nous permettre d'élucider complètement le mystère de la notation ésotérique qui vous a été imposée pour les lectures de chaînes de caractères. Revenons à notre étude de la fonction Utils.lireChaine (cf. § 6.3.1). Nous avons vu que br est une variable de type BufferedReader. Si nous allons voir cette classe dans le package java.io, nous trouvons :
public class BufferedWriter extends Writer {
    // ... plein de choses que je passe
    public void readLine() throws IOException {
        // ... le corps de la méthode readLine
    }
    // ... etc.
}
Le mot clé throws indique que la méthode readLine est susceptible de provoquer une exception de type IOException. Je ne peux donc qu'essayer (try) de l'appeler, et dire ce que je fais (catch) si une exception ioe de type IOException se produit -- en l'occurrence, rien du tout () :
        String reponse = "";
        try {
            reponse = br.readLine();
        } catch (IOException ioe) {}
        return reponse;
Nous avons bien entendu vu de nombreux autres exemples de cette construction, tout au long du polycopié.

6.4.2  Exemple : une méthode qui provoque une exception

Nous avons écrit au § 5.2 une classe PileEntiers dont l'une des méthodes pouvait provoquer une exception :
    public int depiler() throws EmptyStackException {
        if (! pileVide()) {
            --premierLibre; 
            return tab[premierLibre]; 
        }
        else {
            throw new EmptyStackException();
        }
    }
Si on essaie de dépiler une pile vide, on provoque une exception de type EmptyStackException -- l'une des sous-classes de Throwable -- grâce au mot clé throw. Vous noterez au passage que cette classe appartient au package java.util, qui n'est pas importé par défaut, ce qui nous oblige donc à une clause import.

Écrivons maintenant un petit programme utilisant une telle pile, dans lequel nous récupérerons cette erreur et en ferons quelque chose -- en l'occurrence, nous afficherons un message :
import java.util.*;

public class TestPile {
    public static void main(String[] args) {
        PileEntiers maPile = new PileEntiers();
        maPile.empiler(3);
        maPile.empiler(4);
        try {
            System.out.println("Je dépile " + maPile.depiler());
        } 
        catch(EmptyStackException ese) {
            System.out.println("Attention, la pile est vide");
        }
        try {
            System.out.println("Je dépile " + maPile.depiler());
        } 
        catch(EmptyStackException ese) {
            System.out.println("Attention, la pile est vide");
        }
        try {
            System.out.println("Je dépile " + maPile.depiler());
        } 
        catch(EmptyStackException ese) {
            System.out.println("Attention, la pile est vide");
        }
    }
}
Une exécution de ce programme va donner la trace suivante :
Je dépile 4
Je dépile 3
Attention, la pile est vide

6.5  La récursivité

Une procédure ou une fonction récursive s'appelle elle-même, directement ou indirectement. Elle découle souvent directement d'une formule de récurrence ; cela ne veut pas dire que toute formule de récurrence doit mener à une fonction récursive, car on peut éviter les récursivités inutiles, quand une construction itérative est plus claire.

Pour prendre un premier exemple simpliste, nous savons bien que la factorielle peut être définie par une formule de récurrence :
" n Î N,    n! =
ì
í
î
1 si    n £ 1
n × (n-1)! sinon

Cela peut se traduire directement en Java, sous forme d'une fonction récursive :
static int factorielle(int n) {
    /* n négatif non traité ici */
    if (n <= 1) {
        return 1;
    }
    else {
        return n * factorielle(n-1);
    }
}
En fait, le principe "diviser pour régner" est très souvent au coeur de l'analyse d'un problème complexe. La récursivité n'en est qu'un des avatars. Quand on use de cette stratégie, il est extrêmement important de s'assurer que :
  1. Tous les cas ont été pris en compte (on n'a pas "perdu des bouts" dans le processus de division).
  2. La division ne part jamais dans une boucle infinie (cas par exemple d'une récursivité mal analysée ou mal mise en oeuvre).
  3. Les sous-problèmes sont effectivement plus simples que le problème de départ !

6.5.1  Exemple : représentation d'un ensemble par un arbre

Revenons à la structure de données "arbre binaire" que nous avons commencé à esquisser au § 5.3. En effet, les manipulations d'arbres sont des exemples particulièrement frappants de la puissance d'une récursivité bien maîtrisée.

Pour tout ensemble E sur lequel on dispose d'une relation d'ordre total, il peut être judicieux d'utiliser un arbre binaire pour représenter la notion d'"ensemble d'éléments de E". En effet, on peut construire un tel arbre binaire de telle manière que lorsqu'on accède à sa racine, tous les noeuds du sous-arbre gauche (celui dont la racine est le fils gauche de la racine) contiennent des valeurs inférieures à la valeur contenue dans la racine, et tous les noeuds du sous-arbre droit contiennent des valeurs supérieures à celle de la racine. De ce fait, pour peu que l'arbre soit équilibré3, on peut tester l'appartenance d'un élément à l'ensemble représenté par l'arbre en un temps moyen de l'ordre de O(logn), où n est le nombre d'éléments de l'ensemble.

Illustrons notre propos en définissant une classe ArbreBinaire, s'appuyant sur la classe NoeudArbre que nous avons déjà vue (§ 5.3), qui permet de représenter un ensemble d'entiers. La seule variable d'instance dont on ait besoin ici est la racine, qui est un noeud, initialisé à null quand on crée un arbre nouveau :
public class ArbreBinaire  {
    private NoeudArbre racine;

    // Constructeur à partir de rien
    ArbreBinaire() {
        racine = null;
    }
Nous aurons également besoin de créer un arbre à partir d'un noeud donné, c'est-à-dire de considérer l'ensemble des noeuds rattachés directement ou indirectement à ce noeud comme un arbre. Nous définissons donc un constructeur à partir d'un noeud :
    // Constructeur à partir d'un noeud donné
    ArbreBinaire(NoeudArbre n) {
        racine = n;
    }
La première méthode, qui teste si l'ensemble est vide, consiste juste à tester si la racine est nulle :
    public boolean vide() {
        return racine == null;
    }
Dans les méthodes récursives de parcours de l'arbre que nous allons voir dans un instant, nous aurons besoin de récupérer les sous-arbres gauche et droit de l'arbre courant. Ici, nous utilisons le constructeur d'un arbre à partir d'un noeud, que nous venons de définir :
    public ArbreBinaire filsGauche() {
        if (racine == null) {
            return null;
        }
        else {
            return new ArbreBinaire(racine.filsGauche);
        }
    }

    public ArbreBinaire filsDroit() {
        if (racine == null) {
            return null;
        }
        else {
            return new ArbreBinaire(racine.filsDroit);
        }
    }
La méthode contient permet de tester l'appartenance d'un entier à l'ensemble représenté par l'arbre. Nous voyons ici la puissance de la récursivité. Connaissant les propriétés de l'arbre, nous pouvons écrire l'algorithme comme suit :
x Î A =
ì
ï
í
ï
î
faux si    A = Ø
vrai si    racine(A) = x
x Î filsDroit(A) si    x > racine(A)
x Î filsGauche(A) si    x < racine(A)

Cette formule de récurrence nous conduit directement à la méthode contient ; notez l'utilisation des méthodes filsGauche et filsDroit précédemment définies :
    public boolean contient(int x) {
        if (racine == null) {
            return false;
        }
        else if (racine.val == x) {
            return true;
        }
        else if (x > racine.val) {
            return filsDroit().contient(x);
        }
        else {
            return filsGauche().contient(x);
        }
    }
De même, la méthode d'ajout d'un élément à l'ensemble s'écrit de manière récursive, en s'inspirant de l'algorithme suivant :
A È x :
ì
ï
í
ï
î
racine(A) = x si    A = Ø
A si    x = racine(A)
filsGauche(A) È x si    x < racine(A)
filsDroit(A) È x si    x > racine(A)
Notez que la méthode ajouter rend un arbre, pour permettre de "raccrocher" les branches quand on revient des appels récursifs :
    public ArbreBinaire ajouter(int x) {
        if (racine == null) {
            // créer un nouveau n{\oe}ud
            NoeudArbre nouveau = new NoeudArbre(x); 
            racine = nouveau;
        }
        else if (x < racine.val) {
            racine.filsGauche = filsGauche().ajouter(x).racine;
        }
        else if (x > racine.val) {
            racine.filsDroit = filsDroit().ajouter(x).racine;
        }
        else {
            // dernier cas : il y est déjà : rien à faire
        }
        // Dans tous les cas, rendre l'arbre créé
        return this;
    }
La dernière méthode que nous allons voir permet d'afficher le contenu de l'ensemble, dans l'ordre qui correspond à la relation d'ordre utilisée dans la construction de l'arbre. Cette méthode est très simple : imprimer l'arbre, c'est
    public void print() {
        if (racine != null) {
            filsGauche().print();
            System.out.print(racine.val + ", ");
            filsDroit().print();
        }
    }
}
Voici maintenant un petit programme de test, qui illustre le fonctionnement de cette classe :
import java.util.*;

public class TestArbre {
    public static void main(String[] args) {
        ArbreBinaire monArbre = new ArbreBinaire();
        monArbre.ajouter(3);
        monArbre.ajouter(4);
        monArbre.ajouter(18);
        monArbre.ajouter(9);
        monArbre.ajouter(1);
        monArbre.ajouter(20);
        monArbre.ajouter(87);
        monArbre.ajouter(9);
        monArbre.ajouter(11);
        monArbre.ajouter(65);
        monArbre.ajouter(41);
        monArbre.ajouter(19);
        if (monArbre.contient(19)) {
            System.out.println("19 y est");
        }
        else {
            System.out.println("19 n'y est pas");
        }
        monArbre.print();
        System.out.println("");
    }
}
La trace d'exécution de ce programme est la suivante :
19 y est
1, 3, 4, 9, 11, 18, 19, 20, 41, 65, 87, 

Exercice 8   Fibonacci, de son vrai nom Léonard de Pise, est né à la fin du 12e siècle. Lors de nombreux voyages qui l'amenèrent en Égypte, en Syrie, en Grèce et en Sicile, il s'initia aux connaissances mathématiques du monde arabe. Quelque temps après son retour chez lui, à Pise, il publia en 1202 son célèbre Liber abbaci, dans lequel il tente de transmettre à l'Occident la science mathématique des Arabes et des Grecs. C'est dans cet ouvrage qu'il pose le problème suivant :

Si on part d'un couple de lapins, combien de couples de lapins obtiendra-t-on après un nombre donné de mois, sachant que chaque couple produit chaque mois un nouveau couple, lequel ne devient lui-même productif qu'à l'âge de deux mois. Autrement dit : La suite des nombres de couples de lapins est appelée suite de Fibonacci, et possède beaucoup de propriétés intéressantes. Elle est décrite par la formule de récurrence suivante :
Fn =
ì
í
î
1 si    n = 0    ou    1
Fn-1 + Fn-2 sinon

Écrire la fonction récursive correspondante.

6.6  Interface homme--machine et programmation événementielle

Au fur et à mesure que notre programme de gestion bancaire a grossi, le dialogue avec l'utilisateur est devenu de plus en plus complexe. Dans son état actuel, il souffre en fait d'un gros défaut : c'est le programme qui "prend l'utilisateur par la main" et qui lui pose les questions dans un certain ordre, alors qu'on souhaiterait bien entendu que l'utilisateur soit plus libre et que l'interface soit plus souple.

Cette interface textuelle est également bien "tristounette" à l'heure des environnements multi-fenêtrés, avec interactions à la souris et via des menus déroulants.

Bien entendu, Java offre toutes les fonctionnalités nécessaires pour créer de telles interfaces. Ce n'est que par souci d'une démarche pédagogique progressive que nous nous sommes cantonnés jusqu'ici à une interface aussi terne...

Dans ce dernier paragraphe du polycopié, nous allons vous donner un avant-goût de ce qu'est la programmation d'une vraie interface graphique. Il est évident que nous n'avons pas le temps de beaucoup étoffer notre programme, et l'exemple que nous allons développer reste très largement en-deçà des possibilités offertes par la bibliothèque de composantes graphiques Swing que nous allons utiliser.

Pour développer cette interface, nous allons devoir faire nos premiers pas en programmation événementielle. En programmation "classique", tout s'exécute sous le contrôle du programme, une fois qu'il est lancé ; en particulier, l'utilisateur doit répondre aux questions posées par le programme. En revanche, en programmation événementielle, le programme est en veille, attendant des stimuli externes, auxquels il réagit. C'est donc le programme qui doit réagir aux interactions de l'utilisateur, et non l'inverse.

Ces stimuli externes peuvent être de différentes natures : clic ou mouvement de souris, activation d'une touche, (dés)activation ou iconification d'une fenêtre, saisie d'un texte dans une zone d'interaction, signal arrivant sur un port de communication, etc.

Nous resterons fidèles à notre démarche tout au long du polycopié : au lieu de vous noyer sous les spécifications théoriques ou techniques, nous vous fournissons un exemple et vous invitons à "jouer" avec ce programme. Peut-être encore plus que beaucoup d'autres aspects, la programmation événementielle se prête bien à l'apprentissage par analogie ; avec un ou deux exemples sous les yeux, et un manuel des fonctionnalités offertes par la bibliothèque à portée de main (ou à portée de clic de souris, ces manuels étant habituellement en ligne), on est vite capable de mettre au point son propre programme interactif.

Tout d'abord, nous devons effectuer plusieurs modifications mineures dans les différentes classes que nous avons déjà conçues. En effet, les méthodes que nous avons écrites affichent parfois des messages ; or dans un système multi-fenêtré, nous n'avons plus de "console" sur laquelle écrire, et si on veut renvoyer un message il faut que la méthode en question renvoie une chaîne qui puisse être récupérée et affichée dans une fenêtre de dialogue.

À tout seigneur tout honneur -- commençons par la classe CompteBancaire. Peu de modifications ici : nous avons ajouté une méthode d'accès au numéro du compte, pour pouvoir l'afficher en dehors de la classe. Nous n'en définissons pas pour solde, qui est protected, puisque le programme interactif que nous allons écrire se trouvera dans le même package que CompteBancaire ; la variable est donc visible (cf. § 6.2). Par ailleurs, la méthode débiter rend désormais un message (type String) indiquant si le débit a été autorisé ou non (stricto sensu, je devrais faire la même chose pour créditer, car on peut imaginer des types de compte pour lesquels on ne peut pas dépasser un certain plafond, par exemple).
// Classe CompteBancaire - version 3.2

import java.io.*;

/**
 * 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 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++;
    }

    // Constructeur par défaut, sans paramètres
    CompteBancaire() {
        nom = "";
        adresse = "";
        numéro = 0;
        solde = 0;
    }

    // Les méthodes
    public void créditer(int montant) {
        solde += montant;
    }
    public String débiter(int montant) {
        solde -= montant;
        return "Débit accepté";
    }
    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;
    }

    public int numéro() {
        return this.numéro;
    }

    // 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();

    // sauvegarde du compte dans un BufferedWriter
    public void save(BufferedWriter bw) {
        Utils.saveString(bw, nom);
        Utils.saveString(bw, adresse);
        Utils.saveInt(bw, numéro);
        Utils.saveInt(bw, solde);
    }

    // chargement du compte à partir d'un BufferedReader
    public void load(BufferedReader br) {
        nom = Utils.loadString(br);
        adresse = Utils.loadString(br);
        numéro = Utils.loadInt(br);
        solde = Utils.loadInt(br);
    }
}
Les deux classes CompteEpargne et CompteDepot n'ont aussi que de légères modifications : les variables désignant les taux sont maintenant protected au lieu de private, et la modification de signature de la méthode débiter est répercutée :
// Classe CompteEpargne - version 1.2

import java.io.*;

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

public class CompteEpargne extends CompteBancaire {
    protected 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;
    }

    // Constructeur par défaut
    CompteEpargne() {
        super();
        tauxIntérêts = 0.0;
    }

    // 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 String débiter(int montant) {
        if (montant <= solde) {
            solde -= montant;
            return "Débit accepté";
        }
        else {
            return "Débit non autorisé";
        }
    }
    
    // Définition de la méthode de traitementQuotidien
    public void traitementQuotidien() {
        créditer((int) ((double) solde * tauxIntérêts));
    }
    // sauvegarde du compte dans un BufferedWriter
    public void save(BufferedWriter bw) {
        super.save(bw);
        Double d = new Double(tauxIntérêts);
        Utils.saveString(bw, d.toString());
    }

    // chargement du compte à partir d'un BufferedReader
    public void load(BufferedReader br) {
        super.load(br);
        tauxIntérêts = Double.valueOf(Utils.loadString(br)).doubleValue();
    }
}
// Classe CompteDepot - version 1.2

import java.io.*;

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

public class CompteDepot extends CompteBancaire {
    protected double tauxAgios;   // taux quotidien des agios

    // 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;
    }

    // Constructeur par défaut
    CompteDepot() {
        super();
        tauxAgios = 0.0;
    }

    // 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
    }
    
    // Définition de la méthode de traitementQuotidien
    public void traitementQuotidien() {
        if (solde < 0) {
            débiter((int) (-1.0 * (double) solde * tauxAgios));
        }
    }
    // sauvegarde du compte dans un BufferedWriter
    public void save(BufferedWriter bw) {
        super.save(bw);
        Double d = new Double(tauxAgios);
        Utils.saveString(bw, d.toString());
    }

    // chargement du compte à partir d'un BufferedReader
    public void load(BufferedReader br) {
        super.load(br);
        tauxAgios = Double.valueOf(Utils.loadString(br)).doubleValue();
    }
}
Dans l'interface ListeDeComptes, plusieurs méthodes changent de signature pour la même raison, et rendent désormais une chaîne de caractères, à savoir le message qui doit être affiché dans une fenêtre. Il s'agit des méthodes supprimer, sauvegarder et charger. Par ailleurs, nous ajoutons deux nouvelles méthodes, listeDesNoms, qui rend un tableau des noms des titulaires des comptes, et getData, qui rend un tableau d'objets représentant les différents champs de tous les comptes. Ces deux méthodes sont utiles pour certaines fonctionnalités de l'interface, comme nous allons le voir.
// Interface ListeDeComptes - version 1.2

import java.io.*;

/**
 * Interface représentant une liste de comptes et les opérations
 * que l'on souhaite effectuer sur cette liste
 * @author Karl Tombre
 */

public interface ListeDeComptes {
    // Récupérer un compte à partir d'un nom donné
    public CompteBancaire trouverCompte(String nom);
    // Ajout d'un nouveau compte
    public void ajout(CompteBancaire c);
    // Suppression d'un compte 
    public String supprimer(CompteBancaire c);
    // Afficher l'état de tous les comptes
    public void afficherEtat();
    // Traitement quotidien de tous les comptes
    public void traitementQuotidien();
    // Sauvegarder dans un fichier
    public String sauvegarder(File f);
    // Charger à partir d'un fichier
    public String charger(File f);
    // Liste des noms des titulaires de comptes
    public String[] listeDesNoms();
    // Récupérer toutes les données
    public Object[][] getData();
}
La classe AgenceBancaire doit bien entendu mettre en oeuvre toutes ces modifications apportées à l'interface. Vous noterez dans la méthode getData l'emploi de l'opérateur instanceof pour donner à un objet booléen la valeur ad hoc. Vous noterez également que comme nous devons passer un tableau d'objets, tous les types élémentaires (entier et booléen ici) sont convertis en objets (instances de Integer et Boolean ici).
// Classe AgenceBancaire - version 1.4

import java.io.*;

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

public class AgenceBancaire implements ListeDeComptes {
    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();
        }
    }

    // Suppression d'un compte
    public String supprimer(CompteBancaire c) {
        boolean trouvé = false;  // rien trouvé pour l'instant
        int indice = 0;
        for (int i = 0 ; i < nbComptes ; i++) {
            if (tabComptes[i] == c) { // attention comparaison de références
                trouvé = true; // j'ai trouvé
                indice = i;    // mémoriser l'indice
                break;         // plus besoin de continuer la recherche
            }
        }
        if (trouvé) {
            // Décaler le reste du tableau vers la gauche
            // On "écrase" ainsi le compte à supprimer
            for (int i = indice+1 ; i < nbComptes ; i++) {
                tabComptes[i-1] = tabComptes[i];
            }
            // Mettre à jour le nombre de comptes
            nbComptes--;
            return "Compte supprimé";
        }
        else {
            // Message d'erreur si on n'a rien trouvé
            return "Je n'ai pas trouvé ce compte";
        }
    }

    public String sauvegarder(File f) {
        BufferedWriter bw = Utils.openWriteFile(f);
        if (bw != null) {
            Utils.saveInt(bw, nbComptes);
            // Sauvegarder la variable de classe
            Utils.saveInt(bw, CompteBancaire.premierNuméroDisponible);
            for (int i = 0 ; i < nbComptes ; i++) {
                Class typeDeCompte = tabComptes[i].getClass();
                Utils.saveString(bw, typeDeCompte.getName());
                tabComptes[i].save(bw);
            }
            try {
                bw.close();
            }
            catch (IOException ioe) {}
            return "Sauvegarde effectuée";
        }
        else {
            return "Impossible de sauvegarder";
        }
    }
    public String charger(File f) {
        BufferedReader br = Utils.openReadFile(f);
        if (br != null) {
            nbComptes = Utils.loadInt(br);
            CompteBancaire.premierNuméroDisponible = Utils.loadInt(br);
            for (int i = 0 ; i < nbComptes ; i++) {
                try {
                    Class typeDeCompte = Class.forName(Utils.loadString(br));
                    tabComptes[i] = (CompteBancaire) typeDeCompte.newInstance();
                    tabComptes[i].load(br);
                }
                catch(ClassNotFoundException cnfe) {}
                catch(IllegalAccessException iae) {}
                catch(InstantiationException ie) {}
            }
            try {
                br.close();
            }
            catch (IOException ioe) {}
            return "Comptes chargés";
        }
        else {
            return "Impossible de charger la sauvegarde";
        }
    }
    // Rendre la liste des noms
    public String[] listeDesNoms() {
        String[] l = new String[nbComptes]; // créer un tableau de chaînes
        for (int i = 0 ; i < nbComptes ; i++) {
            l[i] = tabComptes[i].nom();
        }
        return l;
    }

    public Object[][] getData() {
        Object[][] theData = new Object[nbComptes][5];
        for (int i = 0 ; i < nbComptes ; i++) {
            theData[i][0] = tabComptes[i].nom();
            theData[i][1] = tabComptes[i].adresse();
            theData[i][2] = new Integer(tabComptes[i].numéro());
            // solde est protected et on y accède directement
            // puisqu'on est dans le même package
            theData[i][3] = new Integer(tabComptes[i].solde);
            theData[i][4] = (tabComptes[i] instanceof CompteEpargne) ?
                new Boolean(true) : new Boolean(false);
        }
        return theData;
    }
}
Toutes ces modifications étant faites, passons au programme interactif proprement dit. Celui-ci va être défini dans une classe GestionBanque ; comme vous pouvez le constater, tout se passe dans le constructeur, la procédure main ne contient qu'une ligne. Si vous comparez ce programme avec celui de la classe Banque que nous avons fait évoluer jusque là, vous remarquerez premièrement qu'à part les opérations de chargement à partir d'un fichier et de sauvegarde à la sortie du programme, on ne reconnaît plus du tout le programme. Mais en étudiant le programme de plus près, vous constaterez que toutes les anciennes fonctionnalités sont présentes ; seulement, nous suivons maintenant un style de programmation événémentielle4.
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;

/**
 * Classe GestionBanque, permettant de gérer les comptes d'une agence bancaire
 * à partir d'une interface Swing
 *
 * @author "Karl Tombre" <Karl.Tombre@loria.fr>
 * @version 1.0
 * @see ActionListener
 */

public class GestionBanque implements ActionListener {
    JFrame myFrame;           // la fenêtre principale
    ListeDeComptes monAgence; // l'agence bancaire

    /**
     * Constructeur - c'est ici que tout se passe en fait. On met en place
     * les éléments contenus dans la fenêtre et on active les événements.
     */
    
    GestionBanque() {
        // Créer une fenêtre
        myFrame = new JFrame("Gestion bancaire");  // titre de la fenêtre
        // arrêter tout si on ferme la fenêtre, pour éviter d'avoir un
        // processus qui continue à tourner en aveugle
        myFrame.addWindowListener(new WindowAdapter() {
                public void windowClosing(WindowEvent e) {
                    System.exit(0);
                }
            });

        // Création du panel principal
        JPanel mainPanel = new JPanel();
        // GridLayout(0, 2) --> 2 colonnes, autant de lignes que nécessaire
        mainPanel.setLayout(new GridLayout(0, 2)); 
        // une petite bordure de largeur 5
        mainPanel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));

        // On crée la liste des comptes et on charge la sauvegarde éventuelle
        monAgence = new AgenceBancaire();
        // On pourrait envisager de sélectionner le fichier via un FileChooser !
        File fich = new File("comptes.dat");
        monAgence.charger(fich);

        JOptionPane.showMessageDialog(mainPanel, monAgence.charger(fich));

        // Création des boutons et association avec les actions
        mainPanel.add(new JLabel("Gestion bancaire", JLabel.CENTER));
        JButton bQuitter = new JButton("Quitter");
        bQuitter.setVerticalTextPosition(AbstractButton.CENTER);
        bQuitter.setHorizontalTextPosition(AbstractButton.CENTER);
        bQuitter.setActionCommand("quitter");
        bQuitter.addActionListener(this);
        mainPanel.add(bQuitter);
        JButton bList = new JButton("Liste des comptes");
        bList.setVerticalTextPosition(AbstractButton.CENTER);
        bList.setHorizontalTextPosition(AbstractButton.CENTER);
        bList.setActionCommand("list");
        bList.addActionListener(this);
        mainPanel.add(bList);
        JButton bAjout = new JButton("Ajouter un compte");
        bAjout.setVerticalTextPosition(AbstractButton.CENTER);
        bAjout.setHorizontalTextPosition(AbstractButton.CENTER);
        bAjout.setActionCommand("ajout");
        bAjout.addActionListener(this);
        mainPanel.add(bAjout);
        JButton bCredit = new JButton("Créditer");
        bCredit.setVerticalTextPosition(AbstractButton.CENTER);
        bCredit.setHorizontalTextPosition(AbstractButton.CENTER);
        bCredit.setActionCommand("crédit");
        bCredit.addActionListener(this);
        mainPanel.add(bCredit);
        JButton bDebit = new JButton("Débiter");
        bDebit.setVerticalTextPosition(AbstractButton.CENTER);
        bDebit.setHorizontalTextPosition(AbstractButton.CENTER);
        bDebit.setActionCommand("débit");
        bDebit.addActionListener(this);
        mainPanel.add(bDebit);
        JButton bConsultation = new JButton("Consulter un compte");
        bConsultation.setVerticalTextPosition(AbstractButton.CENTER);
        bConsultation.setHorizontalTextPosition(AbstractButton.CENTER);
        bConsultation.setActionCommand("consulter");
        bConsultation.addActionListener(this);
        mainPanel.add(bConsultation);
        JButton bTraitement = new JButton("Traitement quotidien");
        bTraitement.setVerticalTextPosition(AbstractButton.CENTER);
        bTraitement.setHorizontalTextPosition(AbstractButton.CENTER);
        bTraitement.setActionCommand("traitement");
        bTraitement.addActionListener(this);
        mainPanel.add(bTraitement);


        //Ajouter le panel à la fenêtre et l'afficher
        myFrame.setContentPane(mainPanel);

        myFrame.pack();        // Taille naturelle
        myFrame.setVisible(true); // rendre visible
    }

    private CompteBancaire selectionCompte() {
        // Récupérer la liste des noms
        String[] l = monAgence.listeDesNoms();
        // Lancer un choix du compte sur le nom du titulaire
        ChoixCompte choix = new ChoixCompte(myFrame, l);
        // Quand on revient, on a choisi
        String nomChoisi = choix.value;
        // Rendre le compte correspondant
        return monAgence.trouverCompte(nomChoisi);
    }
        
    // Comme GestionBanque se déclare 'implements ActionListener'
    // et que c'est this qui a été donné comme ActionListener sur les
    // différents événements, c'est ici qu'il faut mettre la méthode
    // actionPerformed qui indique les actions à entreprendre suivant
    // les choix de l'utilisateur
    public void actionPerformed(ActionEvent e) {
        if (e.getActionCommand().equals("list")) {
            Object[][] t = monAgence.getData();
            // Afficher la table des comptes
            TableComptes tab = new TableComptes(myFrame, t);
        }
        else if (e.getActionCommand().equals("ajout")) {
            SaisieCompte s = new SaisieCompte(myFrame);
            // récupérer le compte saisi dans la variable c
            CompteBancaire c = s.leCompte;
            if (monAgence.trouverCompte(c.nom()) == null) {
                monAgence.ajout(c);
            }
            else {
                JOptionPane.showMessageDialog(myFrame, 
                                              "Impossible, ce nom existe déjà");
            }
        }
        else if (e.getActionCommand().equals("débit")) {
            CompteBancaire c = selectionCompte();
            ChoixMontant m = new ChoixMontant(myFrame, "débiter");
            int montant = m.montant;
            // afficher le message rendu par le débit
            JOptionPane.showMessageDialog(myFrame, c.débiter(montant));
        }
        else if (e.getActionCommand().equals("crédit")) {
            CompteBancaire c = selectionCompte();
            ChoixMontant m = new ChoixMontant(myFrame, "créditer");
            int montant = m.montant;
            c.créditer(montant);
        }
        else if (e.getActionCommand().equals("quitter")) {
            // Sauvegarder la base et afficher le message rendu
            try {
                File fich = new File("comptes.dat");
                if (!fich.exists()) {
                    FileOutputStream fp = 
                        new FileOutputStream("comptes.dat");
                    fich = new File("comptes.dat");
                }
                JOptionPane.showMessageDialog(myFrame, 
                                              monAgence.sauvegarder(fich));
            }
            catch (IOException ioe) {}
            myFrame.setVisible(false);
            System.exit(0);
        }
        else if (e.getActionCommand().equals("consulter")) {
            CompteBancaire c = selectionCompte();
            FicheCompte f = new FicheCompte(myFrame, c);
        }
        else if (e.getActionCommand().equals("traitement")) {
            monAgence.traitementQuotidien();
        }
    }

    // La méthode main est réduite à la création de la fenêtre
    public static void main(String[] args) {
        GestionBanque g = new GestionBanque();
    }
}
La figure 6.1 illustre l'interface ainsi créée.


Figure 6.1 : Interface créée par la classe GestionBanque.


Cette classe fait appel à un certain nombre d'autres classes, chacune gérant un type de fenêtre particulier. Les seules fenêtres non gérées par une classe explicitement créée sont les messages courts, que nous affichons dans une fenêtre de dialogue grâce à la méthode de classe JOptionPane.showMessageDialog (cf. Fig. 6.2).


Figure 6.2 : Exemple de fenêtre de dialogue bref.


La classe ChoixCompte est utilisée chaque fois que l'on souhaite choisir l'un des comptes existants, à partir de la liste des noms des titulaires :
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class ChoixCompte extends JDialog {
    // Le nom sélectionné
    public String value;
    private JList list;

    ChoixCompte(Frame f, String[] tabNoms) {
        super(f, "Choix d'un compte", true);
        final JButton okButton = new JButton("OK");
        okButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e)  {
                    value = (String) list.getSelectedValue();
                    setVisible(false);
                }
            });
        list = new JList(tabNoms);
        // Valeur par défaut
        if (tabNoms.length > 0) {
            value = tabNoms[0];
            list.setSelectedIndex(0); // Par défaut
        }
        else {
            value = "";
        }
        // On n'a le droit que de choisir un nom à la fois
        list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        list.addMouseListener(new MouseAdapter() {
                public void mouseClicked(MouseEvent e) {
                    if (e.getClickCount() == 2) {
                        okButton.doClick();
                    }
                }
            });
        // La liste peut être longue, donc mettre un scroll
        JScrollPane listScroller = new JScrollPane(list);
        // Créer un container avec un titre et la liste de choix
        JPanel listPane = new JPanel();
        listPane.setLayout(new BoxLayout(listPane, BoxLayout.Y_AXIS));
        JLabel label = new JLabel("Choisissez un compte");
        listPane.add(label);
        listPane.add(listScroller);
        listPane.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));

        // Mettre aussi le bouton
        JPanel buttonPane = new JPanel();
        buttonPane.add(okButton);

        // Mettre tout ça dans la fenêtre
        Container contentPane = getContentPane();
        contentPane.add(listPane, BorderLayout.CENTER);
        contentPane.add(buttonPane, BorderLayout.SOUTH);

        pack();
        setVisible(true);
    }
}


Figure 6.3 : Fenêtre ouverte par la classe ChoixCompte.


La classe TableComptes, quant à elle, affiche l'état de tous les comptes :
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import java.awt.*;
import java.awt.event.*;

public class TableComptes extends JDialog {
    
    class MyTableModel extends AbstractTableModel {
        final String[] nomsCol = {"Nom", "Adresse", "Numéro", "Solde", "Compte d'épargne"};
        Object[][] data;

        MyTableModel(Object[][] myData) {
            super();
            data = myData;
        }

        public int getColumnCount() {
            return nomsCol.length;
        }
        public int getRowCount() {
            return data.length;
        }
        public String getColumnName(int col) {
            return nomsCol[col];
        }
        public Object getValueAt(int row, int col) {
            return data[row][col];
        }
        public Class getColumnClass(int c) {
            return getValueAt(0, c).getClass();
        }
        // Interdire l'édition
        public boolean isCellEditable(int row, int col) {
            return false;
        }
    }
    
    TableComptes(Frame f, Object[][] myData) {
        super(f, "Liste des comptes", true);
        MyTableModel myModel = new MyTableModel(myData);
        JTable table = new JTable(myModel); 
        // Mettre dans un ascenseur 
        JScrollPane scrollPane = new JScrollPane(table);

        // Créer un panel pour mettre tout ça
        JPanel listPane = new JPanel();
        listPane.setLayout(new BoxLayout(listPane, BoxLayout.Y_AXIS));
        JLabel label = new JLabel("Liste des comptes");
        listPane.add(label);
        listPane.add(scrollPane);
        listPane.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));

        // Mettre un bouton OK
        final JButton okButton = new JButton("OK");
        okButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    setVisible(false);
                }
            });
        // Le mettre dans un panel
        JPanel buttonPane = new JPanel();
        buttonPane.add(okButton);

        // mettre le tout dans la fenêtre de dialogue
        Container contentPane = getContentPane();
        contentPane.add(listPane, BorderLayout.CENTER);
        contentPane.add(buttonPane, BorderLayout.SOUTH);

        pack();
        setVisible(true);
    }
}


Figure 6.4 : Fenêtre ouverte par la classe TableComptes.


La classe SaisieCompte crée une fenêtre de saisie, dans laquelle l'utilisateur peut entrer les données d'un nouveau compte qu'il veut créer. L'ajout lui-même reste dans GestionBanque, et on commence par vérifier que le nom du titulaire ne figure pas déjà dans la liste.
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;

public class SaisieCompte extends JDialog {
    public CompteBancaire leCompte;
    JTextField nom;
    JTextField adresse;
    JCheckBox épar;
    boolean épargne;

    SaisieCompte(Frame f) {
        super(f, "Saisie d'un nouveau compte", true);

        // Pour l'instant, pas de compte saisi
        leCompte = null;
        épargne = false;

        final JButton okButton = new JButton("OK");
        okButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    if (épargne) {
                        leCompte = new CompteEpargne(nom.getText(),
                                                     adresse.getText(),
                                                     0.00015);
                    }
                    else {
                        leCompte = new CompteDepot(nom.getText(),
                                                   adresse.getText(),
                                                   0.00041);
                    }
                    setVisible(false);
                }
            });
        
        // On crée un panel pour la saisie
        JPanel pane = new JPanel();
        pane.setLayout(new GridLayout(0, 2)); 

        pane.add(new JLabel("Nom"));
        nom = new JTextField(25);
        pane.add(nom);
        pane.add(new JLabel("Adresse"));
        adresse = new JTextField(25);
        pane.add(adresse);

        épar = new JCheckBox("Compte d'épargne");
        épar.setSelected(false);
        épar.addItemListener(new ItemListener() {
                public void itemStateChanged(ItemEvent e) {
                    if (e.getStateChange() == ItemEvent.SELECTED) {
                        épargne = true;
                    }
                    else {
                        épargne = false;
                    }
                }
            });
        pane.add(new JLabel("Type du compte"));
        pane.add(épar);
        
        pane.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));

        // Mettre aussi le bouton
        JPanel buttonPane = new JPanel();
        buttonPane.add(okButton);

        // Mettre tout ça dans la fenêtre
        Container contentPane = getContentPane();
        contentPane.add(pane, BorderLayout.CENTER);
        contentPane.add(buttonPane, BorderLayout.SOUTH);

        pack();
        setVisible(true);
        
    }
}


Figure 6.5 : Fenêtre ouverte par la classe SaisieCompte.


La classe ChoixMontant permet de saisir un montant à créditer ou à débiter :
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class ChoixMontant extends JDialog {
    // Le montant sélectionné
    public int montant;
    JTextField myTextField;

    ChoixMontant(Frame f, String objet) {
        super(f, "Choix du montant", true);
        final JButton okButton = new JButton("OK");
        okButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    montant = Integer.parseInt(myTextField.getText());
                    setVisible(false);
                }
            });
        montant = 0; // par défaut
        myTextField = new JTextField(10);
        myTextField.setText("0");
        // Créer un container avec un titre et le textfield
        JPanel tPane = new JPanel();
        tPane.setLayout(new GridLayout(0, 2));
        JLabel label = new JLabel("Montant à " + objet);
        tPane.add(label);
        tPane.add(myTextField);
        // Puis le bouton
        // Mettre aussi le bouton
        JPanel buttonPane = new JPanel();
        buttonPane.add(okButton);

        // Mettre tout ça dans la fenêtre
        Container contentPane = getContentPane();
        contentPane.add(tPane, BorderLayout.CENTER);
        contentPane.add(buttonPane, BorderLayout.SOUTH);

        pack();
        setVisible(true);
    }
}


Figure 6.6 : Fenêtre ouverte par la classe ChoixMontant.


Enfin, la classe FicheCompte crée une fenêtre où les détails d'un compte sont affichés :
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;

// Solution simple : FicheCompte est fortement inspiré de SaisieCompte
// mais avec des labels seulement
public class FicheCompte extends JDialog {
    FicheCompte(Frame f, CompteBancaire c) {
        super(f, "Fiche du compte sélectionné", true);

        // Le bouton OK de la fin
        final JButton okButton = new JButton("OK");
        okButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    setVisible(false);
                }
            });
        
        // On crée un panel
        JPanel pane = new JPanel();
        // 2 colonnes, autant de lignes qu'on veut
        pane.setLayout(new GridLayout(0, 2)); 

        pane.add(new JLabel("Nom "));
        pane.add(new JLabel(c.nom()));
        pane.add(new JLabel("Adresse "));
        pane.add(new JLabel(c.adresse()));
        pane.add(new JLabel("Numéro "));
        pane.add(new JLabel(Integer.toString(c.numéro())));
        pane.add(new JLabel("Solde "));
        pane.add(new JLabel(Integer.toString(c.solde)));

        if (c instanceof CompteDepot) {
            pane.add(new JLabel("Statut "));
            pane.add(new JLabel("Compte de dépôt"));

            pane.add(new JLabel("Taux quotidien des agios "));
            pane.add(new JLabel(Double.toString(((CompteDepot) c).tauxAgios)));
            }
        else {
            pane.add(new JLabel("Statut "));
            pane.add(new JLabel("Compte d'épargne"));

            pane.add(new JLabel("Taux quotidien des intérêts "));
            pane.add(new JLabel(Double.toString(((CompteEpargne) c).tauxIntérêts)));
        }
            
        pane.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));

        // Mettre aussi le bouton
        JPanel buttonPane = new JPanel();
        buttonPane.add(okButton);

        // Mettre tout ça dans la fenêtre
        Container contentPane = getContentPane();
        contentPane.add(pane, BorderLayout.CENTER);
        contentPane.add(buttonPane, BorderLayout.SOUTH);

        pack();
        setVisible(true);
        
    }
}


Figure 6.7 : Fenêtre ouverte par la classe FicheCompte.


6.7  Conclusion

Ils n'ont rien rapporté que des fronts sans couleur
Où rien n'avait grandi, si ce n'est la pâleur.
Tous sont hagards après cette aventure étrange ;
Songeur ! tous ont, empreints au front, des ongles d'ange,
Tous ont dans le regard comme un songe qui fuit,
Tous ont l'air monstrueux en sortant de la nuit !
On en voit quelques-uns dont l'âme saigne et souffre,
Portant de toutes parts les morsures du gouffre !
Victor Hugo

1
InputStream étant une classe abstraite, cette variable désigne forcément une instance d'une de ses sous-classes, mais nous n'y accédons que par le biais de l'interface d'InputStream.
2
La réflexivité peut se définir d'une manière générale comme la description du comportement d'un système qui entretient un rapport de cause à effet avec lui-même. Plus précisément, un système réflexif possède une structure, appelée auto-représentation, qui décrit la connaissance qu'il a de lui-même. Java possède certaines propriétés de réflexivité, notamment celles que nous utilisons ici pour demander à une classe son nom, ou pour récupérer un objet de type Class à partir de son nom.
3
L'arbre très simple que nous programmons dans l'exemple donné ici peut bien entendu être très déséquilibré, puisque sa configuration dépend fortement de l'ordre dans lequel on introduit les données. Mais il existe des versions plus élaborées de structures d'arbres où l'on rééquilibre l'arbre chaque fois qu'une adjonction ou une suppression menace de rompre l'équilibre.
4
En fait, on ne retrouve pas vraiment toutes les fonctionnalités : je vous ai laissé la programmation d'un bouton permettant de supprimer un compte, à titre d'exercice d'application !

Précédent Remonter Suivant