Chapitre 2 Les constructions de base en Java
... conduire par ordre mes pensées, en commençant par les
objets les plus simples et les plus aisés à connaître, pour monter
peu à peu, comme par degrés, jusques à la connaissance des plus
composés ; et supposant même de l'ordre entre ceux qui ne se
précèdent point naturellement les uns les autres.
Descartes
2.1 Constantes et variables
La manière la plus simple de manipuler des valeurs dans un programme
est d'écrire directement ces valeurs. Ainsi, on pourra écrire
24 pour désigner un nombre entier, 176.54 pour
désigner le nombre réel correspondant, 2.54E+12 pour désigner
le nombre 2.54 × 1012, 'a' pour indiquer un
caractère, "Jean-Paul" pour désigner une chaîne de
caractères, et true pour indiquer la valeur booléenne "vrai".
Toutes ces valeurs sont des constantes. Je peux par exemple demander à
un programme d'afficher le résultat de l'opération
1+1.
Le seul problème est bien entendu que si je souhaite effectuer une
autre opération, il faudra modifier le programme, par exemple en
écrivant 1+2.
Or l'un des intérêts de la programmation est justement de pouvoir
décrire une démarche opératoire qui reste la même alors que les
données du problème peuvent changer.
Par exemple, il est clair que l'opération "créditer un compte
bancaire" correspond au même algorithme et au même programme, quels que
soient le solde initial du compte et le montant dont on doit le
créditer.
Il est donc indispensable de ne pas rester limité à l'emploi de
constantes dans les programmes informatiques.
D'un point de vue algorithmique, la variable est un objet qui a
un nom (l'identificateur de la variable) et, au cours de sa vie, une valeur
à chaque instant t donné.
Concrètement, au sein de l'ordinateur, la variable va correspondre à
un emplacement mémoire dans lequel on peut stocker une valeur.
Quand on écrit un programme, le
traducteur (compilateur ou interprète) associe à chaque variable
l'adresse de l'emplacement mémoire correspondant et réalise
l'adressage indirect nécessaire pour accéder en lecture ou en écriture
à cet emplacement.
Il est bon de prendre l'habitude dès le début de donner à ses
variables des noms mnémotechniques.
Bien entendu, pour l'ordinateur, il est strictement équivalent que
vous appeliez vos variables a, b, c, d, i1, i2, i3 ou
soldeDuCompte, montantACrediter, ageDuCapitaine, nomDuClient ;
mais vous comprenez aisément que lorsqu'un programme dépasse quelques
dizaines de lignes, il est beaucoup plus facile pour un être humain de
comprendre, de modifier et de maintenir un programme utilisant le
deuxième type de noms de variables.
Revenons aux constantes.
Nous avons vu qu'une valeur constante peut être écrite telle quelle
dans un programme, par exemple 19.6 ou "Bonjour".
Mais dans bien des cas, il est préférable de donner un nom symbolique
aux constantes également, comme pour les variables.
Pourquoi ?
Prenons l'exemple d'un calcul de prix ttc. À l'heure où
j'écris ces lignes, le taux de tva standard est de 19,6%.
Si donc je fais tous mes calculs de prix ttc dans mon
programme en multipliant les variables désignant le prix hors taxe par
1.196, je serai dans le pétrin le jour où le gouvernement relève ou
baisse le taux de la tva.
Il me faudra chercher toutes les occurrences du nombre 1.196
dans le programme, vérifier qu'elles correspondent bien à un calcul de
tva, et faire les modifications.
Il est donc bien plus judicieux de désigner la constante par un nom,
comme pour les variables.
C'est ce que permet le mot clé final, grâce
auquel on écrira :
final double tauxTVA = 1.196;
pour indiquer que tauxTVA est une constante de type réel
(cf. § 2.3).
Tous les calculs se feront avec cette constante nommée, et quand le
taux de tva changera, il y aura une seule ligne à modifier dans le
programme...
De même, en cas de programme multilingue, on pourra par exemple
écrire :
final String salutation = "Bonjour";
ou au contraire
final String salutation = "Guten Tag";
ou
final String salutation = "Buenos dias";
2.2 Typage
Le type d'une expression ou d'une variable indique le domaine des valeurs
qu'elle peut prendre et les opérations qu'on peut lui appliquer.
En Java comme dans la plupart des langages évolués, toute variable
doit être déclarée avec un type donné. Ce peut être un des types de
base du langage, ou un type construit avec les outils de structuration
fournis par le langage. Toute expression valide a aussi un type, et le
résultat d'une opération doit a priori être affecté à une
variable du type correspondant.
Il est relativement aisé de donner une interprétation ensembliste à la
notion de type. Nous aurons l'occasion de le faire plus d'une fois,
en particulier quand nous aborderons la notion de classe
(cf. § 3.1 et 4.1).
-
On dispose d'ensembles de base, auxquels correspondent en Java
des types de bases1 :
-
R : double, float
- Z : byte, short, int, long
- Bool : boolean
- caractères : char
- On peut définir de nouveaux ensembles : si on a besoin d'un
ensemble de couleurs, on pourra écrire quelque chose du genre
class Couleur, comme nous le verrons ultérieurement. Cette
capacité à définir de nouveaux ensembles ou types existe dans la grande
majorité des langages de programmation.
- On peut définir des produits d'ensemble, comme par exemple
Couleur × Bool ou Z × Z ×
R. Nous verrons que les classes permettent en particulier ce
genre de construction.
- On peut définir des fonctions, comme par exemple f : Zp
× Z ® Bool, qui s'écrira en Java
boolean f(int a, int b).
- Grâce aux listes et autres collections, on peut définir des
séquences d'objets.
La plupart des langages évolués offrent les constructions nécessaires
pour mettre en oeuvre les opérations ensemblistes de base que sont le
produit de deux ensembles (et l'accès aux fonctions de projection
associées), les fonctions et opérateurs, et la séquence ordonnée ou
non d'éléments d'un ensemble.
2.3 Types élémentaires en Java
Le langage Java définit un certain nombre de types élémentaires ; les
variables déclarées avec ces types correspondent des "cases
mémoire" en adressage direct. La taille en mémoire de ces cases est
définie par le type.
-
boolean : booléen, peut valoir true ou
false
- byte : entier sur 8 bits [-128, 127]
- char : caractère Unicode
(cf. § F.1) codé sur 16 bits
- short : entier codé sur 16 bits
[-32 768, 32 767] (cf. § F.2)
- int : entier codé sur 32 bits
- long : entier codé sur 64 bits
- float : réel codé sur 32 bits, au format
IEEE 754 (cf. § F.3)
- double : réel double précision codé sur 64 bits, au
format IEEE 754
Voici quelques exemples de déclarations et d'initialisations valides
en Java :
boolean b = true;
int i = 3;
b = (i != 0);
char c = 'A';
char newline = '\n';
char apostrophe = '\'';
char delete = '\377';
char aleph = '\u05D0';
long somme = 456L;
float val = -87.56;
float cumul = 76.3f;
double pi = 3.14159;
double large = 456738.65D;
double veryLarge = 657E+234;
Attention : comme l'indiquent les exemples ci-dessus, en Java,
toute variable doit être déclarée (avec indication de son type) une et une
seule fois, avant d'être utilisée.
Exercice 1
Quelles sont les erreurs de syntaxe
dans les lignes qui suivent ?
boolean b;
double a2 = 5.2;
int k = 5;
int j = k;
int i = 7;
char a = i;
int k = 7;
int l = t;
int t = 7;
2.4 Expressions
Java offre un jeu riche d'opérateurs pour écrire toutes sortes
d'expressions arithmétiques et logiques.
Des règles de priorité entre opérateurs permettent d'écrire les
expressions de la manière la plus "naturelle" possible ; par
exemple, a + b * c équivaut à a + (b * c) et non à
(a + b) * c.
Ceci étant, en cas de doute, rien ne vous empêche de parenthéser vos
expressions.
2.4.1 Opérateurs arithmétiques
Les opérateurs arithmétiques unaires sont + et - ;
par exemple, -a équivaut à -1 * a.
Les opérateurs arithmétiques binaires sont présentés dans le tableau
ci-après.
Op. |
Exemple |
Description / remarques |
+ |
a + 5 |
addition -- ou concaténation de chaînes
dans le cas particulier de String (cf. § 4.4) |
- |
b - c |
soustraction |
* |
13 * x |
multiplication |
/ |
a / b |
division -- attention, si les
opérandes sont entiers, il s'agit de la division euclidienne ! Ceci
est une source connue d'erreurs chez les débutants. |
% |
12 % 5 |
modulo (reste de la division euclidienne) |
Exercice 2
Donner la valeur des variables après les instructions suivantes :
int i = 13;
int j = 5;
i = i % 5;
j = j * i + 2;
int k = (i * 7 + 3) / 5;
2.4.2 Opérateurs logiques et relationnels
Tout d'abord, si vous n'avez jamais vu l'algèbre de Boole dans vos études,
je vous conseille de lire au préalable l'annexe B, qui vous
donne une courte introduction à la notion même d'opérations et de fonctions
logiques.
Un opérateur relationnel compare deux valeurs.
Les opérateurs logiques permettent d'opérer sur des valeurs
logiques, habituellement obtenues grâce à l'application d'opérateurs
relationnels.
Les opérateurs relationnels de Java sont les suivants :
Op. |
Exemple |
Description /
remarques |
> |
a > 0 |
a > 0 |
>= |
b >= c |
b ² c |
< |
a < 0 |
a < 0 |
<= |
a <= b |
a £ b |
== |
a == b |
test d'égalité a=b -- attention, ne
pas confondre avec l'affectation d'une valeur à une variable
(cf. § 2.5),
c'est une source d'erreur fréquente, qui peut avoir des conséquences
catastrophiques ! |
!= |
a != b |
test d'inégalité a ¹ b |
instanceof |
a instanceof C |
test d'appartenance à une
classe (cf. p. ??). |
Les opérateurs logiques disponibles sont les suivants :
Op. |
Exemple |
Description /
remarques |
&& |
(a>0) && (b<0) |
et logique -- le
deuxième opérande n'est évalué que si le premier opérande est
vrai |
|| |
(a>0) || (b<0) |
ou logique -- le
deuxième opérande n'est évalué que si le premier opérande est
faux |
! |
!(b >= c) |
non logique |
& |
(a>0) & (b<0) |
et logique -- les
deux opérandes sont toujours évalués -- Utilisation déconseillée |
| |
(a>0) | (b<0) |
ou logique -- les
deux opérandes sont toujours évalués -- Utilisation déconseillée |
Je vous conseille de prendre l'habitude d'utiliser par défaut les
versions && et || des opérateurs et/ou.
Java offre également un opérateur à trois opérandes, qui permet de
construire des expressions conditionnelles du type
c ? a : b ; celle-ci donne comme résultat le
second opérande (a) si le premier opérande (c) est vrai, et le troisième
opérande (b) dans le cas contraire. Ainsi, (x > y) ? x : y est
une expression qui a pour valeur max(x,y).
2.4.3 Opérateurs bit à bit
Ce sont des opérateurs qui permettent d'effectuer des opérations au
niveau des bits. Bien que moins fréquemment utilisés, nous les
mentionnons pour mémoire ; mais ne les utiliserons pas dans ce cours.
Op. |
Exemple |
Description /
remarques |
>> |
17 >> 3 |
décalage de 3 bits sur la droite --
ici le résultat est 2 |
>>> |
a >>> 5 |
toujours un décalage de bits sur
la droite (ici 5 bits), mais en version non signée |
<< |
0x03 << 2 |
décalage de 2 bits sur la gauche
-- je profite de cet exemple pour introduire la notation
hexadécimale des nombres (10 s'écrit A, 11 s'écrit
B, ... 15 s'écrit F) ; le résultat de l'exemple
est 0x0C |
& |
0x75 & 0xF0 |
et bit à bit -- le
résultat de l'exemple donné est 0x70 |
| |
0x75 | 0xF0 |
ou bit à bit -- le
résultat de l'exemple donné est 0xF5 |
^ |
0x75 ^ 0xF0 |
ou exclusif (xor) bit à
bit -- le résultat de l'exemple donné est 0x85 |
~ |
~0xF0 |
complément bit à bit -- le résultat de l'exemple donné est
0x0F |
2.5 L'affectation
Nous avons vu (cf. § 2.1) qu'à une variable correspond un
emplacement mémoire. Pour simplifier les explications qui vont suivre,
considérons la mémoire comme une table M ; nous supposerons que la
variable de nom x est stockée à la "case" M[i].
On appelle l-valeur (l-value, c'est-à-dire
left-value) de la variable x son adresse en
mémoire, dans notre cas i. On appelle r-valeur (r-value,
c'est-à-dire right-value) de x la valeur stockée à cet
emplacement, dans notre cas M[i].
L'affectation est une opération fondamentale dans les langages
de programmation impératifs, dont Java fait partie ; elle consiste à
changer la r-valeur d'une variable, c'est-à-dire à stocker une
nouvelle valeur à l'emplacement désigné par la l-valeur de la
variable.
De manière conceptuelle, on la note habituellement x
¬ y, qui signifie qu'on stocke la r-valeur de y
à l'adresse désignée par la l-valeur de x.
En Java, cette opération s'écrit x = y.
Attention -- on ne pose pas ici une égalité, et on n'établit
pas de lien pérenne entre les deux variables x et y.
On se contente, à un instant précis, de recopier la valeur stockée
dans y à l'adresse de x. Ce qui suit illustre ce
point important :
int x = 3;
int y = 5; // x == 3, y == 5
x = y; // x == 5, y == 5
y = 6; // x == 5, y == 6 : x et y ne sont pas des variables liées !
Il est très fréquent d'ajouter ou de soustraire une valeur à une
variable, par exemple d'écrire a = a + 3;2.
Java vous offre donc la possibilité d'écrire la même chose de manière
raccourcie ; de même pour les autres opérations arithmétiques, ainsi
que les opérations bit à bit :
a += 3; // équivaut à a = a + 3
a -= 3; // équivaut à a = a - 3
a *= 3; // équivaut à a = a * 3
a /= 3; // équivaut à a = a / 3
a %= 3; // équivaut à a = a % 3
a <<= 3;
// etc. pour >>=, >>>=, &=, |= et ^=
Parmi ces opérations, le cas particulier de l'incrémentation et de la
décrémentation est assez fréquent pour justifier d'une notation
particulière :
a++; // équivaut à a += 1 ou à a = a +1
++a; // équivaut à a++, sauf pour la valeur rendue
a--; // équivaut à a -= 1 ou à a = a -1
--a; // équivaut à a--, sauf pour la valeur rendue
Cette question de valeur rendue vous semble probablement étrange ;
aussi longtemps que vous utilisez ces opérations de la manière dont
elles sont illustrées ci-dessus, vous n'avez pas besoin de vous
tracasser à ce sujet. C'est lorsqu'on commence à utiliser la r-valeur
de l'expression -- ce qui est appelé par certains "programmer par
effets de bord" et que je vous déconseille vivement, du moins dans
un premier temps -- qu'il faut y être attentif, dans la mesure où la
forme préfixée rend la valeur de la variable après
incrémentation/décrémentation, alors que la forme postfixée rend
la valeur avant l'opération :
int x = 3;
int y = 5; // x == 3, y == 5
x = y++; // x == 5, y == 6
x = ++y; // x == 7, y == 7
2.6 Mon premier programme Java
À ce stade, nous disposons de constantes, de variables, d'expressions
et de l'instruction d'affectation d'une valeur à une variable.
On peut déjà écrire des programmes simples avec ces éléments.
Nous allons donc construire notre tout premier programme en Java.
Nous nous rendrons vite compte qu'il faut recourir à plusieurs
constructions que nous n'avons pas encore expliquées ; prenez-les dans
un premier temps comme des "recettes", et nous indiquerons chaque fois
la référence en avant de la partie du cours où la recette est
expliquée.
Quand nous commençons ainsi à écrire des programmes, il est important
de prendre tout de suite de bonnes habitudes d'écriture ; je
vous invite donc dès maintenant à consulter l'annexe H.
Comme thème récurrent dans ce cours, nous allons choisir la gestion d'un compte
bancaire.
Commençons par écrire les lignes nécessaires pour créditer un compte
bancaire d'un montant donné :
int solde = 0; // variable désignant le solde et initialisée à 0
int montant = 16; // variable désignant le montant à créditer
solde += montant; // l'opération de crédit à proprement parler
Pour que ces lignes constituent un programme qui puisse
s'exécuter, il faut les inclure dans une construction qui vous
semblera probablement compliquée pour l'instant, mais que nous
expliquerons par la suite.
Tout d'abord, l'ensemble de lignes doit être inclus dans une
procédure particulière appelée main (cf. § 3.2.4),
dont la syntaxe est assez rébarbative au premier abord (mais nous aurons
l'occasion d'expliquer les raisons de cette syntaxe -- cf. § 4.3.1).
Mais Java impose aussi que toute procédure doit appartenir à une
classe, car on ne peut lancer une exécution Java qu'en précisant
dans quelle classe se trouve le code à exécuter (cf. § 4.1).
Nous définirons donc une classe Banque dans le fichier
Banque.java (cf. § H.1).
Nous avons maintenant un programme, contenu dans la classe Banque,
et qui est exécuté lorsque l'interprète Java est lancé sur cette classe.
Le contenu du fichier Banque.java est donc :
public class Banque {
public static void main(String[] args) {
int solde = 0;
int montant = 16;
solde += montant;
}
}
Nous avons maintenant un programme qui compile et qui s'exécute ;
malheureusement, il ne communique pas avec le monde extérieur.
On aimerait au minimum qu'il affiche le résultat de son calcul.
Nous allons pour cela recourir à une fonction de la bibliothèque
d'entrées/sorties fournie avec Java, appelée java.io
(cf. § 6.3) ; cette
fonction permet d'afficher à l'écran une chaîne de caractères, et
convertit en chaîne de caractères tous les objets d'un autre type, tel
que int dans notre exemple.
Nous allons également utiliser une propriété de la classe
String, représentant les chaînes de caractères
(cf. § 4.4), à savoir que l'opérateur + permet
de concaténer deux chaînes de caractères.
Le programme ainsi modifié est donné ci-après :
// Classe Banque - version 1.0
import java.io.*; // Importer toutes les fonctionnalités de java.io
public class Banque {
public static void main(String[] args) {
int solde = 0;
System.out.println("Le solde initial est de " + solde + " euros.");
int montant = 16;
solde += montant;
System.out.println("Le nouveau solde est de " + solde + " euros.");
}
}
Quand on l'exécute, un résultat s'affiche :
Le solde initial est de 0 euros.
Le nouveau solde est de 16 euros.
Allons maintenant un peu plus loin : une grosse faiblesse du programme
reste que le montant à créditer est donné dans le programme lui-même,
alors qu'on aimerait qu'il soit donné par l'utilisateur.
Pour cela, nous allons recourir à une nouvelle fonction de la
bibliothèque d'entrées/sorties ; malheureusement, pour l'appeler, nous
avons d'abord besoin de recourir à une petite gymnastique mystérieuse
de conversions. N'ayez crainte ; nous comprendrons plus tard les
raisons de ces conversions (cf. § 6.3.1).
Quand on lit quelque chose au clavier, il peut y avoir des problèmes
(l'utilisateur peut par exemple donner une entrée invalide) et le
système le sait ; il faut donc lui dire comment traiter ces éventuels
problèmes, appelés exceptions (cf. § 6.4).
Enfin, la fonction de lecture rend une chaîne de caractères, qu'il
conviendra de convertir en un entier, par une fonction de conversion
ad hoc.
La nouvelle version du programme est donnée ci-après ; dans tout ce
polycopié, nous indiquons sur fond grisé les lignes qui changent
d'une version à l'autre, sauf quand les modifications sont majeures et
nécessitent de reconsidérer l'ensemble des lignes :
// Classe Banque - version 1.1
import java.io.*; // Importer toutes les fonctionnalités de java.io
public class Banque {
public static void main(String[] args) {
// Conversions mystérieuses...
InputStreamReader ir = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(ir);
int solde = 0;
System.out.println("Le solde initial est de " + solde + " euros.");
System.out.print("Montant à créditer = ");
System.out.flush();
String lecture = ""; // la chaîne qu'on va lire, initialisée à rien
try { // prise en compte des exceptions par try/catch
lecture = br.readLine();
} catch (IOException ioe) {}
int montant = Integer.parseInt(lecture); // conversion en int de la chaîne lue
solde += montant; // on retrouve l'incrémentation classique
System.out.println("Le nouveau solde est de " + solde + " euros.");
}
}
Le résultat de l'exécution donne (les données entrées par
l'utilisateur sont toujours indiquées sur fond grisé) :
Le solde initial est de 0 euros.
Montant à créditer = 43
Le nouveau solde est de 43 euros.
Voilà ! nous avons un premier programme, certes rudimentaire, mais qui
fait quelque chose d'à peu près sensé...
Bien évidemment, nous avons dû passer sous silence les raisons de
beaucoup de détails, qui vous paraissent peut-être pour l'instant
relever plus de la formule magique que de la démarche raisonnée.
Mais les choses vont se clarifier au fur et à mesure de la progression.
2.7 De la structuration du discours : les instructions de
contrôle
Pour structurer et décomposer un programme, l'idée de base dans
beaucoup de langages est que stricto sensu, on ne gère toujours qu'une
seule instruction à chaque niveau, y compris le programme
complet. Cette instruction peut être :
-
L'appel à une fonction ou à une procédure ; nous verrons les
fonctions et les procédures plus tard (§ 3.2),
mais c'est en particulier l'instruction qui sert de point de départ à
l'exécution d'un programme, par le lancement de
la procédure main(), comme nous l'avons vu
ci-dessus.
- Une instruction atomique de déclaration d'une variable3
ou d'affectation du résultat
d'un calcul à une variable, voire une instruction vide
(;). Comme nous l'avons vu intuitivement dans l'exemple que
nous avons déroulé au § 2.6,
une expression ou une déclaration de variable (avec initialisation
éventuelle) "devient" une instruction quand on lui ajoute le
signe `;' qui joue le rôle de terminaison
d'instruction4.
- Une construction conditionnelle
(cf. § 2.7.1) ou d'itération
(cf. § 2.7.2).
- Un bloc composé ; l'idée est ici que si on a besoin de plusieurs
instructions à un même niveau, elles sont regroupées dans un bloc,
entre les délimitateurs `{' et `}'.
Un bloc composé a sa propre portée lexicale
(cf. § 6.1) ; on peut en particulier y définir et
y utiliser des variables locales au bloc.
2.7.1 Instructions conditionnelles
Dans le modèle classique en vigueur depuis la
machine de von Neumann (cf. chapitre A), le flot normal
d'exécution est séquentiel, c'est-à-dire qu'après exécution d'une
instruction élémentaire, c'est l'instruction immédiatement consécutive
dans le programme qui est activée.
Les langages de programmation de haut niveau ont pour la plupart
repris ce principe simple, et c'est le cas notamment de Java : des
instructions atomiques vont être exécutées dans l'ordre dans lequel elles
se succèdent dans le programme.
Cependant, pour élaborer des programmes plus complets, il est
nécessaire de disposer d'instructions permettant de modifier dans
certains cas ce déroulement linéaire.
La première que nous allons voir est la conditionnelle.
Pour oser une comparaison ferroviaire, on peut dire qu'il existe deux
types de conditionnelles :
-
l'aiguillage, qui permet de choisir l'une de deux "branches"
d'exécution, suivant la valeur d'une condition ;
- la gare de triage, qui permet de multiples embranchements,
suivant les conditions rencontrées.
L'aiguillage correspond à la structure algorithmique classique appelée
si--alors--sinon, et qu'on peut écrire sous la forme suivante :
si |
condition |
|
alors instruction1 |
|
[sinon instruction2] |
finsi |
La partie sinon est facultative. Pour l'indiquer, nous avons mis des
crochets [ ] ci-dessus.
La construction correspondante en Java utilise les mots clés
if et else. Notons qu'il
n'y a pas de mot clé particulier pour traduire alors.
Illustrons tout de suite cette construction en modifiant notre
programme bancaire. Nous souhaitons maintenant pouvoir choisir
d'effectuer une opération de crédit ou de débit.
Il faut donc demander à l'utilisateur ce qu'il souhaite faire, et
effectuer en conséquence les opérations appropriées.
Dans le programme, nous utilisons aussi, sans l'expliquer pour
l'instant5,
la construction choix.equals("D"), qui
permet de comparer deux chaînes de caractères.
L'interface reste ici rudimentaire, on pourrait entre
autres imaginer que si l'utilisateur donne un choix invalide, on lui
repose la question au lieu de décider par défaut que c'est une
opération de crédit :
// Classe Banque - version 1.2
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);
int solde = 0;
System.out.println("Le solde initial est de " + solde + " euros.");
System.out.print("[D]ébit ou [C]rédit (choix par défaut) ? ");
String choix = "";
try {
choix = br.readLine();
} catch (IOException ioe) {}
boolean credit = true; // variable vraie si c'est un crédit (défaut)
if (choix.equals("D") || choix.equals("d")) {
credit = false;
}
// sinon on reste à true par défaut, donc crédit
// Notez l'utilisation de l'opérateur à 3 opérandes ?:
System.out.print("Montant à " + (credit ? "créditer" : "débiter") + " = ");
System.out.flush();
String lecture = ""; // la chaîne qu'on va lire, initialisée à rien
try {
lecture = br.readLine();
} catch (IOException ioe) {}
int montant = Integer.parseInt(lecture); // conversion en int de la chaîne lue
if (credit) {
solde += montant;
}
else {
solde -= montant;
}
System.out.println("Le nouveau solde est de " + solde + " euros.");
}
}
Et voici le résultat de deux exécutions successives de ce programme :
Le solde initial est de 0 euros.
[D]ébit ou [C]rédit (choix par défaut) ? d
Montant à débiter = 124
Le nouveau solde est de -124 euros.
Le solde initial est de 0 euros.
[D]ébit ou [C]rédit (choix par défaut) ? c
Montant à créditer = 654
Le nouveau solde est de 654 euros.
Vous avez peut-être noté l'emploi de blocs composés,
délimités par `{' et `}', bien que ceux-ci ne
contiennent qu'une seule instruction.
Stricto sensu, ils ne sont pas nécessaires ici, mais il est
fortement conseillé d'en systématiser l'usage pour toutes les
constructions conditionnelles ou itératives ; cela augmente la
lisibilité et la clarté du programme, en mettant mieux en valeur la
structure de votre "discours".
La deuxième construction conditionnelle possible correspond à ce que
nous avons appelé la gare de triage.
Pour l'exprimer dans un algorithme, on écrira quelque chose du
genre :
|
ì
ï
í
ï
î |
cas1 |
® instruction1 |
cas2 |
® instruction2 |
... |
casn |
® instructionn |
|
|
En Java, il n'existe pas de vraie construction syntaxique permettant
de traduire cette notion ; on utilise habituellement une série de
if ...else imbriqués.
Cependant, dans le cas particulier où les différents cas peuvent
s'exprimer sous la forme de valeurs possibles pour une expression de
type entier (byte, short, int ou
long) ou caractère (char), on dispose de la
construction switch. Cependant, comme nous
l'illustrons dans
l'exemple ci-après, cette construction correspond plutôt à un ensemble
de branchements à des points précis du programme, ce qui explique la
présence des instructions break ;
sans celles-ci, le flot de contrôle
se poursuivrait séquentiellement. Cela explique aussi l'absence de
structuration par des blocs composés : on reste au même niveau de
structuration. À noter aussi qu'on ne peut pas, dans une construction
de type switch, employer des conditions booléennes, les
étiquettes de branchement -- introduites par le mot clé
case, l'étiquette
default indiquant une valeur par défaut --
correspondant à des valeurs constantes et
discrètes, de type caractère ou entier.
Il faut donc réserver
cette construction à des cas très précis et spécifiques, et
privilégier une suite de constructions if ...else,
éventuellement imbriquées, dans la majorité des cas de
conditionnelles.
Donnons toutefois une nouvelle variante du programme bancaire où nous
utilisons une condition switch pour choisir le type
d'opération à effectuer.
Cette fois-ci, au lieu de comparer des chaînes de caractères, nous
utilisons l'opération choix.charAt(0) pour récupérer le
premier caractère (position 0) de la chaîne6,
le switch se
faisant sur la valeur de ce caractère (majuscules et minuscules
autorisées) :
// Classe Banque - version 1.3
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);
int solde = 0;
System.out.println("Le solde initial est de " + solde + " euros.");
System.out.print("Votre choix : [D]ébit, [C]rédit, [F]in ? ");
String choix = "";
try {
choix = br.readLine();
} catch (IOException ioe) {}
boolean fin = false; // variable vraie si on veut s'arrêter
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: // Etiquette spéciale si aucun des cas prévus n'est vérifié
fin = true; // On va considérer que par défaut on s'arrête
}
// Poser la question uniquement si on n'a pas choisi de finir
// Notez que toute cette partie reste identique au programme précédent
// mais elle est maintenant incluse dans une construction if
if (!fin) {
System.out.print("Montant à " + (credit ? "créditer" : "débiter") + " = ");
System.out.flush();
String lecture = ""; // la chaîne qu'on va lire, initialisée à rien
try {
lecture = br.readLine();
} catch (IOException ioe) {}
int montant = Integer.parseInt(lecture); // conversion en int
if (credit) {
solde += montant;
}
else {
solde -= montant;
}
}
// Dans tous les cas, imprimer le nouveau solde
System.out.println("Le nouveau solde est de " + solde + " euros.");
}
}
Un exemple de deux exécutions :
Le solde initial est de 0 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? d
Montant à débiter = 123
Le nouveau solde est de -123 euros.
Le solde initial est de 0 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? f
Le nouveau solde est de 0 euros.
2.7.2 Instructions itératives
Pour garder l'analogie ferroviaire, la construction itérative permet
de réaliser un circuit dans le flot de contrôle -- ce qui plaira
sûrement à tous les amateurs de modélisme.
Plus sérieusement, une construction itérative permet de répéter les
mêmes instructions
-
tant qu'une condition reste vérifiée ; on écrit alors :
tantque |
condition faire |
|
instructions |
fintantque
|
- pour tous les éléments d'un ensemble ; on écrira alors quelque
chose du genre :
pour |
tout x Î E faire |
|
instructions |
finpour
|
Java offre deux constructions correspondant à ces deux cas de figure.
Commençons par la première.
En Java, elle s'écrit avec les mots clés while et
éventuellement do.
En reprenenant l'exemple bancaire, ajoutons
maintenant la possibilité d'effectuer successivement plusieurs
opérations de débit et de crédit, jusqu'à ce que l'utilisateur désire
finir :
// Classe Banque - version 1.4
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);
int solde = 0;
System.out.println("Le solde initial est de " + solde + " euros.");
boolean fin = false; // variable vraie si on veut s'arrêter
while (!fin) { // tant qu'on n'a pas demandé de finir
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) {
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) {
solde += montant;
}
else {
solde -= montant;
}
// N'afficher le nouveau solde que s'il y a eu une opération
System.out.println("Le nouveau solde est de " + solde + " euros.");
}
}
}
}
Un exemple d'exécution est donné ci-après :
Le solde initial est de 0 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? C
Montant à créditer = 500
Le nouveau solde est de 500 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? D
Montant à débiter = 234
Le nouveau solde est de 266 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? D
Montant à débiter = 65
Le nouveau solde est de 201 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? C
Montant à créditer = 67
Le nouveau solde est de 268 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? F
La variante do ...while a comme seule
différence que les
instructions sont exécutées une première fois avant que soit testée la
condition d'arrêt.
Nous aurions par exemple pu écrire une version légèrement différente
du programme précédent, avec :
do {
System.out.print("Votre choix : [D]ébit, [C]rédit, [F]in ? ");
... // etc.
} while (!fin);
Dans le cas présent, la différence entre ces deux versions est
minime ; il y a cependant des cas où le choix de l'une ou de l'autre
des variantes rend l'écriture du programme plus simple ou plus claire.
Pour le deuxième type d'itération, qui permet de parcourir tous les
éléments d'un ensemble ou d'une collection, il est souvent utile
d'employer une autre construction Java, celle qui emploie le mot clé
for.
Si par exemple, au lieu de demander à l'utilisateur d'indiquer quand
il a fini de saisir des écritures comptables, on sait qu'on va lui en
demander dix, on pourra écrire :
for (int i = 0 ; i < 10 ; i++) {
System.out.print("Votre choix : [D]ébit ou [C]rédit ? ");
... // etc
}
Les trois expressions séparées par des `;' dans cette
construction for correspondent respectivement à
l'initialisation de la boucle, à la condition de continuation, et au
passage à l'élément suivant de l'ensemble à traiter.
Les plus perspicaces auront probablement compris qu'une construction
équivalente7,
dans le cas présent, serait :
int i = 0;
while (i < 10) {
System.out.print("Votre choix : [D]ébit ou [C]rédit ? ");
... // etc
i++;
}
NB : Une erreur assez commune faite par les débutants et de
séparer les trois expressions constitutives de la construction for
par des virgules et non par des points-virgules. Tenez-le vous pour dit !
Le principal avantage de for est de rendre explicite
l'énumération des éléments d'un ensemble ou d'une collection.
Nous aurons l'occasion d'illustrer plus complètement l'utilisation de
cette construction dans d'autres paragraphes (cf. § 3.3).
Deux mots clés supplémentaires peuvent être utilisés dans les
itérations8 :
-
continue permet de revenir de manière
prématurée en début de
boucle. Si par exemple on veut éviter d'effectuer une opération de
crédit ou de débit si le montant de l'opération est nul, on peut
modifier le programme vu précédemment en ajoutant les lignes sur fond grisé
ci-dessous :
...
while (!fin) { // tant qu'on n'a pas demandé de finir
...
if (!fin) {
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 (montant == 0) {
continue; // repartir en début de boucle while(!fin)
}
if (credit) {
solde += montant;
}
else {
solde -= montant;
}
System.out.println("Le nouveau solde est de " + solde + " euros.");
}
...
}
Il va sans dire qu'il faut éviter autant que faire se peut de
recourir à ce genre de construction, qui rend le programme
considérablement moins lisible.
- break, que nous avons déjà vu pour la
construction
switch, permet aussi de sortir d'une boucle d'itération
sans repartir par le test de départ. Ainsi, nous aurions pu écrire
notre programme différemment, en prévoyant une boucle infinie
(construction while (true)) dont on sort explicitement dès
que l'utilisateur donne le code de fin. Le programme peut sembler un
peu plus élégant de cette manière ; cependant, il ne faut pas
oublier qu'il est a priori contre-intuitif et peu explicite d'écrire
while (true) quand on sait pertinemment qu'on va sortir de
la boucle à un moment donné. Il faut donc utiliser des instructions
comme break et continue avec beaucoup de
modération...
Donnons néanmoins une version corrigée de notre programme, avec
utilisation de break :
// Classe Banque - version 1.5
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);
int solde = 0;
System.out.println("Le solde initial est de " + 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) {
solde += montant;
}
else {
solde -= montant;
}
System.out.println("Le nouveau solde est de " + solde + " euros.");
}
}
}
}
- 1
- Stricto sensu, les types Java entiers et réels
représentent bien entendu des sous-ensembles de Z et de
R, du fait de la représentation discrète et bornée des
nombres en informatique (cf. § F.2 et
F.3).
- 2
- Cet exemple illustre bien la différence fondamentale entre une
égalité mathématique -- qui n'aurait aucun sens ici -- et
l'affectation : ici, on prend la r-valeur de a, on lui ajoute
3, et on stocke le résultat à l'adresse donnée par la
l-valeur de cette même variable a.
- 3
- En règle générale, la déclaration d'une variable doit être
immédiatement suivie de l'initialisation de cette variable. Sauf cas
très particulier, quand vous n'avez aucun moyen de donner une valeur
d'initialisation réaliste à une variable, nous vous conseillons
d'adopter ce principe.
- 4
- Pour ceux qui connaissent le langage Pascal, il faut
noter qu'en Pascal, `;' est un séparateur
d'instructions, ce qui implique entre autres que la dernière
instruction d'un bloc n'est pas terminée par un `;' -- en
Java, en revanche, chaque instruction se termine par un `;'
- 5
- Pour ceux qui sont curieux de savoir comment ça marche, voir
§ 4.4.
- 6
- Une fois de plus, les esprits curieux peuvent faire un saut en avant
jusqu'au § 4.4 pour mieux comprendre cette opération.
- 7
- Au petit détail près que dans un cas la variable i n'existe
que lors de l'itération, alors que dans l'autre elle est déclarée avant
le début de la boucle et continue donc sa vie à la sortie de
l'itération.
- 8
- On peut noter que ces deux instructions break et
continue peuvent être suivies d'une étiquette, qui spécifie
soit une boucle englobante (pour continue),
soit une instruction quelconque englobante (pour
break). Dans ce dernier cas, break indique que
l'on veut quitter le niveau englobant ainsi spécifié. Nous ne vous
conseillons pas d'utiliser ces fonctionnalités plus avancées...