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");
}
}
-
La variable booléenne trouvé existe tout au long de
cette méthode ; elle est donc créée quand la méthode est lancée, et
détruite à la sortie de la méthode.
- Il y a deux variables entières nommées i, qui sont
créées chacune à un moment différent et qui n'existent qu'au sein du
bloc dans lequel elles ont été déclarées. Les deux blocs
correspondants sont :
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
}
}
et
for (int i = indice+1 ; i < nbComptes ; i++) {
tabComptes[i-1] = tabComptes[i];
}
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 :
-
On peut lire au clavier et écrire sur l'écran, mais aussi sur
une imprimante ; on peut lire et écrire à partir d'un fichier, ou
d'une adresse mémoire correspondant à un flux de données en
provenance d'un réseau par exemple, etc.
- On peut avoir envie de lire ou écrire des caractères (en Java,
souvenez-vous qu'ils sont représentés sur 2 octets), ou un flot
d'octets, ou des objets plus structurés.
- On peut avoir des entrées et sorties "bufferisées",
c'est-à-dire gérées ligne par ligne. C'est typiquement le cas dans
des programmes interactifs simples, où vous saisissez vos réponses
au programme au clavier, en souhaitant qu'elles ne soient prises
en compte qu'une fois que vous tapez "Entrée". Jusque là, vous
êtes libres de revenir en arrière, d'effacer un caractère, etc. Les
caractères que vous saisissez sont stockés dans une zone tampon (un
buffer -- d'où le nom). Dans d'autres cas,
au contraire, on ne veut surtout pas que le caractère "Entrée" ait
une signification particulière ; dans les flots à partir de fichiers, par
exemple, on veut prendre en compte et traiter immédiatement
chaque caractère entré (imaginez qu'on vous demande d'écrire un
traitement de texte...)
À 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 :
-
println pour écrire une chaîne de caractères
(String) en vidant le buffer, et passer à la ligne,
- print qui fait la même chose sans passer à la ligne,
c'est-à-dire en écrivant dans le buffer,
- flush qui force l'affichage du buffer de sortie sans
qu'il y ait eu de retour à la ligne.
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 :
-
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.
- 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 :
-
Tous les cas ont été pris en compte (on n'a pas "perdu des
bouts" dans le processus de division).
- La division ne part jamais dans une boucle infinie (cas par
exemple d'une récursivité mal analysée ou mal mise en oeuvre).
- 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
-
imprimer le sous-arbre gauche (appel récursif),
- imprimer la valeur de la racine,
- imprimer le sous-arbre droit (appel récursif).
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 :
-
Au début : 1 couple
- Au bout de 1 mois : 1 couple
- Au bout de 2 mois : 2 couples
- Au bout de 3 mois : 3 couples
- Au bout de 4 mois : 5 couples
- Au bout de 5 mois : 8 couples
- Au bout de 6 mois : 13 couples...
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 !