Morceaux de Java au fil des TP
Introduction
Ce document décrit des éléments de programmation Java, tels que vus
dans l'ordre des TP. Il n'entend pas se substituer aux autres
documentations et en particulier à l'annexe Java du poly.
1 Un langage plutôt classe
Nous choisissons de ne presque rien cacher: Java est un langage objet
avec des classes.
C'est à dire que les valeurs manipulées en Java (les ``trucs'') se
scindent en deux catégories distinctes:
-
Les valeurs de bases ou scalaires, c'est à dire des
entiers, des booléens etc. (voir section 2.4).
- Les objets.
Les objets sont des trucs particuliers qui contiennent:
-
Des variables, ou champs, qui contiennent
tout simplement des trucs, que l'on désigne par
objet
.
nom.
- Des méthodes, qui sont plus ou moins des fonctions
à n arguments, que l'on appelle
par
objet
.
nom(
arg1,
...
,argn)
.
Par exemple, une variable i déclarée comme int i
,
contient un bête entier.
Les habitués de Caml noteront que les méthodes ne sont pas des
valeurs.
Bon, on crée les objets en instanciant des classes, il n'est
donc pas trop surprenant que les classes regroupent
variables et méthodes tout comme les objets. En simplifiant, on crée
un objet à partir d'une classe en en faisant une espèce de copie.
1.1 Programme de classe
Là où ça devient drôle c'est que les classes existent aussi en tant
que telles.
Essentiellement, les classes structurent les programmes
que vous écrirez.
Plus précisément, un programme se construit à partir de une ou
plusieurs classes, dont une au moins contient une méthode main
qui est le point d'entrée du programme.
Les variables des classes sont les variables globales du programme et leurs
méthodes sont les fonctions du programme.
Une variable ou une méthode qui existent dès que la classe existe
est dite statique.
Les autres existent à raison d'une variable ou méthode différente par
objet quand on crée les objets, elle sont dites dynamiques.
Pour s'y retrouver on a tendance à donner des noms
qui commencent par une majuscule aux classes et par une minuscule aux
objets.
Commençons par un programme fabriqué à l'aide d'une seule classe.
Par exemple, la classe simple suivante est un programme qui affiche
coucou !
sur la console:
class Simple {
static String msg = "coucou !" ;
public static void main (String []) // déclaration de méthode
{
System.out.println(msg) ;
}
}
Cette classe ne sert pas à fabriquer des objets. Elle se suffit à elle
même.
Par conséquent tout ce qu'elle définit (variable msg
et méthode
main
) est statique. Par re-conséquent, toutes les déclarations
sont précédées du mot-clé static.
En plus d'être statique, la méthode main
doit impérativement
être publique et prendre un tableau de chaîne en argument.
Sinon, l'environnement d'exécution ne saura pas la lancer.
Si le source est contenu dans un fichier Simple.java, il se
compile par javac Simple.java
et se lance par java
Simple. C'est une bonne idée de mettre les classes dans des fichiers
homonymes, ça permet de s'y retrouver.
En termes de programmation objet, la méthode main
invoque la méthode
println
de l'objet System.out
, avec l'argument
msg
.
L'objet System.out
étant rangé comme la variable out
de
la classe système.
Notons que msg
est en fait Simpl.msg
, mais dans la
classe Simple
, on peut se passer de rappeler que msg
est
une variable de la classe Simple
, alors autant en profiter.
Reste à se demander quel est l'objet rangé dans System.out
. Eh
bien, disons que c'est un objet d'une autre classe (La classe
BufferedWriter
) qui a été mis là
par le système Java et ne nous en préoccupons plus. Ouf.
1.2 Complément : La méthode main
La déclaration de cette méthode doit obligatoirement être de la forme :
public static void main (String [] arg)
ou encore :
public static int main (String [] arg)
Le sens du mot-clé public est expliqué plus
loin. Le mot clé static est connu (cf. la
section 1.1).
Le reste des obligations portent sur le type de la valeur renvoyée,
qui peut n'être rien (premier cas) ou un entier (second cas) et des
arguments.
Ces types assurent une petite collaboration avec Unix.
En effet tous les programmes Unix renvoie un code de retour qui est un
entier (ce code est accessible par $status
une fois la commande exécutée). Ça ne sert presque à rien.
Les arguments sont plus intéressants, il s'agit des arguments données
sur la ligne de commande. De sorte que l'on peut facilement
écrire une commande echo en Java:
class Echo {
public static void main (String [] arg) {
for (int i = 0 ; i < arg.length ; i++) {
System.out.println(arg[i]);
}
}
}
Ce qui nous donnera après compilation :
# java Echo coucou foo bar
coucou
foo
bar
1.3 Collaboration de classe
La classe-programme Simple utilisait d'autres classes, par
exemple, la classe System. Le source de ces
classes a été écrit par les auteurs du système Java, et on peut en
profiter.
Pour structurer vos programmes à vous, vous pouvez aussi écrire
plusieurs classes.
Par exemple, réécrivons le programme simple à l'aide de deux classes.
Le message est fourni par une classe Message
class Message {
static String coucou = "coucou !" ;
}
Tandis que le programme est modifié ainsi :
class Simple {
public static void main (String []) // déclaration de méthode
{
System.out.println(Message.coucou) ;
}
}
Si l'on met la classe Message dans un fichier
Message.java. Elle sera compilée automatiquement lorsque l'on
compile le fichier Simple.java (par
javac Simple.java
). Encore une bonne raison pour
mettre les classes dans des fichiers homonymes.
1.4 Méfiance de classe
Lorsque l'on fabrique un programme avec plusieurs classes, l'une
d'entre elles contient la méthode main
. Les autres fournissent
des fonctionnalités, en général sous forme de méthodes accessibles à
partir des autres classes.
Prenons l'exemple du TD-1.
La classe Counter fournit la fonctionnalité d'une table
d'association des chaînes vers les entiers.
Cela passe par 3 méthodes utilisables à partir
d'autres classes, dont voici les déclarations:
static void init (int size) // initialisation de la table d'association
static void set (String key, int v) // création ou mise à jour d'une entrée
static int get (String key) // récupérer une valeur
De façon interne, la classe Counter utilise une table de
hachage. Tout ce qui concerne cette table de hachage n'a pas besoin
d'être vu des autres classes (ici Stat)
mais doit quand même être là. Les variables et méthodes
privées, accessibles seulement à partir de la classe
Counter elle-même sont déclarées avec le mot-clef
private. Par exemple :
class Counter {
/*************************************/
/* Données et méthodes privées */
/*************************************/
/* Deux tableaux pour la table de hachage */
private static int [] count = null; // rien au de'part
private static String [] name = null; // non plus
/* Taille de la table */
private static int size = 0;
// Fonction de hachage, qui transforme s en un entier
private static int hash (String s) {
...
}
/*************************************/
/* Le reste est accessible */
/*************************************/
static void set (String key, int v) {
...
}
static int get (String key) {
...
}
static void init (int size) {
count = new int [size]; // initialisation du tableau des compteurs
name = new String [size]; // initialisation du tableau des chaînes
Counter.size = size; // Jeu subtil sur le nommage des variables
}
}
Ainsi on le voit, la méthode init
(non-privée) de la classe
Counter a bien accès aux variables privées count,
name et size. Et, si ni get, ni set ne
bouleversent ces initialisation, on sait positivement que ces valeurs
initiales ne changeront pas.
Pour fixer les idées, voici le source complet de la classe
Counter.
Cette pratique, de restreindre autant que possible la visibilité des
variables et méthodes structure fortement les programmes :
-
Chaque classe propose un service, qui sera maintenu même si la
réalisation de ce service change (en clair, on peut changer la partie
privée d'une classe indépendamment de l'usage qui est fait, car on est
positivement certain que personne n'a accès aux données et méthodes
privées).
- La structure des programmes est plus claire car, pour comprendre
comment les diverses classes interagissent, il suffit de comprendre
les méthodes (et variables) exportées (i.e., non-privées) des classes.
En fait il suffit normalement de comprendre les déclarations des méthodes
exportées, déclarations souvent assorties d'un commentaire judicieux.
Les documentations de
la
librairie Java et de la
MacLib fonctionnent selon ce principe.
On parle d'abstraction, la séparation en classe segmente le programme
en unités plus petites, dont on n'a pas besoin de tout savoir.
Au niveau de la classe elle-même, la démarche d'abstraction revient à
écrire plusieurs méthodes, chaque méthode réalisant une tâche
spécifique.
L'abstraction (découpage en classes et puis en méthodes, qui
interagissent selon des conventions claires qui disent le quoi
et cachent les détails du comment) est un
fondement de la bonne programmation, c'est à dire de la production de
programme compréhensibles et donc de programmes qui sont facilement
mis au point, puis modifiés.
En Java, par défaut, les méthodes (et les variables) de classes sont
visibles de tout le package de leur classe (les classes sont regroupés
en packages, une notion sans intérêt pour le moment),
Le mot-clé private restreint la visibilité
des méthodes (et variables) à leur propre classe.
À l'opposé, le mot-clé public étend la visibilité des méthodes
aux autres packages. C'est pourquoi, la méthode main doit être
publique, afin que l'environnement d'exécution Java (qui se trouve
dans un autre package que les classes que vous écrivez, vous
utilisateur de cet environnement) puisse la voir et
l'invoquer.
Le mot-clef public s'applique aussi aux classe, il entraîne
alors que cette classe
peut être étendue sans distinction de package, si
je ne me trompe pas.
1.5 Reproduction de classe par héritage
La programmation objet exprime toute sa puissance par le mécanisme de
l'héritage.
Nous n'examinons que l'héritage du point de vue de la classe.
C'est en fait extrêment simple, si une classe Foo
se déclare
ainsi
class Foo extends Bar {
...
}
Alors, la classe Foo
démarre dans la vie avec toutes les
variables et toutes les méthode de la classe Bar
, chouette
non ?
Mais la classe Foo
ne va pas se contenter de démarrer dans la
vie, elle peut effectivement étendre la classe dont elle hérite et
ceci de deux façons :
-
En se donnant de nouvelles méthodes et de nouvelles variables.
- En redéfinissant les méthodes (et variables) de la classe dont
elle hérite. On parle alors d'overriding ou
de redéfinition de méthode.
Le TD-2 utilise extends pour
construire une interface graphique à partir de la
MacLib.
2 Constructions de base
En première approximation, Java c'est du C avec un emballage
``objet''. C'est à dire que les déclarations et initialisations
des variables et les corps des méthodes ressemblent furieusement à du
C. Si on connaît déjà C, c'est bien pratique, sinon on apprend C en
même temps...
Java (et C aussi d'ailleurs, mais moins) a l'ambition d'être un
langage fortement typé, c'est à dire que si l'on écrit un programme
incorrect du point de vue des types, alors le compilateur vous jette.
Il s'agit non pas d'une contrainte irraisonnée imposée par des
informaticiens fous, mais d'une aide extraordinaire à la mise au point
des programmes : la majorité des erreurs stupides ne passe pas la
compilation.
Par exemple, le programme suivant contient au moins deux erreurs de
typage (confusion entre entier et booléen, oubli d'un argument dans
un appel de méthode) :
class MalType {
// la methode incr prend un entier i et renvoie i+1
static int incr (int i) {
return (i+1) ;
}
static void mauvais() {
System.out.println(incr(true)) ; // Mauvais type
System.out.println(incr()) ; // Oubli d'argument
}
}
Eh bien, la compilation de la classe MalType échoue :
# javac MalType.java
MalType.java:9: Incompatible type for method. Can't convert boolean to int.
System.out.println(incr(true)) ; // Mauvais type
^
MalType.java:10: No method matching incr() found in class MalType.
System.out.println(incr()) ; // Oubli d'argument
^
2 errors
Néanmoins, le système de types de Java est plus puissant que celui
de C. Les classes permettent
certaines audaces. La plus courante se voit très bien dans
l'utilisation de System.out.println
(afficher une ligne sur la
console), on peut passer n'importe quoi ou presque en argument, séparé
par des ``+'':
System.out.println ("booléen :" + (10 < 11) + "entier : " + (314*413)) ;
Ça peut s'admettre si on sait que + est l'opérateur de
concaténation sur les chaînes, que System.out.println
prend une
chaîne en argument et que le compilateur insère des
conversions de types là où il sent que c'est utile.
Complément : Les types de type
Il y a en java trois sortes de types, les types scalaires
qui sont des mot-clefs (donc colorés par emacs),
les classes qui sont le type de leurs objets
(par exemple String type des chaînes, objets de la classe
String),
et les types des tableaux à la syntaxe plutôt
particulière (le type de chaque tableau est paramétré par le type de ses
éléments, le type générique des tableaux
ne peut donc pas être une classe).
2.2 Déclarations
De façon générale, une déclaration établit une correspondance entre un
nom et une construction nommable (valeur, méthode).
En outre la déclaration réserve quelque part la place nécessaire pour
ranger cette valeur ou cette méthode.
Les déclarations de variables sont de la forme
suivante :
modifiers type name ;
Les modifiers sont des
mots-clés (genre, static
pour
les variables vivantes dans toute la classe, final
pour les
constantes), le type est un type (genre int
,
int[]
ou String
) et name est un nom de
variable.
Une bonne pratique est d'initialiser les variables dès leur
déclaration, ça évite bien des oublis. Pour ce faire :
modifiers type name = expression ;
Où expression est une expression du bon type.
Par exemple, voici trois déclarations, de variables de type entier,
chaîne et tableau de chaîne :
static int i = 0;
static String message = "coucou" ;
static String [] jours =
{"dimanche", "lundi", "mardi", "mercredi",
"jeudi", "vendredi", "samedi"} ;
Les déclarations peuvent apparaître à pas mal d'endroits, dans une
classe, un corps de fonction, l'initialisation d'une boucle.
Nous y reviendront.
Les déclarations de fonctions (de méthodes en terminologie Java) ne
peuvent se situer qu'au niveau des classes et
suivent la forme générale suivante :
modifiers type name (args)
Où type est le type du résultat de la fonction (void
si il n'y en a pas), name est le nom de la fonction et
args sont les déclarations des arguments de la fonction qui
sont de bêtes déclarations de variables (sans le ``;'' final)
séparées par des virgules
``,
''.
Suit ensuite le corps de la fonction, entre accolades
``{
'' et ``}
''. Il n'y a pas lieu d'initialiser les
arguments et d'ailleurs cela n'aurait aucun sens puisque les arguments
sont initialisés à chaque appel de fonction.
Complément : Déclarations sans initialisation.
Les déclarations de variables qui sont aussi des déclarations de
champs de classe (et d'objet, c'est pareil) sont un rien
particulières.
En effet, si ces déclarations ne comportent pas d'initialisation,
alors les variables déclarées contiennent des
valeurs conventionnelles, zéro pour un
entier, false pour un booléen etc. et null pour un
objet. On peut rapprocher cet effet, dont il fait savoir profiter,
du cas des éléments des tableaux (cf. la section 6.7.2).
2.3 Principales instructions
Il y a une distinction assez jésuite entre expressions (qui produisent
un résultat) et instructions (qui n'en produisent pas). C'est jésuite
parce qu'en fait toute expression suivie de ``;'' devient une
instruction.
Les expressions les plus basiques sont :
-
Constantes
- Soit
1
(entier), true
(booléen),
"coucou !"
(chaîne), etc.
Une constante amusante est null, qui est un objet sans champs
ni méthodes.
- Usage de variable
-
Soit ``
i
'', où i
est une variable déclarée par ailleurs.
En fait, si la variable i
est déclarée dans la classe Classe,
alors on y accède normalement par ``Classe.i
'', si la variable
variable i
est un champ de l'objet objet, alors on y
accède par ``objet.i
''. Heureusement pour nous, on
peut utiliser la notation abrégée ``i
'' à l'intérieur de la
classe Classe et de l'objet objet. Ce qui fait que
dans la pratique, il y a pas mal d'implicite dans les usages de variables.
- Appel de méthode
-
C'est comme les variables :
-
statique
- Soit Classe
.f(i)
où f
est une méthode
de la classe Classe.
par ailleurs, si Classe.g
ne prend pas d'argument, on écrit
Classe.g()
.
- dynamique
-
Si objet est un objet possédant la méthode
f
, alors on
invoque cette méthode par ``objet.f(
...)
''.
Notez bien que les mêmes notations abrégées que pour les variables
s'appliquent à l'intérieur des classes et des objets.
La notation abrégée est parfois source de surprises et ont doit de temps en
temps revenir à la notation complète, au moins dans sa tête, pour
comprendre ce qui se passe.
- Variable this
- C'est carrément spécial et
peut être ignoré en première lecture, à l'intérieur des méthodes d'un
objet, this est cet objet lui-même.
De part la notation abrégée qui interprète ``variable'' comme
``this.variable en l'absence de liaison locale de
la variable variable on a généralement pas besoin
d'expliciter this.
- Usage des opérateurs
- Par exemple
i+1
(addition) ou
i == 1 || i == 2
(opérateur égalité et opérateur ou booléen).
- Accès dans les tableaux
- Par exemple
t[i]
, où t
est un tableau défini par ailleurs.
- Affectation
- Par exemple
i = 1
, l'expression à droite de
=
est calculée et le résultat est rangé dans la variable
donnée à gauche de =
. En fait on peut trouver autre chose
qu'une variable à gauche de =
, on peut trouver tout ce qui
désigne une case de mémoire, par exemple, un élément de tableau
t[i]
.
Le résultat de cette ``expression'' est la valeur
affectée. Ce qui permet des trucs du genre i = j = 0
, pour
initialiser i
et j
à zéro.
Cela se comprend si on lit cette expression comme
i = (j = 0)
.
- Expression parenthésée
- Si e est une expression, alors
(
e)
est aussi une expression. Cela permet
essentiellement de contourner les priorités relatives des opérateurs,
mais aussi de rendre un source plus clair. Par exemple, on peut
écrire (i == 1) || (i == 2)
, c'est peut-être plus lisible
que i == 1 || i == 2
.
C contenait quelques expressions pas piquées des vers qui sont
reprises en Java.
-
Incrément, décrément
-
Soit
i
variable de type entier. Alors l'expression
i++
range i+1
dans i
et renvoie l'ancienne valeur
de i
. L'expression i--
fait la même chose avec
i-1
.
Enfin l'expression ++i
(resp. --i
) est similaire,
mais elle renvoie la valeur incrémentée (resp. décrémentée) de
i
en résultat.
- Affectations particulières
-
La construction
i
op=
expression est
sensiblement équivalente à i = i
op
expression.
Par exemple:
i *= 2
range deux fois le contenu de i
dans i
et renvoie donc
ce contenu doublé.
Les finauds noteront que ++i
est aussi i += 1
.
Ces expressions avancées sont géniales, mais il est de bon goût de les
employer avec parcimonie.
Que l'on songe seulement au sens de t[++i] *= i++
et l'on
comprendra.
Les instructions les plus courantes sont les suivantes:
-
Expressions suivies de ;
-
Évidemment cette construction n'est utile que si e fait des
effets de bords (c'est à dire fait autre chose que rendre son
résultat). C'est bien sûr le cas d'une affectation par exemple.
- Séquence
-
On peut grouper plusieurs instructions en une séquence d'instruction,
les instructions s'exécutent dans l'ordre.
i = i + 1;
i = i + 1;
- Déclarations
- Une déclaration de variable (suivie de
``;'') est une instruction qui réserve de la place pour la
variable déclarée et l'initialise éventuellement:
int i = 0;
Il ne faut pas confondre affectation et déclaration, dans le premier
cas, on modifie le contenu d'une variable qui existe déjà, dans le
second on crée une nouvelle variable.
- Bloc
-
On peut mettre une séquence d'instructions dans un bloc
{
...}
.
La portée des déclaration internes au bloc s'éteint à la sortie du bloc.
Par exemple, le programme suivant:
int i = 0 ;
{
int i = 1; // Déclaration d'un nouvel i
System.out.println("i=" + i);
}
System.out.println("i=" + i);
affiche une première ligne i=1
puis une seconde i=0
.
Il faut bien comprendre que d'un affichage à l'autre l'usage de
variable ``i
'' ne fait pas référence à la même variable.
Lorsque l'on veut savoir à quelle déclaration correspond un usage, la
règle est de remonter le source du regard vers le haut, jusqu'à
trouver la bonne déclaration.
Attention, seule la portée des variables est limitée par les blocs,
en revanche l'effet des instruction passe allègrement les frontières
de blocs. Par exemple, le programme suivant :
int i = 0 ;
{
i = 1; // Affectation de i
System.out.println("i=" + i);
}
System.out.println("i=" + i);
affiche deux lignes i=1
.
- Retour de méthode
-
On peut dans le corps d'une méthode retourner à tout
moment par l'instruction
return
expression;
,
où expression est une expression dont le type est celui des
valeurs retournées par la méthode.
Par exemple, la méthode double qui double son argument entier
s'écrit :
int double (int i) {
return i+i ;
}
Si la méthode ne renvoie rien, alors return
n'a pas
d'argument :
void rien () {
return ;
}
À noter que, si la dernière instruction d'une méthode est
return ;
(sans argument), alors on peut l'omettre. De sorte que
la méthode rien s'écrit aussi :
void rien () { }
- Conditionnelle
- C'est l'instruction
if
:
if (i % 2 == 0) { // opérateur modulo
System.out.println("C'est pair") ;
} else {
System.out.println("C'est impair") ;
}
- Boucle while
- C'est assez tradi également.
Voici les entiers de zéro à neuf:
int i = 0 ;
while (i < 10) {
System.out.println(i) ;
i = i+1 ;
}
Soit while (
expression)
bloc,
on exécute expression, qui est une expression booléenne, si le
résultat est false c'est fini, sinon on exécute
bloc et on recommence.
- Boucle do
- C'est la même chose mais on teste la
condition à la fin du tour de boucle, conclusion : on passe au moins une fois dans la boucle :
i = 0 ;
do {
System.out.println(i) ;
i = i+1 ;
} while (i < 10)
(Les entiers de zéro à neuf).
- Boucle for
- C'est celle de C. Elle est un rien
complexe.
for (int i=0 ; i < 10 ; i = i+1)
System.out.println(i);
C'est à dire que la syntaxe générale est
for (einit ; econd ; enext)
bloc
Et que c'est quasiment la même chose que d'écrire :
{einit ;
while (econd) {
bloc
enext;
}
}
- Gestion du contrôle
-
Certaines instructions permettent de ``sauter'' quelquepart de
l'intérieur des boucles. Ce sont des formes polies de goto.
Il s'agit de break, qui fait sortir de la boucle, et de
continue qui commence l'itération suivante en sautant par
dessus ce qui reste de l'itération en cours.
Ainsi on peu ecrire, pour rechercher si l'entier foo est
présent dans le tableau t.
boolean trouve = false ;
for (int i = 0 ; i < t.length ; i++) {
if (t[i] == foo) {
trouve = true ;
break ;
}
}
// trouve == true ssi foo est dans t
Ou encore si on veut cette fois compter les occurences de foo
dans
t :
int count = 0 ;
for (int i = 0 ; i < t.length ; i++) {
if (t[i] != foo) {
contine ;
}
count++ ;
}
Noter que dans les deux cas on fait des choses un peu compliquées.
Dans le premier cas, on pourrait faire une méthode et utiliser
return, ou une boucle while sur trouve.
Dans le second cas, on pourrait écrire le test d'égalité.
Bon, c'est pour expliquer car, dans certaines situations,
ces instructions sont pratiques.
2.4 Types scalaires
Les booléens sont les deux valeurs true
et false
,
le type des booléen est boolean
.
Les booléens s'introduisent discrètement dans les
programmes par l'intermédiaire des opérateurs de comparaison
(qui s'appliquent aux scalaires en général),
on a l'égalité ==
, la différence !=
, les plus petit,
plus grand, etc. <
, >
, <=
et >=
.
boolean b1 = 1 < 2 ; // vrai
boolean b2 = 1 != 1 ; // faux
Il existe des opérateurs sur les booléens, la négation !
(sisi), le ``et logique'' &&
et le ``ou logique'' ||
.
par exemple on exprime la condition i
appartient à l'intervalle
[0...9]
de la façon assez élégante suivante :
if (0 <= i && i <= 9) ...
Complément : Sémantique
Les opérateurs &&
et ||
sont paresseux et s'évaluent de
la gauche vers la droite, c'est à dire que dans l'expression
``e1 &&
e2'',
l'expression e2 ne sera pas évaluée dans la cas
où e1 s'évalue comme false
car ce résultat est
suffisant pour savoir que la conjonction vaut false
.
Il en va de même avec ||
et true
.
Ce truc peut servir dans une expression du genre :
((0 <= i && i < max) && a[i] < m)
où max
est la taille du tableau a
.
La sémantique de &&
évite toute erreur d'accès au tableau
a
.
Rien de bien nouveau, le type scalaire des entiers s'appelle int
.
les entiers sont signés sur 32 bits (c'est à dire compris entre
-231 et 231 - 1.
Les entiers explicites sont en base dix et
les opérations usuelles *
, +
, -
et \
sont
disponibles,
l'opérateur reste de la division euclidienne (ou modulo) est noté
%
.
Notons qu'il s'agit en fait d'arithmétique modulo 232.
Les représentants des classes d'équivalence étant choisis un peu
bizarrement (certains sont négatifs).
Il existe aussi in type long
sur 64 bits et une règle de
promotion des entiers qui veut qu'une expression entière possède le
type son sous-composant le plus gros.
(Ainsi, si i
est un int
et l
est un long
, alors
i+l
est un long
.)
2.4.3 Caractères
Là c'est plus original le type char
des caractères est un
entier sur 16 bits, ce qui permet de représenter le jeu de caractère
Unicode qui regroupe les caractères de nombreuses langues.
On note les caractères par 'a'
, 'b'
, ..., 'z'
,
'0'
, '1'
, ...'9'
, '%'
, etc.
L'entier qui est le caractère est un code interne, conforme dans le
cas des 128 premier au code ascii qui marche partout.
Ça correspond plus ou moins aux caractères que vous pouvez entrer aux
clavier sans vous poser de questions.
Pour les autres c'est nettement moins clair et nous éviterons de les
utiliser.
Donc les caractères sont des entiers, c'est à dire que les opérations
entières fonctionnent avec eux, en particulier on peut les soustraire
et les additionner. Ça devient utile quand on sait que les lettres de
l'alphabet sont consécutives, le code suivant :
char jMajuscule = 'j' - 'a' + 'A';
range donc le caractère 'J'
dans la variable jMajuscule
et ceci sans connaître la valeur exacte des caractères en termes
d'entiers.
Une autre utilisation assez classique est de trouver la valeur d'un
chiffre décimal rangé comme un caractère dans c
, en sachant que
les codes des chiffres sont consécutifs :
int i ;
if ('0' <= c && c <= '9')
i := c - '0';
else
// Erreur
Complément : Changement de type
Lorsque l'on affiche un caractère par System.out.println
, il y a
un miracle c'est bien le caractère qui s'affiche :
for (int i = 0 ; i < 10 ; i++)
System.out.print('a');
System.out.println();
ce programme affiche ``aaaaaaaaaa
''.
Mais comment afficher caractères ``visibles'' du jeu ascii,
dont les codes vont de 32 à 127.
Il suffit de convertir l'entier i
en un caractère, à
l'aide d'une contrainte de type (type cast):
for (int i = 32 ; i < 128 ; i++)
System.out.print((char)i);
System.out.println();
L'exemple ci-dessus est un peu gratuit, mais la contrainte de type est
parfois nécessaire en raison de la règle de promotion et parce que
la différence de deux caractères est (logiquement ?) de type int
.
Ainsi l'expression 'j' - 'a'
'A'+ est un int
,
et donc System.out.println('j' - 'a' + 'A')
affichera le code
ascii de ``J
''.
Ça se corrige par un cast bien senti, notons au passage que le système
Java accepte de ranger un int
dans une variable de type
char
sans râler, je ne suis pas
capable d'expliquer pourquoi.
Pour l'anecdote une solution sans cast et sans entier
du problème de l'affichage des
caractères visibles est également possible :
for (char c = ' ' ; c <= '~' ; c++)
System.out.print(c);
System.out.println()
2.5 Passage par valeur
La règle générale de Java est que les arguments sont passés par
valeur.
Cela signifie que l'appel de méthode se fait par copie des valeurs
passées en argument et que chaque appel de méthode dispose de sa
propre version des paramètres.
Par exemple, soit la méthode:
static void dedans(int i) {
i = i+2 ;
System.out.println("i=" + i)
}
static void dehors(int i) {
System.out.println("i=" + i) ;
dedans(i) ;
System.out.println("i=" + i) ;
}
L'appel de dehors(0)
se traduira par l'affichage de trois
lignes i=0
, i=2
, i=0
. C'est très semblable à
l'exemple sur la structure de bloc. Ce qui compte c'est la portée des
variables.
Le résultat est sans doute moins inattendu si on considère cet autre
programme rigoureusement équivalent :
static void dedans(int j) { // Ca change ici
j = j+2 ;
System.out.println("j=" + j)
}
static void dehors(int i) {
System.out.println("i=" + i) ;
dedans(i) ;
System.out.println("i=" + i) ;
}
Mais ce que l'on doit bien comprendre c'est que le nom de l'argument
de dedans
, i
ou j
n'a pas d'importance, c'est une
variable muette et heureusement.
La règle se nuance très nettement dans le cas des objets, qui eux sont
passés par référence. C'est à dire que l'appelant et l'appelé
partagent le même objet.
(En chipotant c'est une référence sur l'objet qui est passée par valeur !).
Ceci concerne en particulier les tableaux qui sont des sortes
d'objets.
L'exemple suivant montre l'effet du passage par référence :
// Affichage d'un tableau (d'entiers) passé en argument
static void affiche(int[] t) {
for (int i = 0 ; i < t.length ; i++)
System.out.print ("t[" + i + "] = " + t[i] + " ") ;
System.out.println ("") ; // sauter une ligne
}
// trois façons d'ajouter deux
static void dedans (int[] t) {
t[0] = t[0] + 2;
t[1] += 2;
t[2]++ ; t[2]++ ;
}
static void dehors () {
int[] a = {1,2,3} ; // declaration et initialisation d'un tableau
affiche (a) ;
dedans (a) ;
affiche (a) ;
}
L'affichage est le suivant:
1 2 3
3 4 5
Ce qui illustre bien qu'il n'y a, tout au cours de l'exécution du
programme qu'un seul tableau, a
, t
étant juste des noms
différents de ce même tableau.
On peu aussi renommer l'argument t
en a
ça ne change
rien.
3 Cet obscur objet
Dans cette section, nous examinons la difficile et dialectique
question de la classe et de l'objet.
3.1 Utilisation des objets
Enfin, cette question sera examinée plus tard, pour le moment
les seuls objets connus sont construits pour nous par le système (par
exemple System.out
) ou
par nous sans nous en rendre compte (les chaînes, et
les tableaux).
Ajoutons-y l'objet null qui n'a ni champs, ni méthodes, ni
rien, mais alors rien du tout et qui se permet donc d'appartenir à
toutes les classes...
La MacLib expose pas mal d'objets.
Commençons par le point. Le point est un objet de la classe
Point.
On crée un nouveau point par un appel de constructeur dont la syntaxe
est :
Point p = new Point () ;
On peut aussi créer un point en donnant
explicitement ses coordonnées (entières) au constructeur :
Point p = new Point (100,100) ;
On dit que le constructeur de la classe Point est surchargé,
c'est à dire qu'il y a en fait deux constructeurs qui se distinguent
par le type de leurs arguments.
On peut ensuite accéder aux champs d'un point à l'aide de la notation
usuelle. Les points ont deux champs h et v qui sont
leurs coordonnées horizontales et verticales :
if (p.h == p.v)
System.out.println("Le point est sur le diagonale");
3.2 Fabrication des objets, que des champs
Un objet minimal n'a pas de méthodes, il sert uniquement à regrouper
des valeurs ensemble.
On crée très facilement une classe des paires d'entiers de la façon
suivante :
class IntPair {
int x ;
int y ;
IntPair (int x0, int y0) {
x = x0 ;
y = y0 ;
}
}
On remarque que les champs x et y ne sont pas
introduits par le mot-clef static, ils sont dynamiques.
Par conséquent, chaque objet crée aura ses propres champs.
Le programme :
IntPair p1 = new IntPair (0, 1) ;
IntPair p2 = new IntPair (2, 3) ;
System.out.println(p1.x + ", " + p2.y)
Affichera ``0, 3
''.
La structure de paire n'est pas très subtile, mais une définition à
peine différente donne les listes. Ici le deuxième champ est
simplement un autre objet de la même classe :
class IntList {
int cont ;
IntList rest ;
IntList (int cont, IntList rest) {
this.cont = cont ;
this.rest = rest ;
}
}
(Remarquez le truc du ``this.'' pour avoir des arguments de
constructeur et des champs homonymes.)
Avec une classe aussi minimale (aucune méthode), la liste vide est
null on réalise
l'opération premier du cours par l.cont et
l'opération reste par l.rest.
Enfin on construit une nouvelle liste à partir d'une ancienne à l'aide
du constructeur (unique) de la classe IntList, par
new IntList (
i,
l)
.
Mais on peut bien tout faire d'un coup.
Ainsi, on construira une liste à l'aide du constructeur :
IntList l = new IntList (1, new IntList (2, new IntList (3,null))) ;
Autre par exemple, on affichera une liste l
ainsi :
static void print (IntList l) {
while (l != null) {
System.out.println(l.cont) ;
l = l.rest ;
}
Dans un tel programme il faut remarquer une chose absolument fondamentale :
Avant que de faire l.cont
ou l.rest
, il faut
vérifier que l n'est pas l'objet null. Sinon, ça
plante.
3.3 Fabrication des objets, avec méthode
Il n'y a en fait plus grand chose à dire, on ajoute des méthodes
(non-statiques) dynamiques à la classe.
Voici par exemple une classe des piles d'entiers minimale
et conforme à la spécification des piles du
TD-3 :
class IntStack {
final static int MAX=64 ;
private int [] t ;
private int sp ;
IntStack () {
t = new int [MAX] ;
sp = 0 ;
}
private void error (String msg) {
System.err.println (msg) ;
System.exit (2) ;
}
boolean empty () {
return sp == 0 ;
}
int pop () {
if (empty ())
error ("Pop sur pile vide") ;
return t[--sp] ;
}
void push (int i) {
if (sp >= MAX)
error ("Push sur pile pleine") ;
t[sp++] = i ;
}
public String toString () {
StringBuffer r = new StringBuffer () ;
r.append ("{") ;
for (int i = 0 ; i < sp-1 ; i++) {
r.append(t[i]) ;
r.append(", ") ;
}
if (sp > 0)
r.append(t[sp-1]);
r.append ("}") ;
return r.toString () ;
}
}
Au passage, ce source (qui n'est pas la solution du TD-3, mais un
exemple) appelle quelques remarques :
-
Le mot-clef private (cf. la section 1.4)
est largement utilisé pour
restreindre à la classe ce qui ne regarde pas le monde extérieur.
- La méthode toString (qui doit être
publique, voir la section 1.4)
permettra d'afficher les piles par System.out.print, etc.
Le compilateur interprétant plus ou moins
``System.out.print(o)'' comme
``System.out.print(o.toString ())''.
- Le code de toString utilise la classe
StringBuffer (voir section 6.4),
essentiellement pour éviter les recopies de chaînes que la
concaténation ``+'' entraîne.
4 Exception
Les exceptions servent à rompre le flot normal de l'exécution.
Une exception est un objet (On s'en serait douté !) d'une classe
particulière (il y a plusieures classes d'exceptions).
Par exemple la classe ``Error'' est conventionnellement
utilisée pour signaler les situations d'erreur.
C'est le cas, par exemple encore, d'une tentative de dépilage d'une
pile vide, dans une quelconque classe Pile :
int depile() {
if (vide())
throw new Error ("Pile vide") ;
...
}
Observez la technique, un mot-clé, throw,
puis un objet d'une classe appropriée.
Lever l'exception Error modifie radicalement l'exécution en cours,
en particulier le retour de la méthode depile est immédiat et
aucune valeur n'est renvoyée. Plus généralement, le contrôle remonte
à travers toutes les méthodes en attente, jusqu'à planter le programme
lui-même.
Toutefois, on peut arrêter cette folle course en attrapant
l'exception. Ici on pourra par exemple encapsuler l'appel à pile,
dans une méthode qui renvoie 0 en cas de pile vide :
int depileZero (Pile p) {
try {
return p.depile () ;
} catch (Error e) {
System.err.println ("Erreur réparée : " + e.getMessage ()) ;
return 0 ;
}
}
Observez la technique, un mot-clé try, les instructions qui
peuvent lever l'exception, un autre mot-clé (catch)
avec l'exception attrapée en argument (entre (
...
)
) et les instructions à exécuter en cas d'exception.
Si Error n'est pas levée, tout se passe normalement, sinon
le contrôle passe en System.err.println ("Er
...
Les exceptions donnent la possibilité de traiter les cas d'erreur de
l'extérieur des méthodes qui les détectent et qui ne savent pas faire
autre chose que signaler l'erreur.
En particulier on peut attraper l'exception n'importe où (c'est à
dire pas immédiatement autour de la méthode qui la lève).
Ceci permet, par exemple, un traitement personalisé des erreurs dans
la méthode main :
public static void main (String [] arg) {
try {
...
} catch (Error e) {
System.err.println("Cher utilisateur, je suis au regret de devoir vous quitter") ;
System.exit(2) ;
}
}
Là où ça se complique encore, c'est que le système de type de Java
impose de signaler qu'une méthode peut lever une exception, dans le
cas de certaines exceptions.
Ce n'est pas le cas de Error
ci-dessus, c'est malheureusement le cas de IOException
(cf la section 5 suivante).
Les méthodes de librairie déclarent pouvoir lever de
telles exceptions à
l'aide de la déclaration throws (notez le ``s'').
C'est par exemple le cas de la méthode read, qui lit un
caractère et rend son code.
public int read() throws IOException
Plus raide encore, les méthodes qui
appellent une méthode dont la déclaration contient
``throws e'' et qui n'attrapent pas les exceptions
de la classe e, doivent également être déclarées comme pouvant
lever l'exception e.
C'est le cas, par exemple, d'une hypothétique méthode ``avalant'' les
espaces avant de lire un caractère :
int eat_space_read() throws IOException {
int c ;
do {
c = read () ;
if (c != ' ')
return c ;
} while (true) ;
}
5 Entrées-sorties
Nous savons déjà écrire dans la sortie standard (System.out
en Java) : il suffit d'utiliser les méthodes
System.out.print et System.out.println.
Pour lire dans l'entrée standard (System.in, comme vous
pouviez vous en douter), c'est un rien plus complexe, mais sans plus.
Il faut tout d'abord fabriquer une entrée, objet de la classe,
``Reader'' que l'on peut
traduire en ``lecteur'' à partir de System.in :
Reader entree = new InputStreamReader(System.in) ;
Ensuite, on peut lire un caractère de l'entrée par l'appel
``entree.read()''.
Cette fonction renvoie un entier (type int)
avec le comportement un peu tordu suivant :
-
Si un caractère est lu, il est renvoyé comme un entier.
- Si on est à la fin de l'entrée, -1 est renvoyé (la fin
de l'entrée est par vous signalée par ``Control-d'' au clavier et est la fin de
fichier si il y a eu redirection).
- En cas d'erreur l'exception IOException est levée.
Ces erreurs sont du genre de celles détectées au niveau du système
d'exploitation, comme la panne d'un
disque (rare !), ou un delai abusif de transmission par le réseau
(plus fréquent). Bref en gros, pour une raison ou une autre, la
lecture est jugée impossible.
Bref, voici Cat.java un filtre trivial
dont la sortie est une copie de l'entrée :
import java.io.*; // La classe Reader est dans le package java.io
class Cat {
public static void main(String [] args) {
Reader entree = new InputStreamReader(System.in) ;
try {
for (;;) {// Boucle infinie, on sort par break
int c = entree.read ();
if (c == -1)
break;
else
System.out.print ((char)c);
}
entree.close();
} catch (IOException e) { // En cas de malaise
System.err.print("Malaise : ") ;
System.err.println(e.getMessage()) ;
System.exit(-1) ; // Terminer le programme sur erreur
}
}
}
Remarquez :
-
Il y a un try...
catch,
car les méthodes
read et close peuvent lever l'exception
IOException.
À la fin du traitement de l'exception, on utilise System.exit
qui arrête le programme d'un coup.
- Pour afficher c comme un caractère, il faut le changer
en caractère, par ``(char)c''.
- Enfin, il est de bon ton de fermer les flots dont on a plus besoin
par la méthode ``close''. Cela permet au système Java de
récupérer les ressources par eux mobilisées. (Ici c'est inutile car
le programme termine immédiatement après.)
- En fait et à ignorer en première lecture, il vaut mieux, pour
des raisons d'efficacité, éviter
la lecture sur le disque à chaque
appel de read, pour ce faire, on intercalera un tampon
mémoire, à l'aide de la
classe BufferedReader.
6 Quelques fonctions (méthodes) de librairie
6.1 Les entiers
Les entiers (type int
) sont des valeurs de base du langage,
mais il existe aussi une classe
Integer
des entiers vus commes des objets.
Certaines des méthodes de cette classe sont utiles et notamment :
Les fonctions mathématiques usuelles sont définies dans la classe
Math du package java.lang (ouvert par défaut).
Cette classe contient entre autres :
-
abs
public static int abs(int a)
Retourne la valeur absolue de l'entier a
.
C'est une méthode statique, de sorte que l'on obtient la valeur
absolue de l'entier i par ``Math.abs(
i)
''.
- max, min
public static int max(int a, int b)
public static int min(int a, int b)
Retourne le plus grand (le plus petit)
des deux entiers a
et b
.
- random
public static synchronized double random()
Renvoie un double (flottant double précision)
pseudo-aléatoire entre 0.0 et 1.0.
Pour obtenir un entier aléatoire entre zéro et
MAX : (int)(Math.random() * MAX).
- cos, sin
public static native double cos(double a)
public static native double sin(double a)
Fonctions trigonométrique. Attention, les angles sont exprimés en
radians. D'où la définition de constante suivante.
- PI
public static final double PI
Approximation de Pi en flottant double précision.
Le mot-clé final indique une constante (valeur non
modifiable).
- round
public static int round(float a)
public static long round(double a)
Arrondi des flottants vers les entiers, notez que l'entier renvoyé est
un
long (entier sur 64 bits) dans le cas des flottants double
précision et un int (entier normal, sur 32 bits) dans le cas
des flottants simple précision. Snif.
6.3 Les chaînes
Les chaînes sont traitées par la classe
String
du package java.lang.
Néamoins, il y a un peu de syntaxe spécialisée, les constantes chaînes
s'écrivent entre doubles-quotes, par exemple ``"coucou"
''. En
outre, la concaténation de deux chaînes s1 et
s2, s'écrit
``s1+
s2''.
Les autres fonctions classiques sur les chaînes, sont
généralement des méthodes dynamiques normales. Il y a
entre autres :
-
length
public int length()
Renvoie la longueur de la chaîne. C'est une méthode dynamique, de
sorte que la longueur de la chaîne s s'obtient par
``s.length()
''.
- charAt
public char charAt(int index)
Renvoie le caractère d'indice index
. Les indices commencent à
zéro.
- equals
public boolean equals(Object obj)
Compare la chaîne et un objet quelconque. Renvoie true en cas
d'égalité et false autrement.
- compareTo
public int compareTo(String anotherString)
Compare la chaîne avec une autre selon l'ordre lexicographique
(l'ordre du dictionnaire).
L'appel
``s1.compareTo(
s2)
'' renvoie
un entier r avec :
-
r < 0, si s1 précède s2
dans le dictionnaire.
- r = 0, si les deux chaînes sont les mêmes.
- r > 0, si s1 suit s2
dans le dictionnaire.
- toUpperCase,
toLowerCase
public String toUpperCase()
public String toLowerCase()
Cette methode dynamique rend une version majusculée (resp. minusculée)
de la chaîne. Par exemple "COUCOU".toLowerCase{}
est la chaîne
"coucou"
.
En Java, on ne peut pas changer une chaîne (e.g. changer un caractère
individuellement) de plus la longueur des chaîne est fixée une fois
pour toutes. Or, on veut souvent construire une chaîne petit à petit,
on peut le faire en utilisant la concaténation ``+'' mais il y
a alors un prix à payer car une nouvelle chaîne est créée à chaque
concaténation.
En fait, lorsque l'on construit une chaîne de gauche vers la droite
(pour affichage d'un tableau par ex.) on a besoin d'un autre type de
structure de donnée : le tampon.
Les tampons (Buffer) de caractères sont traitées par la classe
StringBuffer
du package java.lang.
On trouvera un exemple d'utilisation classique de la classe
StringBuffer à la section 3.3 :
une création de tampon, des tas de ``append(...)'', et un
``toString()'' final.
Cette classe
Reader des
lecteurs offre une vision d'un flot d'entrée très
semblable à celle du langage C.
Un exemple d'utlisation complet est en section 5.
Elle est membre du package
java.io qui n'est pas ouvert par défaut, de sorte qu'il faut
écrire :
import java.io.* ;
quand on veut s'en servir. Une autre conséquence est que toutes les
méthodes exportée sont publiques (cf. section 1.4).
Les méthodes les plus utiles sont :
La classe Reader est abstraite, elle sert seulement à décrire
une fonctionnalité et il n'existe pas à proprement parler d'objets
de cette classe. Par conséquent, il n'existe pas de constructeurs de
la classe Reader.
En revanche, il existe diverses sous-classes de
Reader qui permettent de construire concrètement des objets.
qui peuvent se faire passer pour des Reader.
Il s'agit par example des classes
StringReader
(lecteur sur une chaîne),
InputStreamReader
(lecteur sur un flot d'octets) et
FileReader
(lecteur sur un fichier).
Le principe des Reader
va assez loin, car il est possible à partir d'un
Reader donné de fabriquer
un autre Reader qui possèdera une fonctionalité
supplémentaire.
C'est la cas de la classe
BufferedReader,
qui intercale un tampon mémoire (buffer) entre son entrée et sa
sortie. Dans la pratique, cette technique regroupe les appels au
système d'expoitation (notamment les accès au disque ou au réseau) et donne
une meilleure efficacité.
-
BufferedReader.
public BufferedReader(Reader in)
Crée un flot d'entrée tamponné, à partir d'un lecteur quelconque.
On peut créer un tel lecteur quelconque à partir d'un fichier de
nom ``nom'' comme un objet de la classe
FileReader.
De sorte que l'on récupère un flot d'entrée tamponné ouvert en lecture
sur un fichier par :
BufferedReader entree = new BufferedReader (new FileReader(nom));
On peut aussi créer un lecteur quelquonque à partir de l'entrée standard
``System.in'' comme un objet de la classe
InputStreamReader.
Soit en passant sur les détails :
BufferedReader entree = new BufferedReader (new InputStreamReader(System.in));
Farbriquer un BufferedReader à partir d'un
StringReader ne présente aucun intérêt, mais c'est possible...
6.7 Les tableaux
Les tableaux sont presques des objets, mais ils n'ont pas vraiment de
classe, tout se passe plus ou moins comme si, pour chaque type
type, un type des tableaux dont
les éléments sont de type type tombait du ciel.
Ce type (cette classe ???) des tableaux dont les éléments sont de type
type, se note, comme en C, ``type[]
''.
En particulier on a :
int [] ti ; // tableau d'entiers
String [] ts ; // tableau de chaînes
String [] [] tts ; // tableau de tableaux de chaînes
6.7.2 Valeurs tableaux
Contrairement à bien des langages, il est possible en Java, de
fabriquer des tableaux directement. On distingue:
-
La constante null avec laquelle on ne peut rien faire
est aussi un tableau dont on ne peut rien faire.
- L'appel de constructeur :
``
new
type [
taille]
'', où
type est le type des éléments du tableau et taille
la taille du tableau. Par exemple, pour déclarer et initialiser un
tableau t de seize entiers, on écrira :
static int [] ti = new int [16];
static String [] ts = new String [16];
static String [] [] tts = new String [16][16] ; // 16 x 16 chaînes.
Par défaut, les éléments du tableau vaudront un scalaire particulier
dans le cas des tableaux de scalaires
(zéro pour un tableau d'entiers, false pour un tableau de
booléens) et null dans dans le cas des tableaux d'objets,
cas qui comprend entre autres les tableaux de chaînes.
- Un tableau explicite, sous la forme
``
{
e1,
e2,
...
en}
'', où les ei sont des
expressions du type des éléments du tableau.
Par exemple, pour déclarer et initialiser le
tableau ti des seize premiers entiers, on écrira :
int [] ti = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16} ;
Dans le cas des tableaux de tableaux, on écrira :
int [] [] tti = {{1,2}, {3}};
C'est à dire que tti[0]
est le tableau {1,2}
et
tti[1]
est le tableau {3}
.
6.7.3 Opérations sur les tableaux
J'en vois deux :
-
Acceder à un élément :
``t
[
ei]
'',
où t est le tableau et ei une expression de
type entier. Attention, les indices commencent à zéro.
- Trouver la longueur d'un tableau : c'est une syntaxe objet, tout
se passe comme si la longueur était le champ length du
tableau, soit t
.length
.
7 La MacLib
La classe MacLib écrite par Philippe Chassignet
fournit une service de fenêtre graphique dans le style de la
librairie QuickDraw du Macintosh qui a suscité l'admiration de
générations de programmeurs.
Cette interface graphique présente l'amusante particularité que
l'origine des coordonées est en haut et à gauche.
Ce n'est pas une classe de librairie à proprement parler, puisqu'elle
ne fait pas partie de la distribution Java standard.
Mais c'est une partie de l'installation de Java à l'X et c'est
standard dans ce sens : tout le monde à l'X peut l'utiliser
(et en effet c'est une classe publique).
7.1 Objets graphiques
L'implantation Java de la MacLib utilise des objets graphiques,
les points les rectangles les polygones etc.
7.1.1 La classe Point
La classe des points est un grand
classique.
Elle sert d'exemple dans la présentation des objets de ce même
document à la section 3.1.
Les points n'ont pas tellement de méthodes utiles, ils servent plutôt
d'arguments aux autres méthodes de la MacLib.
En particulier les polygones sont des tableaux de points, de sorte que
l'on définit un triangle dont les trois sommets sont
les points p1
, p2
et p3
par :
Point p1 = ... ;
Point p2 = ... ;
Point p3 = ... ;
Point [] triangle = {p1, p2, p3} ;
7.1.2 La classe Rect
La classe des rectangle
est celle des objets graphiques qui sont rectangles.
Comme la classe des points, elle sert surtout à construire des objets
passés en argument aux méthodes de la MacLib.
-
Les constructeurs Rect(),
Rect(int, int, int, int) et
Rect(Point, Point).
public Rect()
public Rect(int left, int top, int right, int bottom)
public Rect(Point p1, Point p2)
La construction new Rect ()
renvoie un rectangle de taille
zéro et situé à l'origine
des coordonnées,
Le constructeur qui prend quatre entiers crée un rectangle défini par
ses points nord-ouest et sub-est.
Le dernier constructeur est plus flexible, il renvoie le plus petit
rectange qui contient les deux points passés en arguments.
- setRect
public void setRect(Rect r,int left, int top, int right, int bottom)
Met à jour le rectangle r en en faisant le rectangle compris
entre les coordonées horizontales left et right, et
les coordonées verticales top et bottom.
Normalement on a left est inférieur ou égal à
right et top est inférieur ou égal à
bottom.
Remarquons que les deux codes
``Rect r = new Rect () ; setRect(r,a,b,c,d);
'' et
``Rect r = new Rect (a,b,c,d);
'' sont équivalents.
La MacLib elle même fournit des méthodes qui contrôlent l'interface
graphique. Citons :
-
initQuickDraw
public static void InitQuickDraw()
Démarre la session (c'est à dire procède aux initialisations
nécessaires et affiche la fenêtre graphique).
- moveTo
public static void lineTo(int h, int v)
Pose le crayson au point de coordonnées (h, v).
- lineTo
public static void lineTo(int h, int v)
Trace un segment de droite de la position courante
au point de coordonnées (h, v). Le crayon est en (h,
v) après le tracé.
- foreColor,
backColor.
public static void foreColor(int color)
public static void backColor(int color)
Change la couleur de ce qui sera dessiné (respectivement effacé) par
la suite. L'argument est une couleur représentée par un entier.
La MacLib fournit un certain nombre de ces couleurs,
sous forme des variables (constantes)
blackColor,
whiteColor,
redColor,
greenColor,
blueColor,
cyanColor,
magentaColor,
et yellowColor.
Il y a d'autres façons de spécifier les couleurs dans la MacLib,
mais nous nous en tiendrons à celle-ci, si vous voulez bien.
- frameRect, paintRect,
eraseRect.
public static void frameRect(Rect r)
public static void paintRect(Rect r)
public static void eraseRect(Rect r)
Dessine le tour, paint l'intérieur ou efface le
rectangle passé en argument.
- framePolygon, paintPolygon,
erasePolygon.
public static void framePolygon(Point points[], int pCount)
public static void paintPolygon(Point points[], int pCount)
public static void erasePolygon(Point points[], int pCount)
Dessine le tour, paint l'intérieur ou efface le polygone passé en
argument. Le polygone est donné par le tableau de
points points accompagné du nombre pCount
des points à considérer en tant que sommets.
8 Quelques exemples tordus et trucs
Cette section regroupe des astuces de programmation ou corrige des
erreurs rencontrées lors des TD.
On a parfois recours à la notation complète des noms de variables pour
contourner la portée des variables (voir la section 2.3).
Considérons encore une fois la classe
Counter du
TD-1 et plus particulièrement sa méthode
init :
class Counter {
private static int [] count = null; // rien au de'part
private static String [] name = null; // non plus
private static int size = 0;
...
static void init (int size) {
count = new int [size];
name = new String [size];
Counter.size = size; // Oups !!!
}
}
Dans cette méthode, donc, l'argument size cache
le champ size de la classe, Il convient alors
de désigner ce champ à l'aide de la notation complète
Counter.size.
Notons que l'on aurrait très bien pu écrire :
static void init (int x) {
count = new int [size];
name = new String [size];
size = x;
}
Mais je trouve ça moins clair.
De même à l'intérieur d'une méthode non-statique,
(ou d'un constructeur), on peut cacher les champs
d'un objet par des liaisons locale et toujours avoir accès aux champs
en utilisant this qui est l'objet courant.
class IntList {
int cont ;
IntList rest ;
IntList (int cont, IntList rest) {
this.cont = cont ;
this.rest = rest ;
}
}
8.2 Sur les caractères
Les codes des caractères de '0'
à '9'
se suivent, de
sorte que les deux trucs suivants s'appliquent :
-
Pour savoir si un caractère c est un chiffre :
'0' <= c && c <= '9'
- Pour récupérer la valeur d'un chiffre :
int i = c - '0' ;
Des astuces analogues s'appliquent aux cas des lettres minuscules
et majuscules.
Index
-
abs, 6.2
- append(int), 6.4
- append(Object), 6.4
- append(String), 6.4
- BufferedReader (classe), 6.6
- BufferedReader, 6.6
- backColor, 7.2
- blackColor, 7.2
- blueColor, 7.2
- boolean (type scalaire), 2.4.1
- break (mot-clé), 2.3
- catch (mot-clé), 4
- char (type scalaire), 2.4.3, 8.2
- charAt, 6.3
- close, 6.5
- compareTo, 6.3
- continue (mot-clé), 2.3
- cos, 6.2
- cyanColor, 7.2
- do (mot-clé), 2.3
- else (mot-clé), 2.3
- equals, 6.3
- erasePolygon, 7.2
- eraseRect, 7.2
- extends (mot-clé), 1.5
- final (mot-clé), 2.2
- for (mot-clé), 2.3
- foreColor, 7.2
- framePolygon, 7.2
- frameRect, 7.2
- greenColor, 7.2
- h, 7.1.1
- héritage, 1.5
|
- Integer (classe), 6.1
- IntList (classe), 3.2
- IntPair (classe), 3.2
- if (mot-clé), 2.3
- initQuickDraw, 7.2
- int (type scalaire), 2.4.2
- length, 6.3
- lineTo, 7.2
- liste, 3.2
- long (type scalaire), 2.4.2
- MacLib (classe), 7
- Math (classe), 6.2
- magentaColor, 7.2
- main, 1.2
- max, 6.2
- min, 6.2
- moveTo, 7.2
- null, 3.1
- objet, 3
- overriding, 1.5
- PI, 6.2
- Point (classe), 7.1.1
- Point (classe), 3.1
- Point(), 7.1.1
- Point(int, int), 7.1.1
- paintPolygon, 7.2
- paintRect, 7.2
- paire, 3.2
- parseInt, 6.1
- polygone, 7.1.1, 7.2
- private (mot-clé), 1.4
- public (mot-clé), 1.4
- Reader (classe), 6.5
- Rect (classe), 7.1.2
|
- Rect(), 7.1.2
- Rect(int, int, int, int), 7.1.2
- Rect(Point, Point), 7.1.2
- random, 6.2
- read, 6.5
- redColor, 7.2
- return (mot-clé), 2.3
- round, 6.2
- String (classe), 6.3
- StringBuffer (classe), 6.4
- StringBuffer(), 6.4
- setRect, 7.1.2
- sin, 6.2
- sous-classe, 1.5
- static (mot-clé), 1.1, 2.2
- structure de bloc, 2.3
- surcharge, 3.1, 6.4
- tableaux, 6.7
- this (mot-clé), 2.3
- throw (mot-clé), 4
- throws (mot-clé), 4
- toLowerCase, 6.3
- toString(), 6.4, 7.1.1
- toUpperCase, 6.3
- try (mot-clé), 4
- type, 2.1
- v, 7.1.1
- while (mot-clé), 2.3
- whiteColor, 7.2
- yellowColor, 7.2
|
Ce document a été traduit de LATEX par
HEVEA.