La programmation, c'est l'art d'organiser la complexité.
E. Dijkstra
Il est déjà possible d'écrire des programmes importants avec les
constructions de base que nous avons vues.
Cependant, il nous manque des outils permettant de structurer
nos programmes, pour maîtriser une complexité qui deviendrait sinon trop
grande pour permettre une gestion aisée par un être humain.
Comme nous l'avons déjà vu (cf. § 1.1), la
structuration peut être définie comme l'art de regrouper des entités
élémentaires en entités de plus haut niveau, qui puissent être
manipulées directement, permettant ainsi de s'affranchir de la
maîtrise des constituants élémentaires quand on travaille avec la
structure.
En programmation, cette structuration s'applique suivant deux axes :
-
On peut regrouper des opérations élémentaires, que l'on effectue
régulièrement, en entités auxquelles on donnera un nom et une
sémantique précise. C'est ce que nous verrons avec les fonctions et
les procédures (§ 3.2).
- On peut aussi regrouper des données, qui ensemble servent à
représenter un concept unitaire. Nous allons commencer par ce
premier type de structuration, en définissant la notion de classe
(§ 3.1).
3.1 La classe, première approche : un regroupement de
variables
Il est extrêmement fréquent que l'on manipule un concept complexe,
qui nécessite pour être représenté l'utilisation de plusieurs
variables.
Restons dans notre domaine d'application bancaire ; il est
clair que pour être utilisé, le programme que nous avons écrit doit
s'appliquer à un compte bancaire, qui est caractérisé au
minimum par :
-
le nom du titulaire du compte,
- l'adresse de celui-ci,
- le numéro du compte,
- le solde du compte.
On pourrait bien sûr avoir quatre variables indépendantes,
nom, adresse, numéro et solde,
mais cela n'est guère satisfaisant, car rien n'indique explicitement
que ces quatre variables sont "liées".
Que ferons-nous dans ces conditions pour nous y retrouver quand il
s'agira d'écrire le programme de gestion d'une agence bancaire, dans
laquelle il y a plusieurs centaines de comptes ?
La plupart des langages de programmation offrent donc la possibilité
de regrouper des variables en une structure commune et nommée, que
l'on peut ensuite manipuler globalement.
En Java, cette structure s'appelle la classe1.
Si je souhaite définir le concept de compte bancaire, je peux définir
une classe, avec le mot clé class.
Si j'appelle cette classe CompteBancaire, elle doit être
définie dans un nouveau fichier, CompteBancaire.java,
comme nous l'expliquons au paragraphe H.1.
Voici le contenu de ce fichier :
// Classe CompteBancaire - version 1.0
/**
* Classe permettant de regrouper les éléments qui décrivent un
* compte bancaire. Pas de programmation objet pour l'instant
* @author Karl Tombre
*/
public class CompteBancaire {
String nom; // le nom du client
String adresse; // son adresse
int numéro; // numéro du compte (int pour simplifier)
int solde; // solde du compte (opérations entières pour simplifier)
}
La définition d'une classe définit un nouveau type ; nous pouvons donc
maintenant déclarer une variable de ce nouveau type :
CompteBancaire monCompte
et travailler directement avec cette variable.
Cependant, il faut être capable d'accéder aux éléments individuels de
cette variable, par exemple le solde du compte.
La notation pointée permet de le faire :
monCompte.solde désigne le solde du compte représenté
par la variable monCompte.
NB : À ce stade, on notera qu'on peut considérer une classe comme un
produit cartésien.
3.1.1 Allocation de mémoire
Une classe peut en fait être considérée comme un "moule" ; la
classe CompteBancaire décrit la configuration type de tous
les comptes bancaires, et si on déclare deux variables :
CompteBancaire c1;
CompteBancaire c2;
chacune de ces variables possède son propre exemplaire de nom,
d'adresse, de numéro et de solde.
En effet, il ne serait pas normal que tous les comptes partagent le
même numéro ou le même solde !
Donc c1.numéro et c2.numéro, par exemple, sont deux
variables distinctes.
Une loi générale est que dans une "case" mémoire, on ne peut
stocker que des nombres.
Reste à savoir comment interpréter le nombre contenu dans une telle case ;
cela va bien entendu dépendre du type de la variable :
En présence de types scalaires comme des entiers, l'interprétation
est immédiate : le nombre contenu dans la case est la valeur de l'entier.
Pour les autres types simples (réels, caractères...) un codage
standard de la valeur par un nombre permet également une interprétation
immédiate (cf. annexe F).
Les choses se passent différemment pour toutes les variables de
types définis à partir d'une classe : ces variables, telles que
c1 et c2 dans notre exemple, sont techniquement des
références. Cela signifie que les cases mémoires correspondantes
ne "contiennent" pas directement les différentes données
nécessaires pour représenter un compte, mais uniquement l'adresse
d'un emplacement mémoire où un tel compte est représenté.
Autrement dit, leur r-valeur contient une l-valeur.
Pour réserver un emplacement mémoire effectif, il faut alors passer par une
opération d'allocation mémoire, grâce à l'instruction
new.
Aussi longtemps que celle-ci n'a pas été effectuée, la variable
monCompte, par exemple, a une valeur spécifique appelée
null, correspondant à une adresse indéfinie,
et il se produit une erreur si on essaye d'accéder à
monCompte.numéro.
Il faut donc faire cette allocation mémoire, par exemple au moment de
la déclaration de la variable :
CompteBancaire monCompte = new CompteBancaire();
avant de pouvoir continuer par exemple par :
monCompte.solde = 1000;
Aparté pour les petits curieux : que devient la mémoire qui
n'est plus utilisée ?
Si vous avez l'expérience de certains langages de programmation comme
Pascal, C ou C++, vous vous demandez peut-être comment procéder pour
"rendre" la mémoire affectée par new, une fois que vous
n'en avez plus besoin.
En effet, en plus d'un mécanisme similaire à new, ces
langages ont aussi une construction permettant de libérer de la
mémoire.
Rien de tel en Java ; ses concepteurs ont choisi de le munir d'un
mécanisme de ramasse-miettes (garbage collector en
anglais).
Un processus automatique, qui s'exécute en parallèle et en
arrière-plan, sans que l'utilisateur ait à intervenir, entreprend de
collecter, au fur et à mesure, les "cases mémoire" qui ne sont
plus référencées par aucune variable.
3.1.2 Exemple : un embryon de programme de gestion de compte
Pour illustrer notre propos, reprenons notre exemple bancaire.
Nous allons maintenant demander à l'utilisateur de donner les
coordonnées du compte, et toutes les manipulations se feront sur une
variable de type CompteBancaire.
Notez bien que nous travaillons maintenant avec deux fichiers,
Banque.java et CompteBancaire.java, selon un principe
général en Java, qui impose qu'à chaque classe corresponde un fichier
séparé :
// Classe Banque - version 2.0
/**
* Classe contenant un programme de gestion bancaire, utilisant
* cette fois-ci une variable de type CompteBancaire
* @author Karl Tombre
* @see CompteBancaire
*/
import java.io.*; // Importer toutes les fonctionnalités de java.io
public class Banque {
public static void main(String[] args) {
InputStreamReader ir = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(ir);
// On va travailler sur la variable monCompte
CompteBancaire monCompte = new CompteBancaire();
// Commencer par demander les valeurs des champs du compte
System.out.print("Nom du titulaire = ");
System.out.flush();
try {
monCompte.nom = br.readLine();
} catch (IOException ioe) {}
System.out.print("Son adresse = ");
System.out.flush();
try {
monCompte.adresse = br.readLine();
} catch (IOException ioe) {}
System.out.print("Numéro du compte = ");
System.out.flush();
String reponse = "";
try {
reponse = br.readLine();
} catch (IOException ioe) {}
monCompte.numéro = Integer.parseInt(reponse);
// Solde initial à 0
monCompte.solde = 0;
System.out.println(monCompte.nom +
", le solde initial de votre compte numéro " +
monCompte.numéro +
" est de " + monCompte.solde + " euros.");
boolean fin = false; // variable vraie si on veut s'arrêter
while(true) { // boucle infinie dont on sort par un break
System.out.print("Votre choix : [D]ébit, [C]rédit, [F]in ? ");
String choix = "";
try {
choix = br.readLine();
} catch (IOException ioe) {}
boolean credit = false; // variable vraie si c'est un crédit
// Récupérer la première lettre de la chaîne saisie
char monChoix = choix.charAt(0);
switch(monChoix) {
case 'C':
case 'c': // Même chose pour majuscule et minuscule
credit = true;
fin = false;
break; // Pour ne pas continuer en séquence
case 'd':
case 'D':
credit = false;
fin = false;
break;
case 'f':
case 'F':
fin = true;
break;
default:
fin = true; // On va considérer que par défaut on s'arrête
}
if (fin) {
break; // sortir de la boucle ici
}
else {
System.out.print("Montant à " +
(credit ? "créditer" : "débiter") +
" = ");
System.out.flush();
String lecture = "";
try {
lecture = br.readLine();
} catch (IOException ioe) {}
int montant = Integer.parseInt(lecture); // conversion en int
if (credit) {
monCompte.solde += montant;
}
else {
monCompte.solde -= montant;
}
System.out.println(monCompte.nom +
", le nouveau solde de votre compte numéro " +
monCompte.numéro +
" est de " + monCompte.solde + " euros.");
}
}
}
}
Et voici un résultat d'exécution :
Nom du titulaire = Dominique Scapin
Son adresse = 1, rue des gros sous, 75002 Paris
Numéro du compte = 182456
Dominique Scapin, le solde initial de votre compte numéro 182456 est de 0 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? C
Montant à créditer = 876234
Dominique Scapin, le nouveau solde de votre compte numéro 182456 est de 876234 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? D
Montant à débiter = 542
Dominique Scapin, le nouveau solde de votre compte numéro 182456 est de 875692 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? F
Exercice 3
Définir des classes qui permettent de représenter :
-
des points sur un plan,
- des points dans l'espace,
- des villes de France,
- des villes du monde.
3.2 Fonctions et procédures
3.2.1 Les fonctions -- approche intuitive
Vous avez probablement remarqué que chaque fois que nous effectuons
une lecture d'un entier, nous sommes obligés d'écrire plusieurs lignes
de code, toujours à peu près les mêmes, ce qui finit par alourdir le
programme.
Nous sommes typiquement en présence du besoin de structuration des
instructions : l'opération de lecture au clavier d'un entier, par
exemple, serait bien plus à sa place dans une fonction de
lecture qu'en ligne.
Java permet d'écrire de telles fonctions, qui prennent éventuellement
des paramètres en entrée -- ici nous choisirons la question à poser --
et qui rendent un résultat -- ici un entier.
Si vous avez bien suivi le dernier exemple que nous avons déroulé,
vous avez probablement remarqué en plus que pour lire un entier, il
faut d'abord lire une chaîne de caractères, puis convertir la chaîne
résultat de cette opération de lecture en un entier.
On peut donc écrire la fonction de lecture d'un entier à l'aide de la
fonction de lecture d'une chaîne de caractères.
Où les mettre ?
Nous avons déjà vu qu'en Java, tout doit être défini au sein d'une
classe.
Se pose alors la question de la classe dans laquelle définir ces
fonctions de lecture.
On pourrait tout simplement les ajouter à la classe Banque ;
mais elles sont d'un caractère assez général pour qu'elles puissent
aussi servir dans d'autres contextes que la gestion d'un compte
bancaire.
Nous choisissons donc de définir une nouvelle classe -- notre
troisième -- que nous appelons Utils et dans laquelle nous
regrouperons, au fur et à mesure que nous progressons, les fonctions
utilitaires qui peuvent servir dans nos programmes.
Voici donc une première version de cette classe, définie -- faut-il
le rappeler ? -- dans le fichier Utils.java :
// Classe Utils - version 1.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 :
// 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();
String reponse = "";
try {
reponse = br.readLine();
} catch (IOException ioe) {}
return reponse;
}
// La lecture d'un entier n'est qu'un parseInt de plus !!
public static int lireEntier(String question) {
return Integer.parseInt(lireChaine(question));
}
}
Continuons pour l'instant à prendre comme un acquis la syntaxe
public static ; nous y reviendrons pour en donner
l'explication (cf. § 4.3).
Vous remarquerez que la fonction lireChaine prend en entrée
une variable de type String, à savoir la question que l'on
souhaite poser, et rend une variable de type String
également, à savoir la réponse donnée par l'utilisateur à la question
posée.
Vous remarquerez aussi que la fonction lireEntier s'écrit
par un simple appel à la fonction précédente, lireChaine,
suivi d'un appel à une fonction de conversion d'une chaîne en un entier.
Vous avez donc noté que les fonctions que nous avons définies incluent
elles-mêmes des appels à d'autres fonctions définies dans les bibliothèque
standard de Java, telles que Integer.parseInt justement.
Enfin, vous noterez l'emploi du mot clé
return, pour indiquer la valeur qui est renvoyée
comme résultat de la fonction.
Reprenons maintenant notre programme bancaire et notons comment il se
simplifie grâce à l'emploi des fonctions.
Vous noterez entre autres que les variables rébarbatives de type
BufferedReader et autres n'ont plus lieu d'être déclarées
dans ce programme, puisqu'elles sont propres aux fonctions de lecture.
Notez aussi l'emploi dans ce programme de la notation pointée
Utils.lireEntier, pour indiquer dans quelle classe il faut
aller chercher la fonction lireEntier :
// Classe Banque - version 2.1
/**
* Classe contenant un programme de gestion bancaire, utilisant
* cette fois-ci une variable de type CompteBancaire
* @author Karl Tombre
* @see CompteBancaire
*/
public class Banque {
public static void main(String[] args) {
// On va travailler sur la variable monCompte
CompteBancaire monCompte = new CompteBancaire();
// Commencer par demander les valeurs des champs du compte
monCompte.nom = Utils.lireChaine("Nom du titulaire = ");
monCompte.adresse = Utils.lireChaine("Son adresse = ");
monCompte.numéro = Utils.lireEntier("Numéro du compte = ");
// Solde initial à 0
monCompte.solde = 0;
System.out.println(monCompte.nom +
", le solde initial de votre compte numéro " +
monCompte.numéro +
" est de " + monCompte.solde + " euros.");
boolean fin = false; // variable vraie si on veut s'arrêter
while(true) { // boucle infinie dont on sort par un break
String choix = Utils.lireChaine("Votre choix : [D]ébit, [C]rédit, [F]in ? ");
boolean credit = false; // variable vraie si c'est un crédit
// Récupérer la première lettre de la chaîne saisie
char monChoix = choix.charAt(0);
switch(monChoix) {
case 'C':
case 'c': // Même chose pour majuscule et minuscule
credit = true;
fin = false;
break; // Pour ne pas continuer en séquence
case 'd':
case 'D':
credit = false;
fin = false;
break;
case 'f':
case 'F':
fin = true;
break;
default:
fin = true; // On va considérer que par défaut on s'arrête
}
if (fin) {
break; // sortir de la boucle ici
}
else {
int montant = Utils.lireEntier("Montant à " +
(credit ? "créditer" : "débiter") +
" = ");
if (credit) {
monCompte.solde += montant;
}
else {
monCompte.solde -= montant;
}
System.out.println(monCompte.nom +
", le nouveau solde de votre compte numéro " +
monCompte.numéro +
" est de " + monCompte.solde + " euros.");
}
}
}
}
3.2.2 Les fonctions -- définition plus formelle
La notion de fonction rejoint en fait la fonction telle qu'elle est
connue en mathématiques.
Nous avons déjà vu les opérateurs du langage
(§ 2.4), qui définissent en fait des
fonctions intrinsèques ; par exemple + sur les entiers
correspond à la fonction2
+ : Z × Z ® Z
Plus généralement, une fonction func qui serait définie
mathématiquement par :
func : Z × Bool × Char ® R
aura comme définition en Java :
static double func(int x, boolean b, char c)
Cela s'étend à tous les types, aussi bien ceux qui sont prédéfinis que
ceux qui sont définis dans une bibliothèque ou par le programmeur.
Le passage des paramètres
Prenons un autre exemple : la fonction puissance entière d'un réel
pourra s'écrire par exemple :
static double puiss(double x, int n) {
/* Il manque ici un test de n positif */
double res = 1.0;
int p = 0;
while (p < n) {
p++;
res *= x;
}
return res;
}
Que se passe-t-il alors lors de l'appel de cette fonction ?
Le cas le plus simple est celui de l'utilisation de constantes ; on
peut par exemple écrire :
double x = puiss(5.4, 6);
ce qui va attribuer à x la valeur calculée de 5,46.
Mais on pourrait aussi écrire :
double r = Utils.lireReel("Donnez un nombre réel = ");
int p = Utils.lireEntier("Donnez un nombre entier = ");
double y = puiss(r, p);
en supposant bien entendu que nous ayons écrit également la fonction
lireReel pour lire au clavier une valeur réelle.
Dans ces deux cas, il est important de noter comment les valeurs sont
passées à la fonction.
Dans la définition de celle-ci, x et n sont appelés
les paramètres formels de la fonction.
L'appel puiss(5.4, 6) revient à affecter à x la
valeur 5.4 et à n la valeur 6, avant d'exécuter la
fonction.
De même, l'appel puiss(r, p) revient à faire les affectations
implicites
x (de la fonction) ¬ r (du monde
extérieur à la fonction)
et
n (de la fonction) ¬ p (du monde
extérieur à la fonction)
Il faut bien noter que cette affectation implicite revient à dire
qu'on passe la r-valeur des paramètres avec lesquels on appelle la
fonction -- ici r et p -- et que ceux-ci ne sont
donc pas modifiés par la fonction, qui ne "récupère" qu'une
copie de leur r-valeur (cf. § 2.5).
On dit que le passage des paramètres se fait par valeur, et en
Java c'est le seule mode de passage des paramètres3.
Une fois que la fonction a rendu une valeur au
"monde extérieur" (à celui qui l'a appelée), les paramètres
x et n de la fonction n'ont plus aucune existence --
ils n'en ont qu'au sein de la fonction.
3.2.3 Les procédures
La fonction correspond à une définition mathématique précise ; mais il
arrive aussi souvent qu'on soit amené à effectuer à plusieurs endroits
du programme une même opération, définie par un même ensemble d'instructions.
On souhaite là aussi regrouper ces instructions et leur donner un nom.
Une fois de plus, il est possible que ce regroupement d'instructions
reçoive un ou plusieurs paramètres, mais à la différence d'une
fonction, il ne "rend" aucune valeur particulière.
On appelle un tel regroupement une procédure. En Java, les
procédures sont définies suivant la même syntaxe que les fonctions ;
on se contente d'utiliser un mot clé particulier,
void, à la place du type rendu par la
fonction.
On peut donc dire par souci de simplicité, même si c'est en fait un
abus de langage, qu'en Java, une procédure est une fonction qui ne
retourne rien...
Continuons de simplifier notre programme bancaire.
Plusieurs informations sont maintenant associées au compte bancaire,
et chaque fois que l'on souhaite afficher son solde, il faut énumérer
ces informations.
Nous allons regrouper toutes les instructions nécessaires pour
afficher l'état d'un compte bancaire dans une procédure
étatCompte, à laquelle nous passerons en paramètre un compte
bancaire.
Nous en profitons pour améliorer l'affichage de cet état :
// Classe Banque - version 2.2
/**
* Classe contenant un programme de gestion bancaire, utilisant
* une variable de type CompteBancaire
* @author Karl Tombre
* @see CompteBancaire, Utils
*/
public class Banque {
public static void étatCompte(CompteBancaire unCompte) {
System.out.println("Compte numéro " + unCompte.numéro +
" ouvert au nom de " + unCompte.nom);
System.out.println("Adresse du titulaire : " + unCompte.adresse);
System.out.println("Le solde actuel du compte est de " +
unCompte.solde + " euros.");
System.out.println("************************************************");
}
public static void main(String[] args) {
CompteBancaire monCompte = new CompteBancaire();
// Commencer par demander les valeurs des champs du compte
monCompte.nom = Utils.lireChaine("Nom du titulaire = ");
monCompte.adresse = Utils.lireChaine("Son adresse = ");
monCompte.numéro = Utils.lireEntier("Numéro du compte = ");
// Solde initial à 0
monCompte.solde = 0;
// Afficher une première fois
étatCompte(monCompte);
boolean fin = false; // variable vraie si on veut s'arrêter
while(true) { // boucle infinie dont on sort par un break
String choix = Utils.lireChaine("Votre choix : [D]ébit, [C]rédit, [F]in ? ");
boolean credit = false; // variable vraie si c'est un crédit
// Récupérer la première lettre de la chaîne saisie
char monChoix = choix.charAt(0);
switch(monChoix) {
case 'C':
case 'c': // Même chose pour majuscule et minuscule
credit = true;
fin = false;
break; // Pour ne pas continuer en séquence
case 'd':
case 'D':
credit = false;
fin = false;
break;
case 'f':
case 'F':
fin = true;
break;
default:
fin = true; // On va considérer que par défaut on s'arrête
}
if (fin) {
break; // sortir de la boucle ici
}
else {
int montant = Utils.lireEntier("Montant à " +
(credit ? "créditer" : "débiter") +
" = ");
if (credit) {
monCompte.solde += montant;
}
else {
monCompte.solde -= montant;
}
// Afficher le nouveau solde
étatCompte(monCompte);
}
}
}
}
Une exécution de cette nouvelle version du programme done :
Nom du titulaire = Facture Portails
Son adresse = 165 rue M*crosoft, Seattle (WA), USA
Numéro du compte = 2000
Compte numéro 2000 ouvert au nom de Facture Portails
Adresse du titulaire : 165 rue M*crosoft, Seattle (WA), USA
Le solde actuel du compte est de 0 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? C
Montant à créditer = 189000
Compte numéro 2000 ouvert au nom de Facture Portails
Adresse du titulaire : 165 rue M*crosoft, Seattle (WA), USA
Le solde actuel du compte est de 189000 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? D
Montant à débiter = 654
Compte numéro 2000 ouvert au nom de Facture Portails
Adresse du titulaire : 165 rue M*crosoft, Seattle (WA), USA
Le solde actuel du compte est de 188346 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? F
3.2.4 Le cas particulier de main
Depuis le début, nous écrivons
public static void main(String[] args)
en début de programme.
Il est temps de l'expliquer.
En fait, un programme est une succession d'appels à des procédures et
des fonctions.
Il faut donc bien un "commencement", c'est-à-dire une procédure
qui appelle les autres, qui est à l'origine de
l'enchaînement des appels de procédures et de fonctions.
Cette procédure particulière s'appelle toujours main.
Elle prend en paramètre un tableau de chaînes de caractères, qui
correspond à des arguments avec lesquels on peut éventuellement
lancer le programme.
Nous n'utiliserons jamais cette faculté dans ce cours -- ce qui
signifie que ce tableau d'arguments est vide dans notre cas -- mais nous
sommes bien obligés de donner la déclaration exacte de main
pour que le système s'y retrouve...
En Java, rien n'interdit de définir plusieurs fonctions et procédures
portant le même nom, à condition que leurs signatures --
c'est-à-dire le nombre et le type de leurs paramètres formels --
soient différentes.
Ainsi, on pourra définir (par exemple dans la classe Utils) toutes
les fonctions et procédures suivantes, de nom max :
public static int max(int a, int b) {...}
public static float max(char a, char b) {...}
public static double max(double a, double b) {...}
public static void max(char c, int y) {...}
public static void max(String s, int y) {...}
public static char max(String s) {...}
À noter toutefois que Java interdit de définir deux fonctions ayant la même
signature mais des types de résultat différents, comme par exemple
public static int max(int a, int b) {...}
public static String max(int a, int b) {...}
Le type des paramètres à l'appel et la signature permettent au
compilateur de déterminer laquelle de ces fonctions ou procédures doit
être choisie lors de l'appel. Ainsi max(3, 4) va appeler la
première fonction, alors que max("Bonjour", 3) va
provoquer l'appel de la procédure de l'avant-dernière ligne.
Attention cependant à ne pas abuser de cette faculté ; pour des
raisons de lisibilité par vous-même et par d'autres humains, et pour
éviter les confusions, il vaut mieux n'utiliser la surcharge que dans
les cas où les différentes fonctions ou procédures de même nom
correspondent à la même opération sémantique, mais appliquée à des
types différents.
Exercice 4
Écrire en Java les 3 fonctions dont les définitions mathématiques sont
les suivantes :
|
max |
: Z × Z ® Z |
|
|
|
|
|
|
|
|
|
|
(n,m) |® n si n > m |
|
|
|
|
|
|
|
|
|
|
(n,m) |® m sinon |
|
|
|
|
|
|
|
|
|
f |
: Z ® Z |
|
|
|
|
|
|
|
|
|
|
n |® 2n + 3n |
|
|
|
|
|
|
|
|
|
se |
: R+ ® N |
|
|
|
|
|
|
|
|
|
|
x |® ë x û
|
|
|
|
|
|
|
|
|
|
|
3.3 Les tableaux
Le tableau est une structure de données de base que l'on retrouve dans
beaucoup de langages.
Il permet de stocker en mémoire un ensemble de valeurs de même type,
et d'y accéder directement.
À tout type T de Java correspond un type T[ ] qui indique un
tableau d'éléments de type T.
Pour créer un tableau, on fera une fois de plus appel à l'instruction
new, en précisant cette fois-ci le nombre d'éléments que doit
contenir le tableau. Ainsi, on écrira :
byte[] octetBuffer = new byte[1024];
pour définir un tableau de 1024 entiers codés sur un octet4.
On accède à un élément du tableau en donnant son indice, sachant que
l'indice du premier élément d'un tableau est toujours 0 en
Java !
Ainsi octetBuffer[0] désigne le premier élément,
octetBuffer[1] le deuxième, et octetBuffer[1023] le
dernier.
Par ailleurs, la taille d'un tableau est donnée par la construction
nomDuTableau.length : ainsi, dans l'exemple ci-dessus,
octetBuffer.length vaut 1024.
Attention ! Si vous déclarez un tableau d'objets,
c'est-à-dire de variables
déclarées d'un type défini par une classe, il ne faut pas oublier de
créer les objets eux-mêmes !
Imaginons que nous voulions gérer un tableau de 10 comptes bancaires.
En écrivant :
CompteBancaire[] tabComptes = new CompteBancaire[10];
on crée un tableau de 10 références à des comptes bancaires.
La variable tabComptes[0], par exemple, est de type
CompteBancaire, mais si je veux qu'elle désigne un objet
effectif et qu'elle ne vaille pas null, je dois également
écrire :
tabComptes[0] = new CompteBancaire();
comme nous l'avons vu précédemment.
Illustrons tout cela en faisant une fois de plus évoluer notre célèbre
programme bancaire.
Nous allons maintenant justement gérer 10 comptes dans un tableau.
Il faut donc commencer par initialiser ces 10 comptes.
Ensuite, le programme est modifié en demandant l'indice dans le
tableau avant de demander le montant à créditer ou débiter.
Notez que je devrais en fait vérifier que l'indice saisi est bien dans
les bornes valides, c'est-à-dire dans l'intervalle [0,10[.
En prime, cet exemple me donne l'occasion d'illustrer l'emploi d'une
itération avec la construction for :
// Classe Banque - version 2.3
/**
* Classe contenant un programme de gestion bancaire, utilisant
* un tableau de comptes bancaires
* @author Karl Tombre
* @see CompteBancaire, Utils
*/
public class Banque {
public static void étatCompte(CompteBancaire unCompte) {
System.out.println("Compte numéro " + unCompte.numéro +
" ouvert au nom de " + unCompte.nom);
System.out.println("Adresse du titulaire : " + unCompte.adresse);
System.out.println("Le solde actuel du compte est de " +
unCompte.solde + " euros.");
System.out.println("************************************************");
}
public static void main(String[] args) {
CompteBancaire[] tabComptes = new CompteBancaire[10];
// Initialisation des comptes
for (int i = 0 ; i < tabComptes.length ; i++) {
// D'abord créer le compte !!
tabComptes[i] = new CompteBancaire();
// Et puis lire les données
tabComptes[i].nom = Utils.lireChaine("Nom du titulaire = ");
tabComptes[i].adresse = Utils.lireChaine("Son adresse = ");
tabComptes[i].numéro = Utils.lireEntier("Numéro du compte = ");
// Solde initial à 0
tabComptes[i].solde = 0;
}
boolean fin = false; // variable vraie si on veut s'arrêter
while(true) { // boucle infinie dont on sort par un break
String choix = Utils.lireChaine("Votre choix : [D]ébit, [C]rédit, [F]in ? ");
boolean credit = false; // variable vraie si c'est un crédit
// Récupérer la première lettre de la chaîne saisie
char monChoix = choix.charAt(0);
switch(monChoix) {
case 'C':
case 'c': // Même chose pour majuscule et minuscule
credit = true;
fin = false;
break; // Pour ne pas continuer en séquence
case 'd':
case 'D':
credit = false;
fin = false;
break;
case 'f':
case 'F':
fin = true;
break;
default:
fin = true; // On va considérer que par défaut on s'arrête
}
if (fin) {
break; // sortir de la boucle ici
}
else {
int indice = Utils.lireEntier("Indice dans le tableau des comptes = ");
int montant = Utils.lireEntier("Montant à " +
(credit ? "créditer" : "débiter") +
" = ");
if (credit) {
tabComptes[indice].solde += montant;
}
else {
tabComptes[indice].solde -= montant;
}
// Afficher le nouveau solde
étatCompte(tabComptes[indice]);
}
}
}
}
Une exécution de ce programme donne la trace suivante :
Nom du titulaire = Karl
Son adresse = Sornéville
Numéro du compte = 123
\emph{... Je vous passe un certain nombre de lignes - c'est fastidieux}
Nom du titulaire = Bart
Son adresse = Bruxelles
Numéro du compte = 76243
Votre choix : [D]ébit, [C]rédit, [F]in ? D
Indice dans le tableau des comptes = 4
Montant à débiter = 8725
Compte numéro 524 ouvert au nom de François
Adresse du titulaire : Strasbourg
Le solde actuel du compte est de -8725 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? C
Indice dans le tableau des comptes = 9
Montant à créditer = 8753
Compte numéro 76243 ouvert au nom de Bart
Adresse du titulaire : Bruxelles
Le solde actuel du compte est de 8753 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? C
Indice dans le tableau des comptes = 0
Montant à créditer = 7652
Compte numéro 123 ouvert au nom de Karl
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 7652 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? F
NB : Habituellement, on n'initialise pas un tableau au moment de
sa création, mais on le "remplit" comme dans l'exemple que nous venons
de donner.
Cependant, il y a des cas où l'on souhaite initialiser un tableau ; on
donnera alors entre accolades la liste des valeurs initiales, séparées par
des virgules, comme l'illustre l'exemple suivant, où l'on
définit un tableau de taille 12, initialisé directement par 12 valeurs (la
taille du tableau est déduite par le compilateur du nombre de valeurs
données en initialisation) :
static String[] moisEnFrançais = {"janvier", "février", "mars", "avril",
"mai", "juin", "juillet", "août",
"septembre", "octobre", "novembre",
"décembre"};
Exercice 5
Écrire les fonctions :
-
max(int[] tab) qui retourne le plus grand élément d'un
tableau d'entiers ;
- somme(int[] tab) qui calcule la somme des éléments d'un
tableau d'entiers ;
- estALIndice(int[] tab, int n) qui retourne -1 si
n n'est pas dans le tableau tab, et le premier indice
dans tab auquel se trouve n sinon.
- 1
- En fait, vous comprendrez au chapitre 4 que
la classe est bien plus que cela, puisque c'est l'élément de base de
la programmation objet. Mais nous nous contenterons dans un premier
temps de cette vision simplifiée de la classe, qui la rend analogue
à un RECORD en Pascal, par exemple.
- 2
- En toute rigueur, du fait du codage des entiers sur un ensemble fini
de bits, on opère en Java sur une partie finie de Z.
- 3
- Dans d'autres langages, on a parfois le choix entre passage par
valeur -- passage de la r-valeur -- et passage par référence. Dans
ce deuxième cas, on passe en fait la l-valeur du paramètre d'appel à
la fonction, et celle-ci peut donc agir directement sur la variable
passée en paramètre, et non seulement sur ses propres variables, qui
désignent les paramètres formels.
- 4
- En fait, on peut aussi écrire byte octetBuffer[] ;
c'est-à-dire mettre les crochets après le nom de la variable et non
après le type. Si vous êtes amené plus tard à "jongler" entre
les langages C++ et Java, vous préférerez sans doute cette seconde
solution, pour éviter la dyslexie, car c'est la seule notation
autorisée en C++. Néanmoins, je conseille plutôt d'utiliser la
notation donnée ici, qui a le mérite de désigner clairement la
variable octetBuffer comme étant de type byte[],
c'est-à-dire tableau d'octets.