Cour Ja Va Détaillé
Cour Ja Va Détaillé
Cour Ja Va Détaillé
Bienvenue dans mon cours de programmation en Java. C'est un langage très utilisé, notamment par un
grand nombre de programmeurs professionnels, ce qui en fait un langage incontournable actuellement.
Java est un langage de programmation moderne développé par Sun Microsystems (aujourd'hui racheté
parOracle). Il ne faut surtout pas le confondre avec JavaScript (langage de scripts utilisé principalement sur
les sites web), car Java n'a rien à voir.
Une de ses plus grandes forces est son excellente portabilité : une fois votre programme créé, il
fonctionnera automatiquement sous Windows, Mac, Linux, etc.
des applets, qui sont des programmes Java incorporés à des pages web ;
Exemple d'applet Java
Comme vous le voyez, Java permet de réaliser une très grande quantité d'applications différentes ! Mais...
comment apprendre un langage si vaste qui offre autant de possibilités ?
Heureusement, ce cours est là pour tout vous apprendre de Java à partir de zéro .
Je tiens à faire une dédicace spéciale à ptipilou, zCorrecteur émérite, sans qui ce cours n'aurait pas vu le
jour !
Un grand merci pour ton travail et ton soutien !
#
L'un des principes phares de Java réside dans sa machine virtuelle : celle-ci assure à tous les développeurs
Java qu'un programme sera utilisable avec tous les systèmes d'exploitation sur lesquels est installée une
machine virtuelle Java.
Lors de la phase de compilation de notre code source, celui-ci prend une forme intermédiaire appelée byte
code : c'est le fameux code inintelligible pour votre machine, mais interprétable par la machine virtuelle
Java. Cette dernière porte un nom : on parle plus communément de JRE (Java Runtime Environment).
Plus besoin de se soucier des spécificités liées à tel ou tel OS (Operating System, soit système
d'exploitation). Nous pourrons donc nous consacrer entièrement à notre programme.
JRE ou JDK
Commencez par télécharger l'environnement Java sur le site d'Oracle, comme le montre la figure suivante.
Choisissez la dernière version stable.
Encart
de téléchargement
Vous avez sans doute remarqué qu'on vous propose de télécharger soit le JRE, soit
le JDK (Java DevelopmentKit). La différence entre ces deux environnements est écrite, mais pour les
personnes fâchées avec l'anglais, sachez que le JRE contient tout le nécessaire pour que vos programmes
Java puissent être exécutés sur votre ordinateur ; le JDK, en plus de contenir le JRE, contient tout le
nécessaire pour développer, compiler…
L'IDE contenant déjà tout le nécessaire pour le développement et la compilation, nous n'avons besoin que
du JRE. Une fois que vous avez cliqué sur Download JRE, vous arrivez sur la page représentée à la figure
suivante.
Choix
du système d'exploitation
Cochez la case : Accept License Agreement puis cliquez sur le lien correspondant à votre système
d'exploitation (x86 pour un système 32 bits et x64 pour un système 64 bits). Une popup de téléchargement
doit alors apparaître.
Je vous ai dit que Java permet de développer différents types d'applications ; il y a donc des
environnements permettant de créer des programmes pour différentes plates-formes :
J2SE (Java 2 Standard Edition, celui qui nous intéresse dans cet ouvrage) : permet de
développer des applications dites « client lourd », par exemple Word, Excel, la suite
OpenOffice.org… Toutes ces applications sont des « clients lourds » . C'est ce que nous allons
faire dans ce cours
J2EE (Java 2 Enterprise Edition) : permet de développer des applications web en Java. On
parle aussi de clients légers.
J2ME (Java 2 Micro Edition) : permet de développer des applications pour appareils portables,
comme des téléphones portables, des PDA…
Eclipse IDE
Avant toute chose, quelques mots sur le projet Eclipse. « Eclipse IDE » est un environnement de
développement libre permettant de créer des programmes dans de nombreux langages de programmation
(Java, C++, PHP…). C'est l'outil que nous allons utiliser pour programmer.
Eclipse IDE est lui-même principalement écrit en Java.
Je vous invite donc à télécharger Eclipse IDE. Une fois la page de téléchargement
choisissezEclipse IDE for Java Developers, en choisissant la version d'Eclipse correspondant à
votre OS (Operating System = système d'exploitation), comme indiqué à la figure suivante.
Pour ceux qui l'avaient deviné, Eclipse est le petit logiciel qui va nous permettre de développer nos
applications ou nos applets, et aussi celui qui va compiler tout ça. Notre logiciel va donc permettre de
traduire nos futurs programmes Java en langage byte code, compréhensible uniquement par votre JRE,
fraîchement installé.
La spécificité d'Eclipse IDE vient du fait que son architecture est totalement développée autour de la notion
de plugin. Cela signifie que toutes ses fonctionnalités sont développées en tant que plugins. Pour faire
court, si vous voulez ajouter des fonctionnalités à Eclipse, vous devez :
Lorsque vous téléchargez un nouveau plugin pour Eclipse, celui-ci se présente souvent comme un dossier
contenant généralement deux sous-dossiers : un dossier « plugins » et un dossier « features ». Ces dossiers
existent aussi dans le répertoire d'Eclipse. Il vous faut donc copier le contenu des dossiers de votre plugin
vers le dossier correspondant dans Eclipse (plugins dans plugins etfeatures dans features).
Vous devez maintenant avoir une archive contenant Eclipse. Décompressez-la où vous voulez, entrez dans
ce dossier et lancez Eclipse. Au démarrage, comme le montre la figure suivante, Eclipse vous demande
dans quel dossier vous souhaitez enregistrer vos projets ; sachez que rien ne vous empêche de spécifier un
autre dossier que celui proposé par défaut. Une fois cette étape effectuée, vous arrivez sur la page d'accueil
d'Eclipse. Si vous avez envie d'y jeter un oeil, allez-y !
Vous devez indiquer où enregistrer vos projets
Je vais maintenant vous faire faire un tour rapide de l'interface d'Eclipse. Voici les principaux menus :
File : C'est ici que nous pourrons créer de nouveaux projets Java, les enregistrer et les
exporter le cas échéant.
Les raccourcis à retenir sont :
o ALT + SHIFT + N : nouveau projet ;
Edit : Dans ce menu, nous pourrons utiliser les commandes « copier » , « coller », etc.
Window : Dans celui-ci, nous pourrons configurer Eclipse selon nos besoins.
La barre d'outils
1. nouveau général : cliquer sur ce bouton revient à faire Fichier > Nouveau ;
2. enregistrer : revient à faire CTRL + S ;
Maintenant, je vais vous demander de créer un nouveau projet Java, comme indiqué aux figures suivantes.
Création de projet
Java - étape 1
Création
de projet Java - étape 2
Renseignez le nom de votre projet comme je l'ai fait dans le premier encadré de la deuxième figure. Vous
pouvez aussi voir où sera enregistré ce projet. Un peu plus compliqué, maintenant : vous avez un
environnement Java sur votre machine, mais dans le cas où vous en auriez plusieurs, vous pouvez aussi
spécifier à Eclipse quel JRE utiliser pour ce projet, comme sur le deuxième encadré de la deuxième figure.
Vous pourrez changer ceci à tout moment dans Eclipse en allant dans Window > Preferences, en
dépliant l'arbre Java dans la fenêtre et en choisissant Installed JRE.
Vous devriez avoir un nouveau projet dans la fenêtre de gauche, comme à la figure suivante.
Explorateur de projet
Pour boucler la boucle, ajoutons dès maintenant une nouvelle classe dans ce projet comme nous avons
appris à le faire plus tôt via la barre d'outils. La figure suivante représente la fenêtre sur laquelle vous
devriez tomber.
Une classe est un ensemble de codes contenant plusieurs instructions que doit effectuer votre programme.
Ne vous attardez pas trop sur ce terme, nous aurons l'occasion d'y revenir.
Création
d'une classe
Dans l'encadré 1, nous pouvons voir où seront enregistrés nos fichiers Java. Dans l'encadré 2, nommez
votre classe Java ; moi, j'ai choisi « sdz1 ». Dans l'encadré 3, Eclipse vous demande si cette classe a
quelque chose de particulier. Eh bien oui ! Cochez
public static void main(String[] args) (nous reviendrons plus tard sur ce point), puis
cliquez surFinish. La fenêtre principale d'Eclipse se lance, comme à la figure suivante.
Fenêtre principale d'Eclipse
Avant de commencer à coder, nous allons explorer l'espace de travail. Dans l'encadré de gauche (le vert),
vous trouverez le dossier de votre projet ainsi que son contenu. Ici, vous pourrez gérer votre projet comme
bon vous semble (ajout, suppression…). Dans l'encadré positionné au centre (le bleu), je pense que vous
avez deviné : c'est ici que nous allons écrire nos codes source. Dans l'encadré du bas (le rouge), c'est là que
vous verrez apparaître le contenu de vos programmes… ainsi que les erreurs éventuelles ! Et pour finir,
c'est dans l'encadré de droite (le violet), dès que nous aurons appris à coder nos propres fonctions et nos
objets, que la liste des méthodes et des variables sera affichée.
Comme je vous l'ai maintes fois répété, les programmes Java sont, avant d'être utilisés par la machine
virtuelle, précompilés en byte code (par votre IDE ou à la main). Ce byte code n'est compréhensible que par
une JVM, et c'est celle-ci qui va faire le lien entre ce code et votre machine.
Vous aviez sûrement remarqué que sur la page de téléchargement du JRE, plusieurs liens étaient
disponibles :
un lien pour Windows ;
Ceci, car la machine virtuelle Java se présente différemment selon qu'on se trouve sous Mac, sous Linux ou
encore sous Windows. Par contre, le byte code, lui, reste le même quel que soit l'environnement avec lequel
a été développé et précompilé votre programme Java. Conséquence directe : quel que soit l'OS sous lequel
a été codé un programme Java, n'importe quelle machine pourra l'exécuter si elle dispose d'une JVM !
Tu n'arrêtes pas de nous rabâcher byte code par-ci, byte code par-là… Mais c'est quoi, au juste ?
Eh bien, un byte code (il existe plusieurs types de byte code, mais nous parlons ici de celui créé par Java)
n'est rien d'autre qu'un code intermédiaire entre votre code Java et le code machine. Ce code particulier se
trouve dans les fichiers précompilés de vos programmes ; en Java, un fichier source a pour
extension .java et un fichier précompilé a l'extension .class : c'est dans ce dernier que vous trouverez
du byte code. Je vous invite à examiner un fichier .class à la fin de cette partie (vous en aurez au moins
un), mais je vous préviens, c'est illisible !
Par contre, vos fichiers .java sont de simples fichiers texte dont l'extension a été changée. Vous pouvez
donc les ouvrir, les créer ou encore les mettre à jour avec le Bloc-notes de Windows, par exemple. Cela
implique que, si vous le souhaitez, vous pouvez écrire des programmes Java avec le Bloc-notes ou encore
avec Notepad++.
Reprenons. Vous devez savoir que tous les programmes Java sont composés d'au moins une classe. Elle
doit contenir une méthode appelée main : ce sera le point de démarrage de notre programme.
Une méthode est une suite d'instructions à exécuter. C'est un morceau de logique de notre programme. Une
méthode contient :
Vous verrez un peu plus tard qu'un programme n'est qu'une multitude de classes qui s'utilisent l'une l'autre.
Mais pour le moment, nous n'allons travailler qu'avec une seule classe.
Je vous avais demandé de créer un projet Java ; ouvrez-le ! Vous voyez la fameuse classe dont je vous
parlais ? Ici, elle s'appelle « sdz1 », comme à la figure suivante. Vous pouvez voir que le mot class est
précédé du mot public, dont nous verrons la signification lorsque nous programmerons des objets.
Méthode principale
Pour le moment, ce que vous devez retenir, c'est que votre classe est définie par un mot clé (class),
qu'elle a un nom (ici, « sdz1 ») et que son contenu est délimité par des accolades ({ }). Nous écrirons nos
codes sources entre les accolades de la méthode main. La syntaxe de cette méthode est toujours la même :
Bonne question ! Je vous ai dit précédemment que votre programme Java, avant de pouvoir être exécuté,
doit être précompilé en byte code. Eh bien, la possibilité de forcer le compilateur à ignorer certaines
instructions existe ! C’est ce qu’on appelle des commentaires, et deux syntaxes sont disponibles pour
commenter son texte :
1. Les commentaires unilignes : introduits par les symboles « // », ils mettent tout ce qui les suit
en commentaire, du moment que le texte se trouve sur la même ligne ;
2. Les commentaires multilignes : ils sont introduits par les symboles « /* » et se terminent par les
symboles « */ ».
public static void main(String[] args){
//Un commentaire
//Un autre
//Encore un autre
/*
Un commentaire
Un autre
Encore un autre
*/
C'est simple : au début, vous ne ferez que de très petits programmes. Mais dès que vous aurez pris de la
bouteille, leurs tailles et le nombre de classes qui les composeront vont augmenter. Vous serez contents de
trouver quelques lignes de commentaires au début de votre classe pour vous dire à quoi elle sert, ou encore
des commentaires dans une méthode qui effectue des choses compliquées afin de savoir où vous en êtes
dans vos traitements…
Il existe en fait une troisième syntaxe, mais elle a une utilité particulière. Elle permettra de générer une
documentation pour votre programme (on l'appelle « Javadoc » pour « Java Documentation »). Je n'en
parlerai que très peu, et pas dans ce chapitre. Nous verrons cela lorsque nous programmerons des objets,
mais pour les curieux, je vous conseille le très bon cours de dworkin sur ce sujet disponible sur le Site du
Zéro.
À partir de maintenant et jusqu'à ce que nous programmions des interfaces graphiques, nous allons faire ce
qu'on appelle des programmes procéduraux. Cela signifie que le programme s'exécutera de façon
procédurale, c'est-à-dire qui s'effectue de haut en bas, une ligne après l'autre. Bien sûr, il y a des
instructions qui permettent de répéter des morceaux de code, mais le programme en lui-même se terminera
une fois parvenu à la fin du code. Cela vient en opposition à la programmation événementielle (ou
graphique) qui, elle, est basée sur des événements (clic de souris, choix dans un menu…).
Hello World
Bouton de lancement du
programme
Si vous regardez dans votre console, dans la fenêtre du bas sous Eclipse, vous devriez voir quelque chose
ressemblant à la figure suivante.
La console d'Eclipse
Expliquons un peu cette ligne de code.
Littéralement, elle signifie « la méthode print() va écrire « Hello World ! » en utilisant l'objet out de la
classe System » . Avant que vous arrachiez les cheveux, voici quelques précisions :
System : ceci correspond à l'appel d'une classe qui se nomme « System » . C'est une classe
utilitaire qui permet surtout d'utiliser l'entrée et la sortie standard, c'est-à-dire la saisie clavier et
l'affichage à l'écran.
print : méthode qui écrit dans la console le texte passé en paramètre (entre les parenthèses).
Donc, si nous reprenons notre code précédent et que nous appliquons cela, voici ce que ça donnerait :
<samp>Hello World !
My name is
Cysboy</samp>
Vous pouvez voir que :
lorsque vous utilisez le caractère d'échappement \n, quelle que soit la méthode appelée, celle-
ci ajoute immédiatement un retour à la ligne à son emplacement ;
J'en profite au passage pour vous mentionner deux autres caractères d'échappement :
1. \r va insérer un retour chariot, parfois utilisé aussi pour les retours à la ligne ;
2. \t va faire une tabulation.
Vous avez sûrement remarqué que la chaîne de caractères que l'on affiche est entourée par des « " ». En
Java, les guillemets doubles sont des délimiteurs de chaînes de caractères ! Si vous voulez afficher un
guillemet double dans la sortie standard, vous devrez « l'échapper » avec un « \ », ce qui donnerait
: "Coucou mon \"chou\" !". Il n'est pas rare de croiser le terme anglais quote pour désigner les
guillemets droits. Cela fait en quelque sorte partie du jargon du programmeur.
Je vous propose maintenant de passer un peu de temps sur la compilation de vos programmes en ligne de
commande. Cette partie n'est pas obligatoire, loin de là, mais elle ne peut être qu'enrichissante.
Bienvenue donc aux plus curieux ! Avant de vous apprendre à compiler et à exécuter un programme en
ligne de commande, il va vous falloir le JDK (Java SE Development Kit). C'est avec celui-ci que nous
aurons de quoi compiler nos programmes. Le nécessaire à l'exécution des programmes est dans le JRE…
mais il est également inclus dans le JDK. Je vous invite donc à retourner sur le site d'Oracle et à télécharger
ce dernier. Une fois cette opération effectuée, il est conseillé de mettre à jour votre variable
d'environnement %PATH%.
Euh… quoi ?
Votre « variable d'environnement ». C'est grâce à elle que Windows trouve des exécutables sans qu'il soit
nécessaire de lui spécifier le chemin d'accès complet. Vous — enfin, Windows — en a plusieurs, mais nous
ne nous intéresserons qu'à une seule. En gros, cette variable contient le chemin d'accès à certains
programmes.
Par exemple, si vous spécifiez le chemin d'accès à un programme X dans votre variable d'environnement et
que, par un malheureux hasard, vous n'avez plus aucun raccourci vers X, vous l'avez définitivement perdu
dans les méandres de votre PC. Eh bien vous pourrez le lancer en faisant Démarrer > Exécuter et en
tapant la commande X.exe (en partant du principe que le nom de l'exécutable est « X.exe »</minicode>
.
J'y arrive. Une fois votre JDK installé, ouvrez le répertoire bin de celui-ci, ainsi que celui de votre JRE.
Nous allons nous attarder sur deux fichiers.
Dans le répertoire bin de votre JRE, vous devez avoir un fichier nommé java.exe, que vous retrouvez
aussi dans le répertoire bin de votre JDK. C'est grâce à ce fichier que votre ordinateur peut lancer vos
programmes par le biais de la JVM. Le deuxième ne se trouve que dans le répertoire bin de votre JDK, il
s'agit de javac.exe (Java compiler). C'est celui-ci qui va précompiler vos programmes Java en byte code.
Alors, pourquoi mettre à jour la variable d'environnement pour le JDK ? Eh bien, compiler et exécuter en
ligne de commande revient à utiliser ces deux fichiers en leur précisant où se trouvent les fichiers à traiter.
Cela veut dire que si l'on ne met pas à jour la variable d'environnement de Windows, il nous faudrait :
Avec notre variable d'environnement mise à jour, nous n'aurons plus qu'à :
appeler la commande ;
Allez dans le panneau de configuration de votre PC ; de là, cliquez sur l'icône Système ; choisissez
l'ongletAvancé et vous devriez voir en bas un bouton nommé Variables d'environnement : cliquez
dessus. Une nouvelle fenêtre s'ouvre. Dans la partie inférieure intitulée Variables système, cherchez
la variablePath. Une fois sélectionnée, cliquez sur Modifier. Encore une fois, une fenêtre, plus petite
celle-ci, s'ouvre devant vous. Elle contient le nom de la variable et sa valeur.
Ne changez pas son nom et n'effacez pas son contenu ! Nous allons juste ajouter un chemin d'accès.
Pour ce faire, allez jusqu'au bout de la valeur de la variable, ajoutez-y un point-virgule s'il n'y en a pas et
ajoutez le chemin d'accès au répertoire bin de votre JDK, en terminant celui-ci par un point-virgule ! Chez
moi, ça donne ceci : C:\Sun\SDK\jdk\bin.
%SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;
Et maintenant :
%SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;C:\Sun\SDK\jdk\bin;
Validez les changements : vous êtes maintenant prêts à compiler en ligne de commande.
Pour bien faire, allez dans le répertoire de votre premier programme et effacez le .class. Ensuite,
faitesDémarrer > Exécuter (ou encore touche Windows + R et tapez « cmd » .
Dans l'invite de commande, on se déplace de dossier en dossier grâce à
l'instruction cd.cd <nom du dossier enfant> pour aller dans un dossier contenu dans celui dans
lequel nous nous trouvons, cd .. pour remonter d'un dossier dans la hiérarchie.
Par exemple, lorsque j'ouvre la console, je me trouve dans le dossier C:\toto\titi et mon application se
trouve dans le dossier C:\sdz, je fais donc :
<samp>cd ..
cd ..
cd sdz</samp>
Après de la première instruction, je me retrouve dans le dossier C:\toto. Grâce à la deuxième instruction,
j'arrive à la racine de mon disque. Via la troisième instruction, je me retrouve dans le dossier C:\sdz.
Nous sommes maintenant dans le dossier contenant notre fichier Java ! Cela dit, nous pouvions condenser
cela en :
<samp>cd ../../sdz</samp>
Maintenant, vous pouvez créer votre fichier .class en exécutant la commande suivante :
<samp>javac <nomDeFichier.java></samp>
Si, dans votre dossier, vous avez un fichier test.java, compilez-le en faisant :
<samp>javac test.java</samp>
Et si vous n'avez aucun message d'erreur, vous pouvez vérifier que le fichier test.class est présent en
utilisant l'instruction dir qui liste le contenu d'un répertoire. Cette étape franchie, vous pouvez lancer votre
programme Java en faisant ce qui suit :
<samp>java <nomFichierClassSansExtension></samp>
Ce qui nous donne :
<samp>java test</samp>
Et normalement, le résultat de votre programme Java s'affiche sous vos yeux ébahis !
Attention : il ne faut pas mettre l'extension du fichier pour le lancer, mais il faut la mettre pour le compiler.
Voilà : vous avez compilé et exécuté un programme Java en ligne de commande… Vous avez pu voir qu'il
n'y a rien de vraiment compliqué et, qui sait, vous en aurez peut-être besoin un jour.
En résumé
Les fichiers contenant le code source de vos programmes Java ont l'extension .java.
Les fichiers précompilés correspondant à vos codes source Java ont l'extension .class.
Le byte code est un code intermédiaire entre celui de votre programme et celui que votre
machine peut comprendre.
Un programme Java, codé sous Windows, peut être précompilé sous Mac et enfin exécuté sous
Linux.
Votre machine NE PEUT PAS comprendre le byte code, elle a besoin de la JVM.
Tous les programmes Java sont composés d'au moins une classe.
Le point de départ de tout programme Java est la méthode public static void
main(String[] args).
Nous commençons maintenant sérieusement la programmation. Dans ce chapitre, nous allons découvrir les
variables. On les retrouve dans la quasi-totalité des langages de programmation. Une variable est un
élément qui stocke des informations de toute sorte en mémoire : des chiffres, des résultats de calcul, des
tableaux, des renseignements fournis par l'utilisateur…
Vous ne pourrez pas programmer sans variables. Il est donc indispensable que je vous les présente !
Petit rappel
Avant de commencer, je vous propose un petit rappel sur le fonctionnement d'un ordinateur et
particulièrement sur la façon dont ce dernier interprète notre façon de voir le monde…
Vous n'êtes pas sans savoir que votre ordinateur ne parle qu'une seule langue : le binaire ! Le langage
binaire est une simple suite de 0 et de 1. Vous devez vous rendre compte qu'il nous serait très difficile, en
tant qu'êtres humains, d'écrire des programmes informatiques pour expliquer à nos ordinateurs ce qu'ils
doivent faire, entièrement en binaire… Vous imaginez ! Des millions de 0 et de 1 qui se suivent ! Non, ce
n'était pas possible ! De ce fait, des langages de programmation ont été créés afin que nous ayons à
disposition des instructions claires pour créer nos programmes. Ces programmes sont ensuite compilés pour
que nos instructions humainement compréhensibles soient, après coup, compréhensible par votre machine.
Le langage binaire est donc une suite de 0 et de 1 qu'on appelle bit. Si vous êtes habitués à la manipulation
de fichiers (audio, vidéos, etc.) vous devez savoir qu'il existe plusieurs catégories de poids de programme
(Ko, Mo, Go, etc.). Tous ces poids correspondent au système métrique informatique. Le tableau suivant
présente les poids les plus fréquemment rencontrés :
Si vous vous posez cette question c'est parce que vous ne savez pas encore compter comme un ordinateur et
que vous êtes trop habitués à utiliser un système en base 10. Je sais, c'est un peu confus… Pour comprendre
pourquoi ce découpage est fait de la sorte, vous devez comprendre que votre façon de compter n'est pas
identique à celle de votre machine. En effet, vous avez l'habitude de compter ainsi :
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ... 254, 255, 256 ... 12345678, 12345679,
12345680
Cette façon de compter repose sur une base 10, car elle se décompose en utilisant des puissances de 10.
Ainsi, le nombre 1024 peut se décomposer de cette façon : 1×1000+0×100+2×10+4×1.
Pour bien comprendre ce qui suit, vous devez aussi savoir que tout nombre élevé à la puissance 0 vaut 1,
donc100=1. Partant de ce postulat, nous pouvons donc réécrire la décomposition du nombre 1024 ainsi
: 1×103+0×102+2×101+4×100. Nous multiplions donc la base utilisée, ordonnée par puissance, par un
nombre compris entre 0 et cette base moins 1 (de 0 à 9).
Sauf que votre machine parle en binaire, elle compte donc en base 2. Cela revient donc à appliquer la
décomposition précédente en remplaçant les 10 par des 2. Par contre, vous n'aurez que deux multiplicateurs
possibles : 0 ou 1 (et oui, vous êtes en base 2). De ce fait, en base 2, nous pourrions avoir ce genre de chose
: 1×23+1×22+0×21+1×20, qui peut se traduire de la sorte : 1×8+1×4+0×2+1×1 donc 8+4+0+1 soit 13.
Donc, 1101 en base 2 s'écrit 13 en base 10. Et donc pourquoi des paquets de 1024 comme délimiteur de
poids ? Car ce nombre correspond à une puissance de 2 : 1024=210.
Dans le monde de l'informatique, il existe une autre façon de compter très répandue : l'hexadécimal. Dans
ce cas, nous comptons en base 16 :
1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1A, 1B
, 1C.... 5E, 5F, 60, ... A53A, A53B, A53C...
C'est tordu ! À quoi ça peut bien servir ?
Le côté pratique de cette notation c'est qu'elle se base sur une subdivision d'un octet. Pour représenter un
nombre de 0 à 15 (donc les seize premiers nombres), 4 bits sont nécessaires
: 0×23+0×22+0×21+0×20=0 et 1×23+1×22+1×21+1×20=15.
En fait, vous savez maintenant qu'un octet est un regroupement de 8 bits. Utiliser l'hexadécimal permet de
simplifier la notation binaire car, si vous regroupez votre octet de bits en deux paquets de 4 bits, vous
pouvez représenter chaque paquet avec un caractère hexadécimal. Voici un exemple :
Un nombre binaire
conséquent
Les différents types de variables
Nous allons commencer par découvrir comment créer des variables dans la mémoire. Pour cela, il faut les
déclarer. Une déclaration de variable se fait comme ceci :
Ce qu'on appelle des types simples ou types primitifs, en Java, ce sont tout bonnement des nombres entiers,
des nombres réels, des booléens ou encore des caractères, et vous allez voir qu'il y a plusieurs façons de
déclarer certains de ces types.
Le type byte (1 octet) peut contenir les entiers entre -128 et +127.
byte temperature;
temperature = 64;
Le type short (2 octets) contient les entiers compris entre -32768 et +32767.
short vitesseMax;
vitesseMax = 32000;
Le type int (4 octets) va de -2*109 à 2*109 (2 et 9 zéros derrière… ce qui fait déjà un joli nombre).
int temperatureSoleil;
temperatureSoleil = 15600000; //La température est exprimée en kelvins
Le type long (8 octets) peut aller de −9×1018 à 9×1018 (encore plus gros…).
long anneeLumiere;
anneeLumiere = 9460700000000000L;
Afin d'informer la JVM que le type utilisé est long, vous DEVEZ ajouter un "L" à la fin de votre nombre,
sinon le compilateur essaiera d'allouer ce dernier dans une taille d'espace mémoire de type entier et votre
code ne compilera pas si votre nombre est trop grand...
Le type float (4 octets) est utilisé pour les nombres avec une virgule flottante.
float pi;
pi = 3.141592653f;
Ou encore :
float nombre;
nombre = 2.0f;
Vous remarquerez que nous ne mettons pas une virgule, mais un point ! Et vous remarquerez aussi que
même si le nombre en question est rond, on écrit « .0 » derrière celui-ci, le tout suivi de « f ».
Le type double (8 octets) est identique à float, si ce n'est qu'il contient plus de chiffres derrière la
virgule et qu'il n'a pas de suffixe.
double division;
division = 0.333333333333333333333333333333333333333333334d;
Ici encore, vous devez utiliser une lettre - le « d » - pour parfaire la déclaration de votre variable.
char caractere;
caractere = 'A';
Le type boolean, lui, ne peut contenir que deux valeurs : true (vrai) ou false (faux), sans guillemets
(ces valeurs sont natives dans le langage, il les comprend directement et sait les interpréter).
boolean question;
question = true;
Le type String permet de gérer les chaînes de caractères, c'est-à-dire le stockage de texte.
Il s'agit d'une variable d'un type plus complexe que l'on appelle objet. Vous verrez que celle-ci s'utilise un
peu différemment des variables précédentes :
On te croit sur parole, mais pourquoi String commence par une majuscule et pas les autres ?
C'est simple : il s'agit d'une convention de nommage. En fait, c'est une façon d'appeler nos classes, nos
variables, etc. Il faut que vous essayiez de la respecter au maximum. Cette convention, la voici :
si le nom d'une variable est composé de plusieurs mots, le premier commence par une
minuscule, le ou les autres par une majuscule, et ce, sans séparation ;
tout ceci sans accentuation !
Je sais que la première classe que je vous ai demandé de créer ne respecte pas cette convention, mais je ne
voulais pas vous en parler à ce moment-là… Donc, à présent, je vous demanderai de ne pas oublier ces
règles !
Veillez à bien respecter la casse (majuscules et minuscules), car une déclaration de CHAR à la place
de char ou autre chose provoquera une erreur, tout comme une variable de type string à la place
de String !
Faites donc bien attention lors de vos déclarations de variables… Une petite astuce quand même (enfin
deux, plutôt) : on peut très bien compacter les phases de déclaration et d'initialisation en une seule phase !
Comme ceci :
Avant de nous lancer dans la programmation, nous allons faire un peu de mathématiques avec nos
variables.
Les opérateurs arithmétiques sont ceux que l'on apprend à l'école primaire… ou presque :
« + » : permet d'additionner deux variables numériques (mais aussi de concaténer des chaînes
de caractères ; ne vous inquiétez pas, on aura l'occasion d'y revenir).
« - » : permet de soustraire deux variables numériques.
« / » : permet de diviser deux variables numériques (mais je crois que vous aviez deviné).
Je me doute bien que le modulo est assez difficile à assimiler. Voici une utilisation assez simple : pour
vérifier qu'un entier est pair, il suffit de vérifier que son modulo 2 renvoie 0.
Maintenant, voici quelque chose que les personnes qui n'ont jamais programmé ont du mal à intégrer. Je
garde la même déclaration de variables que ci-dessus.
nbre1 = nbre1 + 1;
nbre1 += 1;
nbre1++;
++nbre1;
Les trois premières syntaxes correspondent exactement à la même opération. La troisième sera
certainement celle que vous utiliserez le plus, mais elle ne fonctionne que pour augmenter d'une unité la
valeur de nbre1! Si vous voulez augmenter de 2 la valeur d'une variable, utilisez les deux syntaxes
précédentes. On appelle cela l'incrémentation. La dernière fait la même chose que la troisième, mais il y a
une subtilité dont nous reparlerons dans le chapitre sur les boucles.
nbre1 = nbre1 * 2;
nbre1 *= 2;
nbre1 = nbre1 / 2;
nbre1 /= 2;
Très important : on ne peut faire du traitement arithmétique que sur des variables de même type sous peine
de perdre de la précision lors du calcul. On ne s'amuse pas à diviser un int par unfloat, ou pire, par
un char ! Ceci est valable pour tous les opérateurs arithmétiques et pour tous les types de variables
numériques. Essayez de garder une certaine rigueur pour vos calculs arithmétiques.
Voici les raisons de ma mise en garde : comme je vous l'ai dit précédemment, chaque type de variable a une
capacité différente et, pour faire simple, nous allons comparer nos variables à différents récipients. Une
variable de type :
À partir de là, ce n'est plus qu'une question de bon sens. Vous devez facilement constater qu'il est possible
de mettre le contenu d'un dé à coudre dans un verre ou un baril. Par contre, si vous versez le contenu d'un
baril dans un verre… il y en a plein par terre !
Ainsi, si nous affectons le résultat d'une opération sur deux variables de type double dans une variable de
type int, le résultat sera de type int et ne sera donc pas un réel mais un entier.
Je suppose que vous voudriez aussi mettre du texte en même temps que vos variables… Eh bien sachez que
l'opérateur « + » sert aussi d'opérateur de concaténation, c'est-à-dire qu'il permet de mélanger du texte brut
et des variables. Voici un exemple d'affichage avec une perte de précision :
Comme expliqué précédemment, les variables de type double contiennent plus d'informations que les
variables de type int. Ici, il va falloir écouter comme il faut… heu, pardon : lire comme il faut ! Nous
allons voir un truc super important en Java. Ne vous en déplaise, vous serez amenés à convertir des
variables.
int i = 123;
float j = (float)i;
D'un type int en double :
int i = 123;
double j = (double)i;
Et inversement :
double i = 1.23;
double j = 2.9999999;
int k = (int)i; //k vaut 1
k = (int)j; //k vaut 2
Ce type de conversion s'appelle une « conversion d'ajustement », ou cast de variable.
Vous l'avez vu : nous pouvons passer directement d'un type int à un type double. L'inverse, cependant,
ne se déroulera pas sans une perte de précision. En effet, comme vous avez pu le constater, lorsque
nouscastons un double en int, la valeur de ce double est tronquée, ce qui signifie que l'int en question
ne prendra que la valeur entière du double, quelle que soit celle des décimales.
Pour en revenir à notre problème de tout à l’heure, il est aussi possible de caster le résultat d'une opération
mathématique en la mettant entre « ( ) » et en la précédant du type de cast souhaité. Donc :
Sachez que l'affectation, le calcul, le cast, le test, l'incrémentation… toutes ces choses sont des opérations !
Et Java les fait dans un certain ordre, il y a des priorités.
Dans le cas qui nous intéresse, il y a trois opérations :
un calcul ;
Eh bien, Java exécute notre ligne dans cet ordre ! Il fait le calcul (ici 3/2), il caste le résultat en double,
puis il l'affecte dans notre variable resultat.
Vous vous demandez sûrement pourquoi vous n'avez pas 1.5… C'est simple : lors de la première opération
de Java, la JVM voit un cast à effectuer, mais sur un résultat de calcul. La JVM fait ce calcul (division de
deuxint qui, ici, nous donne 1), puis le cast (toujours 1), et affecte la valeur à la variable (encore et
toujours 1).
Donc, pour avoir un résultat correct, il faudrait caster chaque nombre avant de faire l'opération, comme ceci
:
int i = 12;
String j = new String();
j = j.valueOf(i);
j est donc une variable de type String contenant la chaîne de caractères 12. Sachez que ceci fonctionne
aussi avec les autres types numériques. Voyons maintenant comment faire marche arrière en partant de ce
que nous venons de faire.
int i = 12;
String j = new String();
j = j.valueOf(i);
int k = Integer.valueOf(j).intValue();
Maintenant, la variable k de type int contient le nombre « 12 ».
Comme vous le savez sûrement, le langage Java est en perpétuelle évolution. Les concepteurs ne cessent
d'ajouter de nouvelles fonctionnalités qui simplifient la vie des développeurs. Ainsi dans la version 7 de
Java, vous avez la possibilité de formater vos variables de types numériques avec un séparateur,
l'underscore (_), ce qui peut s'avérer très pratique pour de grands nombres qui peuvent être difficiles à lire.
Voici quelques exemples :
double d = 123_.159;
int entier = _123;
int entier2 = 123_;
Avant Java 7, il était possible de déclarer des expressions numériques en hexadécimal, en utilisant le
préfixe « 0x » :
On affecte une valeur dans une variable avec l'opérateur égal (« = »).
Après avoir affecté une valeur à une variable, l'instruction doit se terminer par un point-virgule
(« ; »).
Vos noms de variables ne doivent contenir ni caractères accentués ni espaces et doivent, dans la
mesure du possible, respecter la convention de nommage Java.
Lorsque vous effectuez des opérations sur des variables, prenez garde à leur type : vous
pourriez perdre en précision.
Vous pouvez caster un résultat en ajoutant un type devant celui-ci : (int), (double), etc.
Prenez garde aux priorités lorsque vous castez le résultat d'opérations, faute de quoi ce dernier
risque d'être incorrect.
Après la lecture de ce chapitre, vous pourrez saisir des informations et les stocker dans des variables afin de
pouvoir les utiliser a posteriori.
En fait, jusqu'à ce que nous voyions les interfaces graphiques, nous travaillerons en mode console. Donc,
afin de rendre nos programmes plus ludiques, il est de bon ton de pouvoir interagir avec ceux-ci. Par contre,
ceci peut engendrer des erreurs (on parlera d'exceptions, mais ce sera traité plus loin). Afin de ne pas
surcharger le chapitre, nous survolerons ce point sans voir les différents cas d'erreurs que cela peut
engendrer.
La classe Scanner
Je me doute qu'il vous tardait de pouvoir communiquer avec votre application… Le moment est enfin venu
! Mais je vous préviens, la méthode que je vais vous donner présente des failles. Je vous fais confiance pour
ne pas rentrer n'importe quoi n'importe quand !
Je vous ai dit que vos variables de type String sont en réalité des objets de type String. Pour que Java
puisse lire ce que vous tapez au clavier, vous allez devoir utiliser un objet de type Scanner. Cet objet peut
prendre différents paramètres, mais ici nous n'en utiliserons qu'un : celui qui correspond à l'entrée standard
en Java. Lorsque vous faites System.out.println();, je vous rappelle que vous appliquez la
méthodeprintln() sur la sortie standard ; ici, nous allons utiliser l'entrée standard System.in. Donc,
avant d'indiquer à Java qu'il faut lire ce que nous allons taper au clavier, nous devrons instancier un
objetScanner. Avant de vous expliquer ceci, créez une nouvelle classe et tapez cette ligne de code dans
votre méthode main :
import java.util.Scanner;
Voilà ce que nous avons fait. Je vous ai dit qu'il fallait indiquer à Java où se trouve la classe Scanner.
Pour faire ceci, nous devons importer la classe Scanner grâce à l'instruction import. La classe que nous
voulons se trouve dans le package java.util.
Un package est un ensemble de classes. En fait, c'est un ensemble de dossiers et de sous-dossiers contenant
une ou plusieurs classes, mais nous verrons ceci plus en détail lorsque nous ferons nos propres packages.
Les classes qui se trouvent dans les packages autres que java.lang (package automatiquement importé
par Java, on y trouve entre autres la classe System) sont à importer à la main dans vos classes Java pour
pouvoir vous en servir. La façon dont nous avons importé la classe java.util.Scanner dans Eclipse
est très commode. Vous pouvez aussi le faire manuellement :
Voici l'instruction pour permettre à Java de récupérer ce que vous avez saisi pour ensuite l'afficher :
Saisie
utilisateur dans la console
Si vous remplacez la ligne de code qui récupère une chaîne de caractères comme suit :
Exécutez et testez ce programme : vous verrez qu'il fonctionne à la perfection. Sauf… si vous saisissez
autre chose qu'un nombre entier !
Vous savez maintenant que pour lire un int, vous devez utiliser nextInt(). De façon générale, dites-
vous que pour récupérer un type de variable, il vous suffit
d'appelernext<Type de variable commençant par une majuscule> (rappelez-vous de la
convention de nommage Java).
Vous devez vous demander pourquoi charAt(0) et non charAt(1) : nous aborderons ce point lorsque
nous verrons les tableaux… Jusqu'à ce qu'on aborde les exceptions, je vous demanderai d'être rigoureux et
de faire attention à ce que vous attendez comme type de données afin d'utiliser la méthode correspondante.
Une précision s'impose, toutefois : la méthode nextLine() récupère le contenu de toute la ligne saisie et
replace la « tête de lecture » au début d'une autre ligne. Par contre, si vous avez invoqué une méthode
commenextInt(), nextDouble() et que vous invoquez directement après la méthode nextLine(),
celle-ci ne vous invitera pas à saisir une chaîne de caractères : elle videra la ligne commencée par les autres
instructions. En effet, celles-ci ne repositionnent pas la tête de lecture, l'instruction nextLine() le fait à
leur place. Pour faire simple, ceci :
import java.util.Scanner;
import java.util.Scanner;
Pour pouvoir récupérer ce vous allez taper dans la console, vous devrez initialiser
l'objet Scanneravec l'entrée standard, System.in.
Il y a une méthode de récupération de données pour chaque type (sauf les char)
: nextLine()pour les String, nextInt() pour les int ...
LES CONDITIONS
Nous abordons ici l'un des chapitres les plus importants : les conditions sont une autre notion
fondamentale de la programmation. En effet, ce qui va être développé ici s'applique à énormément de
langages de programmation, et pas seulement à Java.
Dans une classe, la lecture et l'exécution se font de façon séquentielle, c'est-à-dire ligne par ligne. Avec
lesconditions, nous allons pouvoir gérer différents cas de figure sans pour autant lire tout le code. Vous
vous rendrez vite compte que tous vos projets ne sont que des enchaînements et des imbrications de
conditions et de boucles (notion que l'on abordera au chapitre suivant).
Avant de pouvoir créer et évaluer des conditions, vous devez savoir que pour y parvenir, nous allons
utiliser ce qu'on appelle des « opérateurs logiques ». Ceux-ci sont surtout utilisés lors de conditions (si
[test] alors [faire ceci]) pour évaluer différents cas possibles. Voici les différents opérateurs à connaître :
« ? : » : l'opérateur ternaire. Pour celui-ci, vous comprendrez mieux avec un exemple qui sera
donné vers la fin de ce chapitre.
Comme je vous l'ai dit dans le chapitre précédent, les opérations en Java sont soumises à des priorités.
Tous ces opérateurs se plient à cette règle, de la même manière que les opérateurs arithmétiques…
Imaginons un programme qui demande à un utilisateur d'entrer un nombre entier relatif (qui peut être
soit négatif, soit nul, soit positif). Les structures conditionnelles vont nous permettre de gérer ces trois cas
de figure. La structure de ces conditions ressemble à ça :
if(//condition)
{
//Exécution des instructions si la condition est remplie
}
else
{
//Exécution des instructions si la condition n'est pas remplie
}
Cela peut se traduire par « si… sinon… ».
Le résultat de l'expression évaluée par l'instruction if sera un boolean, donc soit true, soit false. La
portion de code du bloc if ne sera exécutée que si la condition est remplie. Dans le cas contraire, c'est le
bloc de l'instruction else qui le sera. Mettons notre petit exemple en pratique :
int i = 10;
if (i < 0)
System.out.println("le nombre est négatif");
else
System.out.println("le nombre est positif");
Essayez ce petit code, et vous verrez comment il fonctionne. Dans ce cas, notre classe affiche « le nombre
est positif ». Expliquons un peu ce qui se passe.
Dans un premier temps, la condition du if est testée : elle dit « si i est strictement inférieur à 0
alors fais ça ».
Dans un second temps, vu que la condition précédente est fausse, le programme exécute le else.
Attends un peu ! Lorsque tu nous as présenté la structure des conditions, tu as mis des accolades et là, tu
n'en mets pas. Pourquoi ?
Bien observé. En fait, les accolades sont présentes dans la structure « normale » des conditions, mais
lorsque le code à l'intérieur de l'une d'entre elles n'est composé que d'une seule ligne, les accolades
deviennent facultatives.
Comme nous avons l'esprit perfectionniste, nous voulons que notre programme affiche « le nombre est
nul » lorsque i est égal à 0 ; nous allons donc ajouter une condition. Comment faire ? La condition
du if est remplie si le nombre est strictement négatif, ce qui n'est pas le cas ici puisque nous allons le
mettre à 0. Le code contenu dans la clause else est donc exécuté si le nombre est égal ou strictement
supérieur à 0. Il nous suffit d'ajouter une condition à l'intérieur de la clause else, comme ceci :
int i = 0;
if (i < 0)
{
System.out.println("Ce nombre est négatif !");
}
else
{
if(i == 0)
System.out.println("Ce nombre est nul !");
else
System.out.println("Ce nombre est positif !");
}
Maintenant que vous avez tout compris, je vais vous présenter une autre façon d'écrire ce code, avec le
même résultat : on ajoute juste un petit « sinon si… ».
int i = 0;
if (i < 0)
System.out.println("Ce nombre est négatif !");
else
System.out.println("Ce nombre est nul !");
Alors ? Explicite, n'est-ce pas ?
sinon si i est strictement positif alors le code de cette condition est exécuté.
sinon i est forcément nul alors le code de cette condition est exécuté.
Pour vous repérer dans vos futurs programmes, cela sera très utile. Imaginez deux secondes que vous avez
un programme de 700 lignes avec 150 conditions, et que tout est écrit le long du bord gauche. Il sera
difficile de distinguer les tests du code. Vous n'êtes pas obligés de le faire, mais je vous assure que vous y
viendrez.
Avant de passer à la suite, vous devez savoir qu'on ne peut pas tester l'égalité de chaînes de caractères ! Du
moins, pas comme je vous l'ai montré ci-dessus. Nous aborderons ce point plus tard.
Derrière ce nom barbare se cachent simplement plusieurs tests dans une instruction if (ou else if).
Nous allons maintenant utiliser les opérateurs logiques que nous avons vus au début en vérifiant si un
nombre donné appartient à un intervalle connu. Par exemple, on va vérifier si un entier est compris entre 50
et 100.
int i = 58;
if(i < 100 && i > 50)
System.out.println("Le nombre est bien dans l'intervalle.");
else
System.out.println("Le nombre n'est pas dans l'intervalle.");
Nous avons utilisé l'opérateur &&. La condition de notre if est devenue : « si i est inférieur à
100 ETsupérieur à 50 ».
Avec l'opérateur « && », la clause est remplie si et seulement si les conditions la constituant sont toutes
remplies ; si l'une des conditions n'est pas vérifiée, la clause sera considérée comme fausse.
Cet opérateur vous initie à la notion d'intersection d'ensembles. Ici, nous avons deux conditions qui
définissent un ensemble chacune :
L'opérateur « && » permet de faire l'intersection de ces ensembles. La condition regroupe donc les
nombres qui appartiennent à ces deux ensembles, c’est-à-dire les nombres de 51 à 99 inclus. Réfléchissez
bien à l'intervalle que vous voulez définir. Voyez ce code :
int i = 58;
if(i < 100 && i > 100)
System.out.println("Le nombre est bien dans l'intervalle.");
else
System.out.println("Le nombre n'est pas dans l'intervalle.");
Ici, la condition ne sera jamais remplie, car je ne connais aucun nombre qui soit à la fois plus petit et plus
grand que 100 ! Reprenez le code précédent et remplacez l'opérateur « && » par « || » (petit rappel, il s'agit
duOU). À l'exécution du programme et après plusieurs tests de valeur pour i, vous pourrez vous apercevoir
que tous les nombres remplissent cette condition, sauf 100.
La structure switch
Le switch est surtout utilisé lorsque nous voulons des conditions « à la carte ». Prenons l'exemple d'une
interrogation comportant deux questions : pour chacune d'elles, on peut obtenir uniquement 0 ou 10 points,
ce qui nous donne au final trois notes et donc trois appréciations possibles, comme ceci :
20/20 : bravo !
Dans ce genre de cas, on utilise un switch pour éviter des else if à répétition et pour alléger un peu le
code. Je vais vous montrer comment se construit une instruction switch ; puis nous allons l'utiliser tout de
suite après.
Syntaxe
switch (/*Variable*/)
{
case /*Argument*/:
/*Action*/;
break;
default:
/*Action*/;
}
Voici les opérations qu'effectue cette expression :
Notez bien la présence de l'instruction break;. Elle permet de sortir du switch si une languette
correspond. Pour mieux juger de l'utilité de cette instruction, enlevez tous les break; et compilez votre
programme. Vous verrez le résultat… Voici un exemple de switch que vous pouvez essayer :
switch (note)
{
case 0:
System.out.println("Ouch !");
break;
case 10:
System.out.println("Vous avez juste la moyenne.");
break;
case 20:
System.out.println("Parfait !");
break;
default:
System.out.println("Il faut davantage travailler.");
}
Je n'ai écrit qu'une ligne de code par instruction case, mais rien ne vous empêche d'en mettre plusieurs.
Si vous avez essayé ce programme en supprimant l'instruction break;, vous avez dû vous rendre compte
que le switch exécute le code contenu dans le case 10:, mais aussi dans tous ceux qui suivent !
L'instruction break; permet de sortir de l'opération en cours. Dans notre cas, on sort de
l'instructionswitch, mais nous verrons une autre utilité à break; dans le chapitre suivant.
Depuis la version 7 de Java, l'instruction switch accepte les objets de type String en paramètre. De ce
fait, cette instruction est donc valide :
switch(chaine) {
case "Bonjour":
System.out.println("Bonjour monsieur !");
break;
case "Bonsoir":
System.out.println("Bonsoir monsieur !");
break;
default:
System.out.println("Bonjoir ! :p");
}
La condition ternaire
Les conditions ternaires sont assez complexes et relativement peu utilisées. Je vous les présente ici à titre
indicatif. La particularité de ces conditions réside dans le fait que trois opérandes (c'est-à-dire des variables
ou des constantes) sont mis en jeu, mais aussi que ces conditions sont employées pour affecter des données
à une variable. Voici à quoi ressemble la structure de ce type de condition :
Nous cherchons à affecter une valeur à notre variable max, mais de l'autre côté de l'opérateur
d'affectation se trouve une condition ternaire…
Ce qui se trouve entre les parenthèses est évalué : x est-il plus petit que y ? Donc, deux cas de
figure se profilent à l'horizon :
o si la condition renvoie true (vrai), qu'elle est vérifiée, la valeur qui se trouve après
le ? sera affectée ;
Vous pouvez également faire des calculs (par exemple) avant d'affecter les valeurs :
o la structure ? :.
Si un bloc d'instructions contient plus d'une ligne, vous devez l'entourer d'accolades afin de bien en
délimiter le début et la fin.
Pour pouvoir mettre une condition en place, vous devez comparer des variables à l'aide
d'opérateurs logiques.
Vous pouvez mettre autant de comparaisons renvoyant un boolean que vous le souhaitez dans
une condition.
Pour la structure switch, pensez à mettre les instructions break; si vous ne souhaitez exécuter
qu'un seul bloc case.
LES BOUCLES
Une boucle s'exécute tant qu'une condition est remplie. Nous réutiliserons donc des notions du chapitre
précédent !
La boucle while
Pour décortiquer précisément ce qui se passe dans une boucle, nous allons voir comment elle se construit !
Une boucle commence par une déclaration : ici while. Cela veut dire, à peu de chose près, « tant que ».
Puis nous avons une condition : c'est elle qui permet à la boucle de s'arrêter. Une boucle n'est utile que
lorsque nous pouvons la contrôler, et donc lui faire répéter une instruction un certain nombre de fois. C'est
à ça que servent les conditions. Ensuite nous avons une ou plusieurs instructions : c'est ce que va répéter
notre boucle (il peut même y avoir des boucles dans une boucle !
Nous allons afficher « Bonjour, <un prénom> », prénom qu'il faudra taper au clavier ; puis nous
demanderons si l'on veut recommencer. Pour cela, il nous faut une variable qui va recevoir le prénom, donc
dont le type sera String, ainsi qu'une variable pour récupérer la réponse. Et là, plusieurs choix s'offrent à
nous : soit un caractère, soit une chaîne de caractères, soit un entier. Ici, nous prendrons une variable de
type char. C'est parti !
Détaillons un peu ce qu'il se passe. Dans un premier temps, nous avons déclaré et initialisé nos variables.
Ensuite, la boucle évalue la condition qui nous dit : tant que la variable reponse contient « O », on
exécute la boucle. Celle-ci contient bien le caractère « O », donc nous entrons dans la boucle. Puis
l'exécution des instructions suivant l'ordre dans lequel elles apparaissent dans la boucle a lieu. À la fin,
c'est-à-dire à l'accolade fermante de la boucle, le compilateur nous ramène au début de la boucle.
Cette boucle n'est exécutée que lorsque la condition est remplie : ici, nous avons initialisé la
variablereponse à « O » pour que la boucle s'exécute. Si nous ne l'avions pas fait, nous n'y serions jamais
entrés. Normal, puisque nous testons la condition avant d'entrer dans la boucle !
Voilà. C'est pas mal, mais il faudrait forcer l'utilisateur à ne taper que « O » ou « N ». Comment faire ?
C'est très simple : avec une boucle ! Il suffit de forcer l'utilisateur à entrer soit « N » soit « O » avec
un while ! Attention, il nous faudra réinitialiser la variable reponse à « ' ' » (caractère vide). Il faudra
donc répéter la phase « Voulez-vous réessayer ? » tant que la réponse donnée n'est pas « O » ou « N ».
String prenom;
char reponse = 'O';
Scanner sc = new Scanner(System.in);
while (reponse == 'O')
{
System.out.println("Donnez un prénom : ");
prenom = sc.nextLine();
System.out.println("Bonjour " +prenom+ ", comment vas-tu ?");
//Sans ça, nous n'entrerions pas dans la deuxième boucle
reponse = ' ';
int a = 1, b = 15;
while (a < b)
{
System.out.println("coucou " +a+ " fois !!");
}
Si vous lancez ce programme, vous allez voir une quantité astronomique de « coucou 1 fois !! ». Nous
aurions dû ajouter une instruction dans le bloc d'instructions de notre while pour changer la valeur de a à
chaque tour de boucle, comme ceci :
int a = 1, b = 15;
while (a < b)
{
System.out.println("coucou " +a+ " fois !!");
a++;
}
Ce qui nous donnerait comme résultat la figure suivante.
Correction de la boucle infinie
Une petite astuce : lorsque vous n'avez qu'une instruction dans votre boucle, vous pouvez enlever les
accolades, car elles deviennent superflues, tout comme pour les instructions if, else if ouelse.
Vous auriez aussi pu utiliser cette syntaxe :
int a = 1, b = 15;
while (a++ < b)
System.out.println("coucou " +a+ " fois !!");
Souvenez-vous de ce dont je vous parlais au chapitre précédent sur la priorité des opérateurs. Ici,
l'opérateur « < » a la priorité sur l'opérateur d'incrémentation « ++ ». Pour faire court, la
boucle while teste la condition et ensuite incrémente la variable a. Par contre, essayez ce code :
int a = 1, b = 15;
while (++a < b)
System.out.println("coucou " +a+ " fois !!");
Vous devez remarquer qu'il y a un tour de boucle en moins ! Eh bien avec cette syntaxe, l'opérateur
d'incrémentation est prioritaire sur l'opérateur d'inégalité (ou d'égalité), c'est-à-dire que la boucle
incrémente la variable a, et ce n'est qu'après l'avoir fait qu'elle teste la condition !
Puisque je viens de vous expliquer comment fonctionne une boucle while, je ne vais pas vraiment
m'attarder sur la boucle do… while. En effet, ces deux boucles ne sont pas cousines, mais plutôt sœurs.
Leur fonctionnement est identique à deux détails près.
do{
//Instructions
}while(a < b);
Première différence
La boucle do… while s'exécutera au moins une fois, contrairement à sa sœur. C'est-à-dire que la phase de
test de la condition se fait à la fin, car la condition se met après le while.
Deuxième différence
C'est une différence de syntaxe, qui se situe après la condition du while. Vous voyez la différence ? Oui ?
Non ? Il y a un « ;» après le while. C'est tout ! Ne l'oubliez cependant pas, sinon le programme ne
compilera pas.
Mis à part ces deux éléments, ces boucles fonctionnent exactement de la même manière. D'ailleurs,
refaisons notre programme précédent avec une boucle do… while.
do{
System.out.println("Donnez un prénom : ");
prenom = sc.nextLine();
System.out.println("Bonjour " +prenom+ ", comment vas-tu ?");
do{
System.out.println("Voulez-vous réessayer ? (O/N)");
reponse = sc.nextLine().charAt(0);
}while(reponse != 'O' && reponse != 'N');
System.out.println("Au revoir…");
Vous voyez donc que ce code ressemble beaucoup à celui utilisé avec la boucle while, mais il comporte
une petite subtilité : ici, plus besoin de réinitialiser la variable reponse, puisque de toute manière, la
boucle s'exécutera au moins une fois !
La boucle for
Cette boucle est un peu particulière puisqu'elle prend tous ses attributs dans sa condition et agit en
conséquence. Je m'explique : jusqu'ici, nous avions fait des boucles avec :
Eh bien on met tout ça dans la condition de la boucle for, et c'est tout. Il existe une autre syntaxe pour la
boucle for depuis le JDK 1.5. Nous la verrons lorsque nous aborderons les tableaux. Mais je sais bien
qu'un long discours ne vaut pas un exemple, alors voici une boucle for sous vos yeux ébahis :
Nous pouvons aussi inverser le sens de la boucle, c'est-à-dire qu'au lieu de partir de 0 pour aller à 10, nous
allons commencer à 10 pour atteindre 0 :
Tout comme les conditions, si une boucle contient plus d'une ligne de code à exécuter, vous
devez l'entourer d'accolades afin de bien en délimiter le début et la fin.
TP : CONVERSION CELSIUS -
FAHRENHEIT
Voilà un petit TP qui va vous permettre de mettre en œuvre toutes les notions que vous avez vues jusqu'ici :
les variables ;
les conditions ;
les boucles ;
Accrochez-vous, car je vais vous demander de penser à des tonnes de choses, et vous serez tout seuls.
Lâchés dans la nature… Mais non je plaisante, je vais vous guider un peu.
Élaboration
on n'autorise que les modes de conversion définis dans le programme (un simple contrôle sur la
saisie fera l'affaire) ;
enfin, on demande à la fin à l'utilisateur s'il veut faire une nouvelle conversion, ce qui signifie
que l'on doit pouvoir revenir au début du programme !
Avant de vous lancer dans la programmation à proprement parler, je vous conseille fortement de réfléchir à
votre code… sur papier. Réfléchissez à ce qu'il vous faut comme nombre de variables, les types de
variables, comment va se dérouler le programme, les conditions et les boucles utilisées.
À toutes fins utiles, voici la formule de conversion pour passer des degrés Celsius en degrés Fahrenheit :
F=95×C+32
Pour l'opération inverse, c'est comme ceci :
C=(F−32)×59
La figure suivante est un aperçu de ce que je vous demande.
Rendu du TP
Je vais également vous donner une fonction toute faite qui vous permettra éventuellement d'arrondir vos
résultats. Je vous expliquerai le fonctionnement des fonctions dans deux chapitres. Vous pouvez très bien
ne pas vous en servir. Pour ceux qui souhaitent tout de même l'utiliser, la voici :
public static double arrondi(double A, int B) {
return (double) ( (int) (A * Math.pow(10, B) + .5)) / Math.pow(10, B);
}
Elle est à placer entre les deux accolades fermantes de votre classe, comme à la figure suivante.
Emplacement de la fonction
Voici comment utiliser cette fonction : imaginez que vous avez la variable faren à arrondir, et que le
résultat obtenu est enregistré dans une variable arrondFaren ; vous procéderez comme suit :
Correction
STOP ! C'est fini ! Il est temps de passer à la correction de ce premier TP. Ça va ? Pas trop mal à la tête ?
Je me doute qu'il a dû y avoir quelques tubes d'aspirine vidés. Mais vous allez voir qu'en définitive, ce TP
n'était pas si compliqué que ça.
Surtout, n'allez pas croire que ma correction est parole d'évangile. Il y avait différentes manières d'obtenir
le même résultat. Voici tout de même une des solutions possibles.
import java.util.Scanner;
class Sdz1 {
public static void main(String[] args) {
//Notre objet Scanner
Scanner sc = new Scanner(System.in);
}while(reponse == 'O');
//Fin de programme
}
Ensuite, vous voyez deux do{ consécutifs correspondant à deux conditions à vérifier :
o la volonté de l'utilisateur d'effectuer une nouvelle conversion ;
Fin du programme !
Ce programme n'est pas parfait, loin de là. La vocation de celui-ci était de vous faire utiliser ce que vous
avez appris, et je pense qu'il remplit bien sa fonction. J'espère que vous avez apprécié ce TP. Je sais qu'il
n'était pas facile, mais avouez-le : il vous a bien fait utiliser tout ce que vous avez vu jusqu'ici !
LES TABLEAUX
Comme tout langage de programmation qui se respecte, Java travaille avec des tableaux. Vous verrez que
ceux-ci s'avèrent bien pratiques.
Vous vous doutez (je suppose) que les tableaux dont nous parlons n'ont pas grand-chose à voir avec ceux
que vous connaissez ! En programmation, un tableau n'est rien d'autre qu'une variable un peu particulière.
Nous allons en effet pouvoir lui affecter plusieurs valeurs ordonnées séquentiellement que nous pourrons
appeler au moyen d'un indice (ou d'un compteur, si vous préférez). Il nous suffira d'introduire
l'emplacement du contenu désiré dans notre variable tableau pour la sortir, travailler avec, l'afficher…
Je viens de vous expliquer grosso modo ce qu'est un tableau en programmation. Si maintenant, je vous
disais qu'il y a autant de types de tableaux que de types de variables ? Je crois voir quelques gouttes de
sueur perler sur vos fronts…
Pas de panique ! C'est très logique : comme nous l'avons vu auparavant, une variable d'un type donné ne
peut contenir que des éléments de ce type : une variable de type int ne peut pas recevoir une chaîne de
caractères. Il en va de même pour les tableaux. Voyons tout de suite comment ils se déclarent :
<type du tableau> <nom du tableau> [] = { <contenu du tableau>};
La déclaration ressemble beaucoup à celle d'une variable quelconque, si ce n'est la présence de crochets « [
] » après le nom de notre tableau et d'accolades « { } » encadrant l'initialisation de celui-ci. Dans la
pratique, ça nous donnerait quelque chose comme ceci :
Ici, les choses se compliquent un peu, car un tableau multidimensionnel n'est rien d'autre qu'un tableau
contenant au minimum deux tableaux… Je me doute bien que cette notion doit en effrayer plus d'un, mais
en réalité, elle n'est pas si difficile que ça à appréhender. Comme tout ce que je vous apprends en général !
Je ne vais pas vous faire de grand laïus sur ce type de tableau, puisque je pense sincèrement qu'un exemple
vous en fera beaucoup mieux comprendre le concept. Imaginez un tableau avec deux lignes : la première
contiendra les premiers nombres pairs, et le deuxième contiendra les premiers nombres impairs.
Avant d'attaquer, je dois vous dire quelque chose de primordial : un tableau débute toujours à l'indice 0 ! Je
m'explique : prenons l'exemple du tableau de caractères contenant les lettres de l'alphabet dans l'ordre qui a
été donné plus haut. Si vous voulez afficher la lettre « a » à l'écran, vous devrez taper cette ligne de code :
System.out.println(tableauCaractere[0]);
Cela implique qu'un tableau contenant 4 éléments aura comme indices possibles 0, 1, 2 ou 3. Le 0
correspond au premier élément, le 1 correspond au 2e élément, le 2 correspond au 3e élément et le 3
correspond au 4e élément.
Une très grande partie des erreurs sur les tableaux sont dues à un mauvais indice dans celui-ci. Donc prenez
garde !
Ce que je vous propose, c'est tout simplement d'afficher un des tableaux présentés ci-dessus dans son
intégralité. Sachez qu'il existe une instruction qui retourne la taille d'un tableau : grâce à elle, nous pourrons
arrêter notre boucle (car oui, nous allons utiliser une boucle). Il s'agit de
l'instruction<mon tableau>.length. Notre boucle for pourrait donc ressembler à ceci :
do {//Boucle principale
do {//On répète cette boucle tant que l'utilisateur n'a pas rentré une lettre figurant
dans le tableau
i = 0;
System.out.println("Rentrez une lettre en minuscule, SVP ");
carac = sc.nextLine().charAt(0);
//Boucle de recherche dans le tableau
while(i < tableauCaractere.length && carac != tableauCaractere[i])
i++;
//Si i < 7 c'est que la boucle n'a pas dépassé le nombre de cases du tableau
if (i < tableauCaractere.length)
System.out.println(" La lettre " +carac+ " se trouve bien dans le tableau !");
else //Sinon
System.out.println(" La lettre " +carac+ " ne se trouve pas dans le tableau !");
Résultat de la recherche
Explications sur la recherche
La première correspond au compteur : tant que celui-ci est inférieur ou égal au nombre d'éléments du
tableau, on l'incrémente pour regarder la valeur suivante. Nous passons ainsi en revue tout ce qui se trouve
dans notre tableau. Si nous n'avions mis que cette condition, la boucle n'aurait fait que parcourir le tableau,
sans voir si le caractère saisi correspond bien à un caractère de notre tableau, d'où la deuxième condition.
La deuxième correspond à la comparaison entre le caractère saisi et la recherche dans le tableau. Grâce à
elle, si le caractère saisi se trouve dans le tableau, la boucle prend fin, et donc i a une valeur inférieure à 7.
À ce stade, notre recherche est terminée. Après cela, les conditions coulent de source ! Si nous avons trouvé
une correspondance entre le caractère saisi et notre tableau, i prendra une valeur inférieure à 7 (vu qu'il y a
7 éléments dans notre tableau, l'indice maximum étant 7-1, soit 6). Dans ce cas, nous affichons un message
confirmant la présence de l’élément recherché. Dans le cas contraire, c'est l'instruction du else qui
s'exécutera.
Vous avez dû remarquer la présence d'un i = 0; dans une boucle. Ceci est primordial, sinon, lorsque
vous reviendrez au début de celle-ci, i ne vaudra plus 0, mais la dernière valeur à laquelle il aura été
affecté après les différentes incrémentations. Si vous faites une nouvelle recherche, vous commencerez par
l'indice contenu dans i ; ce que vous ne voulez pas, puisque le but est de parcourir l'intégralité du tableau,
donc depuis l’indice 0.
En travaillant avec les tableaux, vous serez confrontés, un jour ou l'autre, au message suivant :
java.lang.ArrayIndexOutOfBoundsException
Ceci signifie qu'une erreur a été rencontrée, car vous avez essayé de lire (ou d'écrire dans) une case qui n'a
pas été définie dans votre tableau ! Voici un exemple (nous verrons les exceptions lorsque nous aborderons
la programmation orientée objet) :
while (i < 2)
{
j = 0;
while(j < 5)
{
System.out.print(premiersNombres[i][j]);
j++;
}
System.out.println("");
i++;
}
Le résultat se trouve à la figure suivante.
Affichage du tableau
Détaillons un peu ce code :
On entre ensuite dans la première boucle (qui s'exécutera deux fois, donc i vaut 0 la première
fois, et vaudra 1 pendant la deuxième), et on initialise j à 0.
On entre à nouveau dans la deuxième boucle, où le processus est le même que précédemment
(mais là, i vaut 1).
Concernant les tableaux à deux dimensions, que va retourner l'instruction de la première boucle for ? Un
tableau ! Nous devrons donc faire une deuxième boucle afin de parcourir ce dernier !
Voici un code qui permet d'afficher un tableau à deux dimensions de façon conventionnelle et selon la
version du JDK 1.5 (cette syntaxe ne fonctionnera pas sur les versions antérieures au JDK 1.5) :
String tab[][]={{"toto", "titi", "tutu", "tete", "tata"}, {"1", "2", "3", "4"}};
int i = 0, j = 0;
Un tableau est une variable contenant plusieurs données d'un même type.
Pour déclarer un tableau, il faut ajouter des crochets [ ] à la variable ou à son type de
déclaration.
Vous pouvez ajouter autant de dimensions à votre tableau que vous le souhaitez, ceci en
cumulant des crochets à la déclaration.
Vous pouvez utiliser la syntaxe du JDK 1.5 de la boucle for pour parcourir vos tableaux
:for(String str : monTableauDeString).
#
J'ai terminé ce chapitre et je passe au suivant
Maintenant que vous commencez à écrire de vrais programmes, vous vous rendez sûrement compte qu'il y
a certaines choses que vous effectuez souvent. Plutôt que de recopier sans arrêt les mêmes morceaux de
code, vous pouvez écrire une méthode…
Ce chapitre aura pour but de vous faire découvrir la notion de méthode (on l'appelle « fonction » dans
d'autres langages). Vous en avez peut-être déjà utilisé une lors du premier TP, vous vous en souvenez ?
Vous avez pu voir qu'au lieu de retaper le code permettant d'arrondir un nombre décimal, vous pouviez
l'inclure dans une méthode et appeler celle-ci.
Le principal avantage des méthodes est de pouvoir factoriser le code : grâce à elles, vous n'avez qu'un seul
endroit où effectuer des modifications lorsqu'elles sont nécessaires. J'espère que vous comprenez mieux
l'intérêt de tout cela, car c'est ce que nous allons aborder ici. Cependant, ce chapitre ne serait pas drôle si
nous ne nous amusions pas à créer une ou deux méthodes pour le plaisir. Et là, vous aurez beaucoup de
choses à retenir !
Vous l'aurez compris, il existe énormément de méthodes dans le langage Java, présentes dans des objets
comme String : vous devrez les utiliser tout au long de cet ouvrage (et serez même amenés à en modifier
le comportement). À ce point du livre, vous pouvez catégoriser les méthodes en deux « familles » : les
natives et les vôtres.
String chaine = new String("COUCOU TOUT LE MONDE !"), chaine2 = new String();
chaine2 = chaine.toLowerCase(); //Donne "coucou tout le monde !"
la méthode toUpperCase() est simple, puisqu'il s'agit de l'opposé de la précédente. Elle transforme donc
une chaîne de caractères en capitales, et s'utilise comme suit :
if (str1.equals(str2))
System.out.println("Les deux chaînes sont identiques !");
else
System.out.println("Les deux chaînes sont différentes !");
Vous pouvez aussi demander la vérification de l'inégalité grâce à l'opérateur de négation. Vous vous en
souvenez ? Il s'agit de « ! ». Cela nous donne :
if (!str1.equals(str2))
System.out.println("Les deux chaînes sont différentes !");
else
System.out.println("Les deux chaînes sont identiques !");
Ce genre de condition fonctionne de la même façon pour les boucles. Dans l'absolu, cette fonction retourne
un booléen, c'est pour cette raison que nous pouvons y recourir dans les tests de condition.
Le résultat de la méthode charAt() sera un caractère : il s'agit d'une méthode d'extraction de caractère.
Elle ne peut s'opérer que sur des String ! Par ailleurs, elle présente la même particularité que les tableaux,
c'est-à-dire que, pour cette méthode, le premier caractère sera le numéro 0. Cette méthode prend un entier
comme argument.
double X = 0.0;
X = Math.random();
//Retourne un nombre aléatoire
//compris entre 0 et 1, comme 0.0001385746329371058
Je ne vais pas vous faire un récapitulatif de toutes les méthodes présentes dans Java, sinon j'y serai encore
dans mille ans… Toutes ces méthodes sont très utiles, croyez-moi. Cependant, les plus utiles sont encore
celles que nous écrivons nous-mêmes ! C'est tellement mieux quand cela vient de nous.
Tout d'abord, il y a le mot clé public. C'est ce qui définit la portée de la méthode, nous y
reviendrons lorsque nous programmerons des objets.
Juste après, nous voyons double. Il s'agit du type de retour de la méthode. Pour faire simple,
ici, notre méthode va renvoyer un double !
Vient ensuite le nom de la méthode. C'est avec ce nom que nous l'appellerons.
Puis arrivent les arguments de la méthode. Ce sont en fait les paramètres dont la méthode a
besoin pour travailler. Ici, nous demandons d'arrondir le doubleA avec B chiffres derrière la
virgule.
Finalement, vous pouvez voir une instruction return à l'intérieur de la méthode. C'est elle qui
effectue le renvoi de la valeur, ici un double.
Nous verrons dans ce chapitre les différents types de renvoi ainsi que les paramètres que peut accepter une
méthode.
o les méthodes qui ne renvoient rien. Les méthodes de ce type n'ont pas
d'instruction return, et elles sont de type void ;
o les méthodes qui retournent des types primitifs (double, int etc.). Elles sont de
typedouble, int, char etc. Celles-ci possèdent une instruction return ;
o les méthodes qui retournent des objets. Par exemple, une méthode qui retourne un
objet de type String. Celles-ci aussi comportent une instruction return.
Jusque-là, nous n'avons écrit que des programmes comportant une seule classe, ne disposant elle-même que
d'une méthode : la méthode main. Le moment est donc venu de créer vos propres méthodes. Que vous ayez
utilisé ou non la méthode arrondi dans votre TP, vous avez dû voir que celle-ci se place à l'extérieur de la
méthode main, mais tout de même dans votre classe !
Pour rappel, jetez un œil à la capture d'écran du premier TP, à la figure suivante.
ne rien renvoyer.
Avec ce que nous avons défini, nous savons que notre méthode sera de type void et qu'elle prendra un
tableau en paramètre. Voici un exemple de code complet :
Voici un exemple ayant le même effet que la méthode parcourirTableau, à la différence que celle-ci
retourne une valeur : ici, ce sera une chaîne de caractères.
return retour;
}
}
Vous voyez que la deuxième méthode retourne une chaîne de caractères, que nous devons afficher à l'aide
de l'instruction System.out.println(). Nous affichons la valeur renvoyée par la
méthode toString(). La méthode parcourirTableau, quant à elle, écrit au fur et à mesure le
contenu du tableau dans la console. Notez que j'ai ajouté une ligne d'écriture dans la console au sein de la
méthode toString(), afin de vous montrer où elle était appelée.
Il nous reste un point important à aborder. Imaginez un instant que vous ayez plusieurs types d'éléments à
parcourir : des tableaux à une dimension, d'autres à deux dimensions, et même des objets comme
desArrayList (nous les verrons plus tard, ne vous inquiétez pas). Sans aller aussi loin, vous n'allez pas
donner un nom différent à la méthode parcourirTableau pour chaque type primitif !
Vous avez dû remarquer que la méthode que nous avons créée ne prend qu'un tableau de String en
paramètre. Pas un tableau d'int ou de long, par exemple. Si seulement nous pouvions utiliser la même
méthode pour différents types de tableaux… C'est là qu'entre en jeu ce qu'on appelle la surcharge.
La surcharge de méthode
La surcharge de méthode consiste à garder le nom d'une méthode (donc un type de traitement à faire : pour
nous, lister un tableau) et à changer la liste ou le type de ses paramètres. Dans le cas qui nous intéresse,
nous voulons que notre méthode parcourirTableau puisse parcourir n'importe quel type de tableau.
Nous allons donc surcharger notre méthode afin qu'elle puisse aussi travailler avec des int, comme le
montre cet exemple :
Vous pouvez faire de même avec les tableaux à deux dimensions. Voici à quoi pourrait ressembler le code
d'une telle méthode (je ne rappelle pas le code des deux méthodes ci-dessus) :
En résumé
Une méthode est un morceau de code réutilisable qui effectue une action bien définie.
Les méthodes se définissent dans une classe.
Les méthodes ne peuvent pas être imbriquées. Elles sont déclarées les unes après les autres.
Une méthode peut être surchargée en modifiant le type de ses paramètres, leur nombre, ou les
deux.
Pour Java, le fait de surcharger une méthode lui indique qu'il s'agit de deux, trois ou X
méthodes différentes, car les paramètres d'appel sont différents. Par conséquent, Java ne se
trompe jamais d'appel de méthode, puisqu'il se base sur les paramètres passés à cette dernière.
J'ose espérer que vous avez apprécié ce tuto sur les bases du langage Java ! En tout cas, je me suis bien
amusé en le faisant.
Maintenant, nous allons rentrer dans les méandres de la programmation orientée objet !
Alors ?... Toujours prêts ?
Dans la première partie de cet ouvrage sur la programmation en Java, nous avons travaillé avec une seule
classe. Vous allez apprendre qu'en faisant de la programmation orientée objet, nous travaillerons en fait
avec de nombreuses classes. Rappelez-vous la première partie : vous avez déjà utilisé des objets… Oui !
Lorsque vous faisiez ceci : String str = new String("tiens… un objet String");.
Ici str est un objet String. Vous avez utilisé un objet de la classe String : on dit que vous avez
crééune instance de la classeString(). Le moment est venu pour vous de créer vos propres classes.
Structure de base
Une classe peut être comparée à un moule qui, lorsque nous le remplissons, nous donne un objet ayant la
forme du moule ainsi que toutes ses caractéristiques. Comme quand vous étiez enfants, lorsque vous vous
amusiez avec de la pâte à modeler.
Si vous avez bien suivi la première partie de ce cours, vous devriez savoir que notre classe contenant la
méthode main ressemble à ceci :
class ClasseMain{
public static void main(String[] args){
//Vos données, variables, différents traitements…
}//Fin de la méthode main
Il ne peut y avoir qu'une seule méthode main active par projet ! Souvenez-vous que celle-ci est le point de
départ de votre programme. Pour être tout à fait précis, plusieurs méthodes main peuvent cohabiter dans
votre projet, mais une seule sera considérée comme le point de départ de votre programme !
Au final, vous devriez avoir le rendu de la figure suivante.
Classe Ville
Ici, notre classe Ville est précédée du mot clé public. Vous devez savoir que lorsque nous créons une
classe comme nous l'avons fait, Eclipse nous facilite la tâche en ajoutant automatiquement ce mot clé, qui
correspond à la portée de la classe. Retenez pour l'instant que public class UneClasse{} et class
UneClasse{} sont presque équivalents !
En programmation, la portée détermine qui peut faire appel à une classe, une méthode ou une variable.
Vous avez déjà rencontré la portée public : cela signifie que tout le monde peut faire appel à l'élément.
Ici dans le cas qui nous intéresse il s'agit d'une méthode. Une méthode marquée comme public peut donc
être appelée depuis n'importe quel endroit du programme.
Nous allons ici utiliser une autre portée : private. Elle signifie que notre méthode ne pourra être appelée
que depuis l'intérieur de la classe dans laquelle elle se trouve ! Les méthodes
déclarées privatecorrespondent souvent à des mécanismes internes à une classe que les développeurs
souhaitent « cacher » ou simplement ne pas rendre accessibles de l'extérieur de la classe…
Il en va de même pour les variables. Nous allons voir que nous pouvons protéger des variables grâce au
mot clé private. Le principe sera le même que pour les méthodes. Ces variables ne seront alors
accessibles que dans la classe où elles seront nées…
Bon. Toutes les conditions sont réunies pour commencer activement la programmation orientée objet ! Et si
nous allions créer notre première ville ?
Les constructeurs
Vu que notre objectif dans ce chapitre est de construire un objet Ville, il va falloir définir les données
qu'on va lui attribuer. Nous dirons qu'un objet Ville possède :
Nous allons faire ceci en mettant des variables d'instance (de simples variables identiques à celles que vous
manipulez habituellement) dans notre classe. Celle-ci va contenir une variable dont le rôle sera de stocker
le nom, une autre stockera le nombre d'habitants et la dernière se chargera du pays ! Voici à quoi ressemble
notre classe Ville à présent :
1. Les variables d'instance : ce sont elles qui définiront les caractéristiques de notre objet.
2. Les variables de classe : celles-ci sont communes à toutes les instances de votre classe.
3. Les variables locales : ce sont des variables que nous utiliserons pour travailler dans notre
objet.
Dans l'immédiat, nous allons travailler avec des variables d'instance afin de créer des objets différents. Il ne
nous reste plus qu'à créer notre premier objet, pour ce faire, nous allons devoir utiliser ce qu'on appelle
desconstructeurs.
Un constructeur est une méthode d'instance qui va se charger de créer un objet et, le cas échéant,
d'initialiser ses variables de classe ! Cette méthode a pour rôle de signaler à la JVM (Java Virtual Machine)
qu'il faut réserver de la mémoire pour notre futur objet et donc, par extension, d'en réserver pour toutes ses
variables.
Notre premier constructeur sera ce qu'on appelle communément un constructeur par défaut, c'est-à-dire
qu'il ne prendra aucun paramètre, mais permettra tout de même d'instancier un objet, et vu que nous
sommes perfectionnistes, nous allons y initialiser nos variables d'instance. Voici votre premier constructeur
:
Son corollaire est qu'un objet peut avoir plusieurs constructeurs. Il s'agit de la même méthode, mais
surchargée ! Dans notre premier constructeur, nous n'avons passé aucun paramètre, mais nous allons
bientôt en mettre.
Vous pouvez d'ores et déjà créer une instance de Ville. Cependant, commencez par vous rappeler qu'une
instance d'objet se fait grâce au mot clé new, comme lorsque vous créez une variable de type String.
Maintenant, vu que nous allons créer des objets Ville, nous allons procéder comme avec les String.
Vérifions que l'instanciation s’effectue comme il faut. Allons dans notre classe contenant la
méthode mainet instancions un objet Ville. Je suppose que vous avez deviné que le type de notre objet
sera Ville !
Voici le constructeur de notre objet Ville, celui qui permet d'avoir des objets avec des paramètres
différents :
}
Modification des données de notre objet
Vous constatez que nous pouvons accéder aux variables d'instance en utilisant le « . », comme lorsque
vous appelez la méthode subString() de l'objet String. C'est très risqué, et la plupart des
programmeurs Java vous le diront. Dans la majorité des cas, nous allons contrôler les modifications des
variables de classe, de manière à ce qu'un code extérieur ne fasse pas n'importe quoi avec nos objets ! En
plus de ça, imaginez que vous souhaitiez faire quelque chose à chaque fois qu'une valeur change ; si vous
ne protégez pas vos données, ce sera impossible à réaliser… C'est pour cela que nous protégeons nos
variables d'instance en les déclarant private, comme ceci :
//…
}
Désormais, ces attributs ne sont plus accessibles en dehors de la classe où ils sont déclarés ! Nous allons
maintenant voir comment accéder tout de même à nos données.
Accesseurs et mutateurs
Un accesseur est une méthode qui va nous permettre d'accéder aux variables de nos objets en lecture, et un
mutateur nous permettra d'en faire de même en écriture ! Grâce aux accesseurs, vous pourrez afficher les
variables de vos objets, et grâce aux mutateurs, vous pourrez les modifier :
Je vous ai fait faire la différence entre accesseurs et mutateurs, mais généralement, lorsqu'on parle
d'accesseurs, ce terme inclut également les mutateurs. Autre chose : il s'agit ici d'une question de
convention de nommage. Les accesseurs commencent par get et les mutateurs par set, comme vous
pouvez le voir ici. On parle d'ailleurs parfois de Getters et de Setters.
À présent, essayez ce code dans votre méthode main :
Ville v = new Ville();
Ville v1 = new Ville("Marseille", 123456, "France");
Ville v2 = new Ville("Rio", 321654, "Brésil");
/*
Nous allons interchanger les Villes v1 et v2
tout ça par l'intermédiaire d'un autre objet Ville.
*/
Ville temp = new Ville();
temp = v1;
v1 = v2;
v2 = temp;
/*
Nous allons maintenant interchanger leurs noms
cette fois par le biais de leurs mutateurs.
*/
v1.setNom("Hong Kong");
v2.setNom("Djibouti");
les accesseurs -> méthodes servant à accéder aux données des objets ;
Avec nos objets Ville, notre choix est un peu limité par le nombre de méthodes possibles, mais nous
pouvons tout de même en faire une ou deux pour l'exemple :
faire un système de catégories de villes par rapport à leur nombre d'habitants ( <1000 -> A, <10
000 -> B…). Ceci est déterminé à la construction ou à la redéfinition du nombre d'habitants :
ajoutons donc une variable d'instance de type char à notre classe et appelons-la categorie.
Pensez à ajouter le traitement aux bons endroits ;
une méthode pour comparer deux objets par rapport à leur nombre d'habitants.
Nous voulons que la classe Ville gère la façon de déterminer la catégorie elle-même, et non que cette
action puisse être opérée de l'extérieur. La méthode qui fera ceci sera donc déclaréeprivate.
Par contre, un problème va se poser ! Vous savez déjà qu'en Java, on appelle les méthodes d'un objet
comme ceci : monString.subString(0,4);. Cependant, vu qu'il va falloir qu'on travaille depuis
l'intérieur de notre objet, vous allez encore avoir un mot clé à retenir : cette fois, il s'agit du...
Je vous propose de voir le mot-clé this en action dans le code de notre classe Ville en entier, c'est-à-dire
comportant les méthodes dont on vient de parler :
public Ville(){
System.out.println("Création d'une ville !");
nomVille = "Inconnu";
nomPays = "Inconnu";
nbreHabitants = 0;
this.setCategorie();
}
int bornesSuperieures[] = {0, 1000, 10000, 100000, 500000, 1000000, 5000000, 10000000};
char categories[] = {'?', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'};
int i = 0;
while (i < bornesSuperieures.length && this.nbreHabitants > bornesSuperieures[i])
i++;
this.categorie = categories[i];
}
else
str = this.nomVille+" est une ville plus peuplée que "+v1.getNom();
return str;
}
}
Pour simplifier, this fait référence à l'objet courant ! Bien que la traduction anglaise exacte soit « ceci », il
faut comprendre « moi ». À l'intérieur d'un objet, ce mot clé permet de désigner une de ses variables ou une
de ses méthodes.
Pour expliciter le fonctionnement du mot clé this, prenons l'exemple de la
méthodecomparer(Ville V1). La méthode va s'utiliser comme suit :
V.comparer(V2);
Dans cette méthode, nous voulons comparer le nombre d'habitants de chacun des deux objets Ville. Pour
accéder à la variable nbreHabitants de l'objet V2, il suffit d'utiliser la
syntaxeV2.getNombreHabitants() ; nous ferons donc référence à la propriété nbreHabitants de
l'objetV2.
Mais l'objet V, lui, est l'objet appelant de cette méthode. Pour se servir de ses propres variables, on utilise
alors this.nbreHabitants, ce qui a pour effet de faire appel à la variable nbreHabitants de l'objet
exécutant la méthode comparer(Ville V).
Explicitons un peu les trois méthodes qui ont été décrites précédemment.
La méthode categorie()
Elle ne prend aucun paramètre, et ne renvoie rien : elle se contente de mettre la variable de
classecategorie à jour. Elle détermine dans quelle tranche se trouve la ville grâce au nombre d'habitants
de l'objet appelant, obtenu au moyen du mot clé this. Selon le nombre d'habitants, le caractère renvoyé
changera. Nous l'appelons lorsque nous construisons un objet Ville (que ce soit avec ou sans paramètre),
mais aussi lorsque nous redéfinissons le nombre d'habitants : de cette manière, la catégorie est
automatiquement mise à jour, sans qu'on ait besoin de faire appel à la méthode.
La méthode decrisToi()
Celle-ci nous renvoie un objet de type String. Elle fait référence aux variables qui composent l'objet
appelant la méthode, toujours grâce à this, et nous renvoie donc une chaîne de caractères qui nous décrit
l'objet en énumérant ses composants.
Elle prend une ville en paramètre, pour pouvoir comparer les variables nbreHabitants de l'objet
appelant la méthode et de celui passé en paramètre pour nous dire quelle ville est la plus peuplée ! Et si
nous faisions un petit test ?
System.out.println("\n\n"+v1.decrisToi());
System.out.println(v.decrisToi());
System.out.println(v2.decrisToi()+"\n\n");
System.out.println(v1.comparer(v2));
Ce qui devrait donner le résultat de la figure suivante.
Comme je vous le disais au début de ce chapitre, il y a plusieurs types de variables dans une classe. Nous
avons vu les variables d'instance qui forment la carte d'identité d'un objet ; maintenant, voici les variables
de classe.
Celles-ci peuvent s'avérer très utiles. Dans notre exemple, nous allons compter le nombre d'instances de
notre classe Ville, mais nous pourrions les utiliser pour bien d'autres choses (un taux de TVA dans une
classe qui calcule le prix TTC, par exemple).
La particularité de ce type de variable, c'est qu'elles seront communes à toutes les instances de la classe !
Créons sans plus attendre notre compteur d'instances. Il s'agira d'une variable de type int que nous
appellerons nbreInstance, et qui sera public ; nous mettrons aussi son homologue en private en
place et l'appellerons nbreInstanceBis (il sera nécessaire de mettre un accesseur en place pour cette
variable). Afin qu'une variable soit une variable de classe, elle doit être précédée du mot clé static. Cela
donnerait dans notre classe Ville :
public Ville(){
//On incrémente nos variables à chaque appel aux constructeurs
nbreInstances++;
nbreInstancesBis++;
//Le reste ne change pas.
}
À présent, si vous testez le code suivant, vous allez constater l'utilité des variables de classe :
Le principe d'encapsulation
Voilà, vous venez de construire votre premier objet « maison ». Cependant, sans le savoir, vous avez fait
plus que ça : vous avez créé un objet dont les variables sont protégées de l'extérieur. En effet, depuis
l'extérieur de la classe, elles ne sont accessibles que via les accesseurs et mutateurs que nous avons défini.
C'est le principe d'encapsulation !
En fait, lorsqu'on procède de la sorte, on s'assure que le fonctionnement interne à l'objet est intègre, car
toute modification d'une donnée de l'objet est maîtrisée. Nous avons développé des méthodes qui s'assurent
qu'on ne modifie pas n'importe comment les variables.
Prenons l'exemple de la variable nbreHabitants. L'encapsuler nous permet, lors de son affectation, de
déduire automatiquement la catégorie de l'objet Ville, chose qui n'est pas facilement faisable sans
encapsulation. Par extension, si vous avez besoin d'effectuer des opérations déterminées lors de l'affectation
du nom d'une ville par exemple, vous n'aurez pas à passer en revue tous les codes source utilisant
l'objetVille : vous n'aurez qu'à modifier l'objet (ou la méthode) en question, et le tour sera joué.
Si vous vous demandez l'utilité de tout cela, dites-vous que vous ne serez peut-être pas seuls à développer
vos logiciels, et que les personnes utilisant vos classes n'ont pas à savoir ce qu'il s'y passe : seules les
fonctionnalités qui leurs sont offertes comptent. Vous le verrez en continuant la lecture de cet ouvrage, Java
est souple parce qu'il offre beaucoup de fonctionnalités pouvant être retravaillées selon les besoins, mais
gardez à l'esprit que certaines choses vous seront volontairement inaccessibles, pour éviter que vous ne «
cassiez » quelque chose.
Une classe permet de définir des objets. Ceux-ci ont des attributs (variables d'instance) et des
méthodes (méthodes d’instance + accesseurs).
Il est recommandé de déclarer ses variables d'instance private, pour les protéger d'une
mauvaise utilisation par le programmeur.
On crée des accesseurs et mutateurs (méthodes getters et setters) pour permettre une
modification sûre des variables d'instance.
Dans une classe, on accède aux variables de celle-ci grâce au mot clé this.
Une variable de classe est une variable devant être déclarée static.
Les méthodes n'utilisant que des variables de classe doivent elles aussi être déclarées static.
L'HÉRITAGE
Je vous arrête tout de suite, vous ne toucherez rien. Pas de rapport d'argent entre nous ! Non, la notion
d'héritage en programmation est différente de celle que vous connaissez, bien qu'elle en soit tout de même
proche. C'est l'un des fondements de la programmation orientée objet !
Imaginons que, dans le programme réalisé précédemment, nous voulions créer un autre type d'objet : des
objets Capitale. Ceux-ci ne seront rien d'autre que des objets Ville avec un paramètre en plus... disons
un monument. Vous n'allez tout de même pas recoder tout le contenu de la classe Ville dans la nouvelle
classe ! Déjà, ce serait vraiment contraignant, mais en plus, si vous aviez à modifier le fonctionnement de la
catégorisation de nos objets Ville, vous auriez aussi à effectuer la modification dans la nouvelle classe…
Ce n'est pas terrible.
Heureusement, l'héritage permet à des objets de fonctionner de la même façon que d'autres.
Le principe de l'héritage
Comme je vous l'ai dit dans l'introduction, la notion d'héritage est l'un des fondements de la programmation
orientée objet. Grâce à elle, nous pourrons créer des classes héritées (aussi appelées classes classes
dérivées) de nos classes mères (aussi appelées classes classes de base). Nous pourrons créer autant de
classes dérivées, par rapport à notre classe de base, que nous le souhaitons. De plus, nous pourrons nous
servir d'une classe dérivée comme d'une classe de base pour élaborer encore une autre classe dérivée.
Reprenons l'exemple dont je vous parlais dans l'introduction. Nous allons créer une nouvelle classe,
nomméeCapitale, héritée de Ville. Vous vous rendrez vite compte que les objets Capitale auront
tous les attributs et toutes les méthodes associés aux objets Ville !
}
C'est le mot clé extends qui informe Java que la classe Capitale est héritée de Ville. Pour vous le
prouver, essayez ce morceau de code dans votre main :
Objet Capitale
C'est bien la preuve que notre objet Capitale possède les propriétés de notre objet Ville. Les objets
hérités peuvent accéder à toutes les méthodes public (ce n'est pas tout à fait vrai… Nous le verrons avec
le mot clé protected) de leur classe mère, dont la méthode decrisToi() dans le cas qui nous occupe.
En fait, lorsque vous déclarez une classe, si vous ne spécifiez pas de constructeur, le compilateur (le
programme qui transforme vos codes sources en byte code) créera, au moment de l'interprétation, le
constructeur par défaut. En revanche, dès que vous avez créé un constructeur, n'importe lequel, la JVM ne
crée plus le constructeur par défaut.
Notre classe Capitale hérite de la classe Ville, par conséquent, le constructeur de notre objet appelle,
de façon tacite, le constructeur de la classe mère. C'est pour cela que les variables d'instance ont pu être
initialisées ! Par contre, essayez ceci dans votre classe :
Pourquoi cela ? Tout simplement parce les variables de la classe Ville sont déclarées private. C'est ici
que le nouveau mot clé protected fait son entrée. En fait, seules les méthodes et les variables
déclaréespublic ou protected peuvent être utilisées dans une classe héritée ; le compilateur rejette
votre demande lorsque vous tentez d'accéder à des ressources privées d'une classe mère !
}
La raison est toute simple : si nous admettons que nos
classes AgrafeuseAirComprime etAgrafeuseManuelle ont toutes les deux une
méthode agrafer() et que vous ne redéfinissez pas cette méthode dans l'objet AgrafeuseBionique,
la JVM ne saura pas quelle méthode utiliser et, plutôt que de forcer le programmeur à gérer les cas d'erreur,
les concepteurs du langage ont préféré interdire l'héritage multiple.
À présent, continuons la construction de notre objet hérité : nous allons agrémenter notre
classe Capitale. Comme je vous l'avais dit, ce qui différenciera nos objets Capitale de nos
objets Ville sera la présence d'un nouveau champ : le nom d'un monument. Cela implique que nous
devons créer un constructeur par défaut et un constructeur d'initialisation pour notre objet Capitale.
Avant de foncer tête baissée, il faut que vous sachiez que nous pouvons faire appel aux variables de la
classe mère dans nos constructeurs grâce au mot clé super. Cela aura pour effet de récupérer les éléments
de l'objet de base, et de les envoyer à notre objet hérité. Démonstration :
Cependant, la méthode decrisToi() ne prend pas en compte le nom d'un monument. Eh bien le mot
clésuper() fonctionne aussi pour les méthodes de classe, ce qui nous donne une
méthode decrisToi()un peu différente, car nous allons lui ajouter le champ monument pour notre
description :
public Capitale(){
//Ce mot clé appelle le constructeur de la classe mère
super();
monument = "aucun";
}
return str;
}
}
Si vous relancez les instructions présentes dans le main depuis le début, vous obtiendrez quelque chose
comme sur la figure suivante.
Utilisation de super
J'ai ajouté les instructions System.out.println afin de bien vous montrer comment les choses se
passent.
Bon, d'accord : nous n'avons toujours pas fait le constructeur d'initialisation de Capitale. Eh bien ?
Qu'attendons-nous ?
/**
* Description d'une capitale
* @return String retourne la description de l'objet
*/
public String decrisToi(){
String str = super.decrisToi() + "\n \t ==>>" + this.monument + "en est un monument";
return str;
}
/**
* @return le nom du monument
*/
public String getMonument() {
return monument;
}
Dans notre exemple, vous avez vu qu'il suffisait d'utiliser la méthode decrisToi() sur un objet Villeou
sur un objet Capitale. On pourrait construire un tableau d'objets et appeler decrisToi() sans se
soucier de son contenu : villes, capitales, ou les deux.
else{
Capitale C = new Capitale(tab[i], tab2[i], "france", "la tour Eiffel");
tableau[i] = C;
}
}
Vous aurez sans doute remarqué que je n'utilise que des objets Ville dans ma boucle : on appelle ceci
lacovariance des variables ! Cela signifie qu'une variable objet peut contenir un objet qui hérite du type de
cette variable. Dans notre cas, un objet de type Ville peut contenir un objet de type Capitale. Dans ce
cas, on dit que Ville est la superclasse de Capitale. La covariance est efficace dans le cas où la classe
héritant redéfinit certaines méthodes de sa superclasse.
Une méthode surchargée diffère de la méthode originale par le nombre ou le type des
paramètres qu'elle prend en entrée.
Une méthode polymorphe a un squelette identique à la méthode de base, mais traite les choses
différemment. Cette méthode se trouve dans une autre classe et donc, par extension, dans une
autre instance de cette autre classe.
Vous devez savoir encore une chose sur l'héritage. Lorsque vous créez une classe (Ville, par exemple),
celle-ci hérite, de façon tacite, de la classe Object présente dans Java.
Toutes nos classes héritent donc des méthodes de la classe Object, comme equals() qui prend un objet
en paramètre et qui permet de tester l'égalité d'objets. Vous vous en êtes d'ailleurs servis pour tester l'égalité
de String() dans la première partie de ce livre.
Donc, en redéfinissant une méthode de la classe Object dans la classe Ville, nous pourrions utiliser la
covariance.
La méthode de la classe Object la plus souvent redéfinie est toString() : elle retourne
un Stringdécrivant l'objet en question (comme notre méthode decrisToi()). Nous allons donc copier
la procédure de la méthode decrisToi() dans une nouvelle méthode de la classe Ville : toString().
Voici son code :
else{
Capitale C = new Capitale(tab[i], tab2[i], "france", "la tour Eiffel");
tableau[i] = C;
}
}
Attention : si vous ne redéfinissez pas ou ne « polymorphez » pas la méthode d'une classe mère dans une
classe fille (exemple de toString()), à l'appel de celle-ci avec un objet fille, c'est la méthode de la classe
mère qui sera invoquée !
Une précision s'impose : si vous avez un objet v de type Ville, par exemple, que vous n'avez pas redéfini
la méthode toString() et que vous testez ce code :
System.out.println(v);
… vous appellerez automatiquement la méthode toString() de la classe Object ! Mais ici, comme
vous avez redéfini la méthode toString() dans votre classe Ville, ces deux instructions sont
équivalentes :
System.out.println(v.toString());
//Est équivalent à
System.out.println(v);
Pour plus de clarté, je conserverai la première syntaxe, mais il est utile de connaître cette alternative.
Pour clarifier un peu tout ça, vous avez accès aux méthodes public et protected de la
classe Objectdès que vous créez une classe objet (grâce à l'héritage tacite). Vous pouvez donc utiliser
lesdites méthodes ; mais si vous ne les redéfinissez pas, l'invocation se fera sur la classe mère avec les
traitements de la classe mère.
Si vous voulez un exemple concret de ce que je viens de vous dire, vous n'avez qu'à retirer la
méthodetoString() dans les classes Ville et Capitale : vous verrez que le code de la
méthode mainfonctionne toujours, mais que le résultat n'est plus du tout pareil, car à l'appel de la
méthode toString(), la JVM va regarder si celle-ci existe dans la classe appelante et, comme elle ne la
trouve pas, elle remonte dans la hiérarchie jusqu'à arriver à la classe Object…
Vous devez savoir qu'une méthode n'est « invocable » par un objet que si celui-ci définit ladite méthode.
Ainsi, ce code ne fonctionne pas :
else{
Capitale C = new Capitale(tab[i], tab2[i], "france", "la tour Eiffel");
tableau[i] = C;
}
}
Vous voyez donc l'intérêt des méthodes polymorphes : grâce à elles, vous n'avez plus à vous soucier du
type de variable appelante. Cependant, n'utilisez le type Object qu'avec parcimonie.
public boolean equals(Object o), qui permet de vérifier si un objet est égal à un
autre ;
public int hashCode(), qui attribue un code de hashage à un objet. En gros, elle donne
un identifiant à un objet. Notez que cet identifiant sert plus à catégoriser votre objet qu'à
l'identifier formellement.
Il faut garder en tête que ce n'est pas parce que deux objets ont un même code de hashage qu'ils sont égaux
(en effet, deux objets peuvent avoir la même « catégorie » et être différents…) ; par contre, deux objets
égaux ont forcément le même code de hashage ! En fait, la méthode hashcode() est utilisée par certains
objets (que nous verrons avec les collections) afin de pouvoir classer les objets entre eux.
La bonne nouvelle, c'est qu'Eclipse vous permet de générer automatiquement ces deux méthodes, via le
menu Source/Generate hashcode and equals. Voilà à quoi pourraient ressembler ces deux
méthodes pour notre objet Ville.
//On s'assure que les objets sont du même type, ici de type Ville
//La méthode getClass retourne un objet Class qui représente la classe de votre objet
//Nous verrons ça un peu plus tard...
if (getClass() != obj.getClass())
return false;
if (nomVille == null) {
if (other.nomVille != null)
return false;
}
else if (!nomVille.equals(other.nomVille))
return false;
return true;
}
Il existe encore un type de méthodes dont je ne vous ai pas encore parlé : le type final. Une méthode
signée final est figée, vous ne pourrez jamais la redéfinir (la méthode getClass() de la
classeObject est un exemple de ce type de méthode : vous ne pourrez pas la redéfinir).
Nous avons vu précédemment que les méthode equals() et hashcode() sont souvent redéfinies afin de
pouvoir gérer l'égalité de vos objets et de les catégoriser. Vous avez pu vous rendre compte que leur
redéfinition n'est pas des plus simples (si nous le faisons avec nos petits doigts).
Avec Java 7, il existe une classe qui permet de mieux gérer la redéfinitions de ces méthodes
:java.util.Objects. Attention, il ne s'agit pas de la classe java.lang.Object dont tous les objets
héritent ! Ici il s'agit d'Objects avec un « s » ! Ce nouvel objet ajoute deux fonctionnalités qui permettent
de simplifier la redéfinition des méthodes vues précédemment.
Nous allons commencer par la plus simple : hashcode(). La classe Objects propose une
méthodehash(Object… values). Cette méthode s'occupe de faire tout le nécessaire au calcul d'un code
de hashage en vérifiant si les attributs sont null ou non et tutti quanti. C'est tout de même sympa. Voici à
quoi ressemblerait notre méthode hashcode() avec cette nouveauté :
public int hashCode() {
return Objects.hash(categorie, nbreHabitants, nomPays, nomVille);
}
Il faudra, bien sûr, penser à importer la classe pour pouvoir l'utiliser.
Ce nouvel objet intègre aussi une méthode equals() qui se charge de vérifier si les valeurs passées en
paramètre sont null ou non. Du coup, nous aurons un code beaucoup plus clair et lisible. Voici à quoi
ressemblerait notre méthode equals() de l'objet Ville :
//On s'assure que les objets sont du même type, ici de type Ville
if (getClass() != obj.getClass())
return false;
Une classe hérite d'une autre classe par le biais du mot clé extends.
Si aucun constructeur n'est défini dans une classe fille, la JVM en créera un et appellera
automatiquement le constructeur de la classe mère.
La classe fille hérite de toutes les propriétés et méthodes public et protected de la classe
mère.
Les méthodes et les propriétés private d'une classe mère ne sont pas accessibles dans la
classe fille.
On peut redéfinir une méthode héritée, c'est-à-dire qu'on peut changer tout son code.
On peut utiliser le comportement d'une classe mère par le biais du mot clé super.
Si une méthode d'une classe mère n'est pas redéfinie ou « polymorphée », à l'appel de cette
méthode par le biais d'un objet enfant, c'est la méthode de la classe mère qui sera utilisée.
Vous ne pouvez pas hériter d'une classe déclarée final.
Dans ce chapitre, nous allons découvrir le principe de modélisation d'objet. Le sigle « UML »
signifie UnifiedModeling Language, que l'on peut traduire par « langage de modélisation unifié ». Il ne
s'agit pas d'un langage de programmation, mais plutôt d'une méthode de modélisation. La
méthode Merise, par exemple, en est une autre.
En fait, lorsque vous programmez en orienté objet, il vous sera sans doute utile de pouvoir schématiser vos
classes, leur hiérarchie, leurs dépendances, leur architecture, etc. L'idée est de pouvoir, d'un simple coup
d'œil, vous représenter le fonctionnement de votre logiciel : imaginez UML un peu comme une partition de
musique pour le musicien.
Le but de ce chapitre n'est pas de vous transformer en experts UML, mais de vous donner suffisamment de
bases pour mieux appréhender la modélisation et ensuite bien cerner certains concepts de la POO.
Présentation d'UML
Je sais que vous êtes des Zéros avertis en matière de programmation, ainsi qu'en informatique en général,
mais mettez-vous dans la peau d'une personne totalement dénuée de connaissances dans le domaine. Il
fallait trouver un langage commun aux commerciaux, aux responsables de projets informatiques et aux
développeurs, afin que tout ce petit monde se comprenne. Avec UML, c'est le cas.
En fait, avec UML, vous pouvez modéliser toutes les étapes du développement d'une application
informatique, de sa conception à la mise en route, grâce à des diagrammes. Il est vrai que certains de ces
diagrammes sont plus adaptés pour les informaticiens, mais il en existe qui permettent de voir comment
interagit l'application avec son contexte de fonctionnement… Et dans ce genre de cas, il est indispensable
de bien connaître l'entreprise pour laquelle l'application est prévue. On recourt donc à un mode de
communication compréhensible par tous : UML.
Il existe bien sûr des outils de modélisation pour créer de tels diagrammes. En ce qui me concerne,
j'utiliseargoUML. Il a le mérite d'être gratuit et écrit en Java, donc multi-plates-formes.
boUML,
Together,
Poseidon,
Pyut
etc.
Avec ces outils, vous pouvez réaliser les différents diagrammes qu'UML vous propose :
le diagramme de use case (cas d'utilisation) permet de déterminer les différents cas d'utilisation
d'un programme informatique ;
le diagramme de classes ; c'est de celui-là que nous allons nous servir. Il permet de modéliser
des classes ainsi que les interactions entre elles ;
et d'autres encore…
À présent, nous allons apprendre à lire un diagramme de classes. Vous avez deviné qu'une classe est
modélisée sous la forme représentée sur la figure suivante.
Classe en UML
Voici une classe nommée ObjetA qui a comme attributs :
La portée des attributs et des méthodes n'est pas représentée ici. Vous voyez, la modélisation d'un objet est
toute simple et très compréhensible !
Vous allez voir : les interactions sont, elles aussi, très simples à modéliser.
En fait, comme vous l'avez vu avec l'exemple, les interactions sont modélisées par des flèches de plusieurs
sortes. Nous aborderons ici celles dont nous pouvons nous servir dans l'état actuel de nos connaissances (au
fur et à mesure de la progression, d'autres flèches apparaîtront).
Sur le diagramme représenté à la figure suivante, vous remarquez un deuxième objet qui dispose, lui aussi,
de paramètres. Ne vous y trompez pas, ObjetB possède également les attributs et les méthodes de la
classeObjetA. D'après vous, pourquoi ? C'est parce que la flèche qui relie nos deux objets signifie
« extends ». En gros, vous pouvez lire ce diagramme comme suit : l'ObjetB hérite de l'ObjetA, ou
encoreObjetB est un ObjetA.
Représentation de l'héritage
Nous allons voir une autre flèche d'interaction. Je sais que nous n'avons pas encore rencontré ce cas de
figure, mais il est simple à comprendre.
De la même façon que nous pouvons utiliser des objets de type String dans des classes que nous
développons, nous pouvons aussi utiliser comme variable d'instance, ou de classe, un objet que nous avons
codé. La figure suivante modélise ce cas.
Représentation
de l'appartenance
Dans cet exemple simpliste, nous avons toujours notre héritage entre un objet A et un objet B, mais dans ce
cas, l'ObjetA (et donc l'ObjetB) possède une variable de classe de type ObjetC, ainsi qu'une méthode
dont le type de retour est ObjetC (car la méthode retourne un ObjetC). Vous pouvez lire ce diagramme
comme suit : l'ObjetA a un ObjetC (donc une seule instance d'ObjetC est présente dans ObjetA).
Fichier ObjetA.java
Fichier ObjetB.java
Fichier ObjetC.java
}
Il reste une dernière flèche que nous pouvons mentionner, car elle ne diffère que légèrement de la première.
Un diagramme la mettant en œuvre est représenté sur la figure suivante.
Représentation de la composition
Ce diagramme est identique au précédent, à l'exception de l'ObjetD. Nous devons le lire comme ceci :
l'ObjetA est composé de plusieurs instances d'ObjetD. Vous pouvez d'ailleurs remarquer que la variable
d'instance correspondante est de type tableau…
Voici le code Java correspondant :
Fichier ObjetA.java
Fichier ObjetB.java
Fichier ObjetC.java
Fichier ObjetD.java
}
Il est bien évident que ces classes ne font strictement rien. Je les ai utilisées à titre d'exemple pour la
modélisation.
Voilà, c'en est fini pour le moment. Attendez-vous donc à rencontrer des diagrammes dans les prochains
chapitres !
Vous pouvez représenter la composition avec une flèche signifiant « est composé de ».
LES PACKAGES
Lorsque nous avons été confrontés pour la première fois aux packages, c'était pour importer la
classeScanner via l'instruction import java.util.Scanner;. Le fonctionnement des packages est
simple à comprendre : ce sont comme des dossiers permettant de ranger nos classes. Charger un package
nous permet d'utiliser les classes qu'il contient.
Il n'y aura rien de franchement compliqué dans ce chapitre si ce n'est que nous reparlerons un peu de la
portée des classes Java.
L'un des avantages des packages est que nous allons y gagner en lisibilité dans notre package par défaut,
mais aussi que les classes mises dans un package sont plus facilement transportables d'une application à
l'autre. Pour cela, il vous suffit d'inclure le dossier de votre package dans un projet et d'y importer les
classes qui vous intéressent !
Pour créer un nouveau package, cliquez simplement sur cette icône comme à la figure suivante (vous
pouvez aussi effectuer un clic droit puis New > Package).
Nouveau package
Une boîte de dialogue va s'ouvrir et vous demander le nom de votre package, comme à la figure suivante.
Nom du
package
Il existe aussi une convention de nommage pour les packages :
les caractères autorisés sont alphanumériques (de a à z, de 0 à 9) et peuvent contenir des points
(.) ;
tout package doit commencer par com, edu, gov, mil, net, org ou les deux lettres identifiant un
pays (ISO Standard 3166, 1981) ; « fr » correspond à la France, « en » correspond à
l'Angleterre (pour England)etc.
aucun mot clé Java ne doit être présent dans le nom, sauf si vous le faites suivre d'un
underscore (« _ »), comme ceci : com.sdz.package_.
Comme ce cours est issu du Site du Zéro, j'ai pris le nom à l'envers : « sdz.com » nous donne « com.sdz ».
Pour le cas qui nous occupe, appelons-le com.sdz.test. Cliquez sur Finish pour créer le package. Et
voilà : celui-ci est prêt à l'emploi.
Je vous invite à aller voir dans le dossier où se trouvent vos codes sources : vous constaterez qu'il y a
l'arborescence du dossier com/sdz/test dans votre dossier src.
Vous conviendrez que la création d'un package est très simple. Cependant, je ne peux pas vous laisser sans
savoir que la portée de vos classes est affectée par les packages…
Lorsque vous avez créé votre première classe, vous avez vu qu'Eclipse met systématiquement le mot
clépublic devant la déclaration de la classe. Je vous avais alors dit que public class
Ville et class Ville étaient sensiblement différents et que le mot clé public influait sur la portée de
notre classe. En fait, une classe déclarée avec le mot clé public sera visible même à l'extérieur de son
package, les autres ne seront accessibles que depuis l'intérieur du package : on dit que leur portée
est default.
Afin de vous prouver mes dires, je vous invite à créer un second package : je l'ai appelé « com.sdz.test2 ».
Dans le premier package, com.sdz.test, créez une classe A de portée public et une classe B de
portéedefault, comme ceci (j'ai volontairement déclaré les variables d'instance public afin d'alléger
l'exemple) :
package com.sdz.test;
class B {
public String str ="";
}
package com.sdz.test;
public class A {
public B b = new B();
}
Vous aurez remarqué que les classes contenues dans un package ont en toute première instruction la
déclaration de ce package.
Maintenant que cela est fait, afin de faire le test, créez une classe contenant la méthode main, toujours dans
le même package, comme ceci :
package com.sdz.test;
public class Main {
public static void main(String[] args){
A a = new A();
B b = new B();
//Aucun problème ici
}
}
Ce code, bien qu'il ne fasse rien, fonctionne très bien : aucun problème de compilation, entre autres.
Maintenant, faites un copier-coller de la classe ci-dessus dans le package com.sdz.test2. Vous devriez
avoir le résultat représenté à la figure suivante.
Si vous voulez utiliser un mot clé Java dans le nom de votre package, vous devez le faire suivre
d'un underscore (« _ »).
Les classes déclarées public sont visibles depuis l'extérieur du package qui les contient.
Les classes n'ayant pas été déclarées public ne sont pas visibles depuis l'extérieur du package
qui les contient.
Si une classe déclarée public dans son package a une variable d'un type ayant une
portéedefault, cette dernière ne pourra pas être modifiée depuis l'extérieur de son package.
LES CLASSES ABSTRAITES ET LES
INTERFACES
Nous voilà de retour avec deux fondements du langage Java. Je vais essayer de faire simple : derrière ces
deux notions se cache la manière dont Java vous permet de structurer votre programme.
Grâce aux chapitres précédents, vous vous rendez compte que vos programmes Java regorgeront de
classes, avec de l'héritage, des dépendances, de la composition… Afin de bien structurer vos programmes
(on parle d'architecture logicielle), vous allez vous creuser les méninges pour savoir où ranger des
comportements d'objets :
Comment obtenir une structure assez souple pour pallier les problèmes de programmation les plus
courants ?
La réponse est dans ce chapitre.
Une classe abstraite est quasiment identique à une classe normale. Oui, identique aux classes que vous
avez maintenant l'habitude de coder. Cela dit, elle a tout de même une particularité : vous ne pouvez pas
l'instancier ! Vous avez bien lu. Imaginons que nous ayons une classe A déclarée abstraite. Voici un code
qui ne compilera pas :
Dans ce programme, vous aurez des loups, des chiens, des chats, des lions et des tigres. Mais vous n'allez
tout de même pas faire toutes vos classes bêtement : il va de soi que tous ces animaux ont des points
communs ! Et qui dit points communs dit héritage. Que pouvons-nous définir de commun à tous ces
animaux ? Le fait qu'ils aient une couleur, un poids, un cri, une façon de se déplacer, qu'ils mangent et
boivent quelque chose.
Nous pouvons donc créer une classe mère : appelons-la Animal. Avec ce que nous avons dégagé de
commun, nous pouvons lui définir des attributs et des méthodes. La figure suivante représente nos classes.
Classe Animal
Nous avons bien notre classe mère Animal et nos animaux qui en héritent. À présent, laissez-moi vous
poser une question. Vu que notre classe Animal est public, qu'est censé faire un objet Animal ? Quel
est son poids, sa couleur, que mange-t-il ? Je sais, cela fait plus qu'une question.
C'est là qu'entrent en jeu nos classes abstraites. En fait, ces classes servent à définir une superclasse : par
là, vous pouvez comprendre qu'elles servent essentiellement à créer un nouveau type d'objets. Voyons
maintenant comment créer une telle classe.
En fait, il existe une règle pour qu'une classe soit considérée comme abstraite. Elle doit être déclarée avec
le mot clé abstract. Voici un exemple illustrant mes dires :
Retenez bien qu'une méthode abstraite n'est composée que de l'en-tête de la méthode suivie d'un point-
virgule « ; ».
Il faut que vous sachiez qu'une méthode abstraite ne peut exister que dans une classe abstraite. Si, dans une
classe, vous avez une méthode déclarée abstraite, vous devez déclarer cette classe comme étant abstraite.
Voyons à quoi cela peut servir. Vous avez vu les avantages de l'héritage et du polymorphisme. Eh bien nos
classes enfants hériteront aussi des méthodes abstraites, mais étant donné que celles-ci n'ont pas de corps,
nos classes enfants seront obligées de redéfinir ces méthodes ! Elles présentent donc des méthodes
polymorphes, ce qui implique que la covariance des variables pointe à nouveau le bout de son nez :
Vous pouvez aussi utiliser une variable de type Object comme référence à un objet Loup, à un
objetChien etc. Vous saviez déjà que ce code fonctionne :
Pour le moment, nous n'avons de code dans aucune classe ! Les exemples que je vous ai fournis ne font rien
du tout, mais ils fonctionneront lorsque nous aurons ajouté des morceaux de code à nos classes.
Nous allons donc ajouter des morceaux de code à nos classes. Tout d'abord, établissons un bilan de ce que
nous savons :
Nos objets seront probablement tous de couleur et de poids différents. Nos classes auront donc le
droit de modifier ceux-ci.
Ici, nous partons du principe que tous nos animaux mangent de la viande. La
méthode manger()sera donc définie dans la classe Animal.
Idem pour la méthode boire(). Ils boiront tous de l'eau (je vous voyais venir).
Ils ne crieront pas et ne se déplaceront pas de la même manière. Nous emploierons donc des
méthodes polymorphes et déclarerons les méthodes deplacement() et crier() abstraites dans
la classeAnimal.
La figure suivante représente le diagramme des classes de nos futurs objets. Ce diagramme permet de voir
si une classe est abstraite : son nom est alors en italique.
Hiérarchie de nos classes
Nous voyons bien que notre classe Animal est déclarée abstraite et que nos classes filles héritent de
celle-ci. De plus, nos classes filles ne redéfinissent que deux méthodes sur quatre, on en conclut donc que
ces deux méthodes doivent être abstraites. Nous ajouterons deux constructeurs à nos classes filles : un
par défaut et un autre comprenant les deux paramètres d'initialisation. À cela, nous ajouterons aussi les
accesseurs d'usage. Mais dites donc… nous pouvons améliorer un peu cette architecture, sans pour
autant rentrer dans les détails !
Nous allons redéfinir la méthode deplacement() dans cette classe, car nous allons partir du principe que
les félins se déplacent d'une certaine façon et les canins d'une autre. Avec cet exemple, nous réviserons le
polymorphisme. La figure suivante correspond à notre diagramme mis à jour (vous avez remarqué ? J'ai
ajouté une méthode toString()).
Nouvelle architecture des classes
Animal.java
Felin.java
Chien.java
public Chien(){
void crier() {
System.out.println("J'aboie sans raison !");
}
}
Loup.java
public Loup(){
void crier() {
System.out.println("Je hurle à la Lune en faisant ouhouh !");
}
}
Lion.java
public Lion(){
void crier() {
System.out.println("Je rugis dans la savane !");
}
}
Tigre.java
}
public Tigre(String couleur, int poids){
this.couleur = couleur;
this.poids = poids;
}
void crier() {
System.out.println("Je grogne très fort !");
}
}
Chat.java
public Chat(){
}
public Chat(String couleur, int poids){
this.couleur = couleur;
this.poids = poids;
}
void crier() {
System.out.println("Je miaule sur les toits !");
}
}
Dis donc ! Une classe abstraite ne doit-elle pas comporter une méthode abstraite ?
Je n'ai jamais dit ça ! Une classe déclarée abstraite n'est pas « instanciable », mais rien ne l'oblige à
comprendre des méthodes abstraites. En revanche, une classe contenant une méthode abstraite doit être
déclarée abstraite ! Je vous invite maintenant à faire des tests :
Dans la méthode toString() de la classe Animal, j'ai utilisé la méthode getClass() qui — je vous le
donne en mille — se trouve dans la classe Object. Celle-ci retourne «class <nom de la classe> ».
Dans cet exemple, nous pouvons constater que nous avons un objet Loup :
À l'appel de la méthode deplacement() : c'est la méthode de la classe Canin qui est invoquée
ici.
À l'appel de la méthode crier() : c'est la méthode de la classe Loup qui est appelée.
Remplacez le type de référence (ici, Loup) par Animal, essayez avec des objets Chien, etc. Vous verrez
que tout fonctionne.
Les interfaces
L'un des atouts majeurs — pour ne pas dire l'atout majeur — de la programmation orientée objet est
laréutilisabilité de vos objets. Il est très commode d'utiliser un objet (voire une architecture) que nous
avons déjà créé pour une nouvelle application.
Admettons que l'architecture que nous avons développée dans les chapitres précédents forme une bonne
base. Que se passerait-il si un autre développeur vous demandait d'utiliser vos objets dans un autre type
d'application ? Ici, nous ne nous sommes occupés que de l'aspect générique des animaux que nous avons
créés. Cependant, la personne qui vous a contacté, elle, développe une application pour un chenil.
La contrainte principale, c'est que vos chiens devront apprendre à faire de nouvelles choses telles que :
faire le beau ;
Je ne vois pas le problème… Tu n'as qu'à ajouter ces méthodes dans la classe Animal !
Ouh là ! Vous vous rendez compte que vous obtiendrez des lions qui auront la possibilité de faire le beau ?
Dans ce cas, on n'a qu'à mettre ces méthodes dans la classe Chien, mais j'y vois deux problèmes :
1. vous allez devoir mettre en place une convention de nommage entre le programmeur qui va utiliser
vos objets et vous. Vous ne pourrez pas utiliser la méthode faireCalin(), alors que le
programmeur oui ;
2. si vous faites cela, adieu au polymorphisme ! Vous ne pourrez pas appeler vos objets par le biais
d'un supertype. Pour pouvoir accéder à ces méthodes, vous devrez obligatoirement passer par une
référence à un objet Chien. Pas terrible, tout ça !
Tu nous as dit que pour utiliser au mieux le polymorphisme, nous devions définir les méthodes au plus haut
niveau de la hiérarchie. Alors du coup, il faut redéfinir un supertype pour pouvoir utiliser le
polymorphisme !
Oui, et je vous rappelle que l'héritage multiple est interdit en Java. Et quand je dis interdit, je veux dire que
Java ne le gère pas ! Il faudrait pouvoir développer un nouveau supertype et s'en servir dans nos
classesChien. Eh bien nous pouvons faire cela avec des interfaces.
En fait, les interfaces permettent de créer un nouveau supertype ; on peut même en ajouter autant que
l'on le veut dans une seule classe ! Quant à l'utilisation de nos objets, la convention est toute trouvée.
Pourquoi ? Parce qu'une interface n'est rien d'autre qu'une classe 100 % abstraite ! Allez : venons-en aux
faits !
public class A{ }
… il vous suffit de faire :
public interface I{ }
Voilà : vous venez d'apprendre à déclarer une interface. Vu qu'une interface est une classe 100 % abstraite,
il ne vous reste qu'à y ajouter des méthodes abstraites, mais sans le mot clé abstract.
public interface I{
public void A();
public String B();
}
public interface I2{
public void C();
public String D();
}
Et pour faire en sorte qu'une classe utilise une interface, il suffit d'utiliser le mot clé implements. Ce qui
nous donnerait :
Comme le titre de cette sous-section le stipule, nous allons créer l'interface Rintintin pour ensuite
l'implémenter dans notre objet Chien.
Sous Eclipse, vous pouvez faire File > New > Interface, ou simplement cliquer sur la flèche noire à
côté du « C » pour la création de classe, et choisir interface, comme à la figure suivante. Voici son code
:
À présent, il ne nous reste plus qu'à implémenter l'interface dans notre classe Chien :
public Chien(){
}
public Chien(String couleur, int poids){
this.couleur = couleur;
this.poids = poids;
}
void crier() {
System.out.println("J'aboie sans raison !");
}
System.out.println("--------------------------------------------");
//Les méthodes de l'interface
c.faireCalin();
c.faireLeBeau();
c.faireLechouille();
System.out.println("--------------------------------------------");
//Utilisons le polymorphisme de notre interface
Rintintin r = new Chien();
r.faireLeBeau();
r.faireCalin();
r.faireLechouille();
}
}
Objectif atteint ! Nous sommes parvenus à définir deux superclasses afin de les utiliser comme supertypes
et de jouir pleinement du polymorphisme.
Dans la suite de ce chapitre, nous verrons qu'il existe une façon très intéressante d'utiliser les interfaces
grâce à une technique de programmation appelée « pattern strategy ». Sa lecture n'est pas indispensable,
mais cela vous permettra de découvrir à travers un cas concret comment on peut faire évoluer au mieux un
programme Java.
Le pattern strategy
Nous allons partir du principe que vous avez un code qui fonctionne, c'est-à-dire un ensemble de classes
liées par l'héritage, par exemple. Nous allons voir ici que, en dépit de la puissance de l'héritage, celui-ci
atteint ses limites lorsque vous êtes amenés à modifier la hiérarchie de vos classes afin de répondre à une
demande (de votre chef, d'un client etc.).
Le fait de toucher à votre hiérarchie peut amener des erreurs indésirables, voire des absurdités : tout cela
parce que vous allez changer une structure qui fonctionne à cause de contraintes que l'on vous impose. Pour
remédier à ce problème, il existe un concept simple (il s'agit même d'un des fondements de la
programmation orientée objet) : l'encapsulation !
Nous allons parler de cette solution en utilisant un design pattern (ou « modèle de conception » en
français). Un design pattern est un patron de conception, une façon de construire une hiérarchie des
classes permettant de répondre à un problème. Nous aborderons le pattern strategy, qui va nous
permettre de remédier à la limite de l'héritage. En effet, même si l'héritage offre beaucoup de
possibilités, il a ses limites.
Posons le problème
Mettez-vous dans la peau de développeurs jeunes et ambitieux d'une toute nouvelle société qui crée des
jeux vidéo. Le dernier titre en date, « Z-Army », un jeu de guerre très réaliste, a été un succès international
! Votre patron est content et vous aussi. Vous vous êtes basés sur une architecture vraiment simple afin de
créer et utiliser des personnages, comme le montre la figure suivante.
Les guerriers savent se battre tandis que les médecins soignent les blessés sur le champ de bataille. Et c'est
maintenant que commencent les ennuis !
Votre patron vous a confié le projet « Z-Army 2 : The return of the revenge », et vous vous dites : « Yes !
Mon architecture fonctionne à merveille, je la garde. » Un mois plus tard, votre patron vous convoque dans
son bureau et vous dit : « Nous avons fait une étude de marché, et il semblerait que les joueurs aimeraient
se battre aussi avec les médecins ! » Vous trouvez l'idée séduisante et avez déjà pensé à une solution :
déplacer la méthode combattre() dans la superclasse Personnage, afin de la redéfinir dans la
classe Medecinet jouir du polymorphisme ! La figure suivante schématise le tout.
Déplacement de la méthode combattre()
À la seconde étude de marché, votre patron vous annonce que vous allez devoir créer des civils, des
snipers, des chirurgiens etc. Toute une panoplie de personnages spécialisés dans leur domaine, comme le
montre la figure suivante.
Nouveaux
personnages spécialisés
Personnage.java
Guerrier.java
Medecin.java
Civil.java
Chirurgien.java
Sniper.java
le code contenu dans la méthode seDeplacer() est dupliqué dans toutes les classes ; il est
identique dans toutes celles citées ci-dessus ;
le code de la méthode combattre() des classes Chirurgien et Civil est lui aussi dupliqué !
La duplication de code est une chose qui peut générer des problèmes dans le futur. Je m'explique.
Pour le moment, votre chef ne vous a demandé que de créer quelques classes supplémentaires. Qu'en serait-
il si beaucoup de classes avaient ce même code dupliqué ? Il ne manquerait plus que votre chef vous
demande de modifier à nouveau la façon de se déplacer de ces objets, et vous courrez le risque d'oublier
d'en modifier un. Et voilà les incohérences qui pointeront le bout de leur nez !
No problemo ! Tu vas voir ! Il suffit de mettre un comportement par défaut pour le déplacement et pour le
combat dans la superclasse Personnage.
Effectivement, votre idée se tient. Donc, cela nous donne ce qui suit :
Personnage.java
Guerrier.java
Medecin.java
Civil.java
Chirurgien.java
public class Chirurgien extends Personnage{
public void soigner(){
System.out.println("Je fais des opérations.");
}
}
Sniper.java
for(Personnage p : tPers){
System.out.println("\nInstance de " + p.getClass().getName());
System.out.println("***************************************");
p.combattre();
p.seDeplacer();
}
}
Le résultat correspond à la figure suivante.
Résultat du code
Apparemment, ce code vous donne ce que vous voulez ! Mais une chose me chiffonne : vous ne pouvez pas
utiliser les classes Medecin et Chirurgien de façon polymorphe, vu que la méthode soigner() leur
est propre ! On pourrait définir un comportement par défaut (ne pas soigner) dans la
superclassePersonnage et le tour serait joué.
Seulement voilà ! Votre chef n'avait pas fini son speech : « Au fait, il faudrait affecter un comportement à
nos personnages en fonction de leurs armes, leurs habits, leurs trousses de soin… Enfin, vous voyez ! Les
comportements figés pour des personnages de jeux, de nos jours, c'est un peu ringard ! »
Vous commencez à voir ce dont il retourne : vous devrez apporter des modifications à votre code, encore et
encore. Bon : pour des programmeurs, cela est le train-train quotidien, j'en conviens. Cependant, si nous
suivons les consignes de notre chef et que nous continuons sur notre lancée, les choses vont se compliquer.
Un problème supplémentaire
Attelons-nous à appliquer les modifications dans notre programme. Selon les directives de notre chef, nous
devons gérer des comportements différents selon les accessoires de nos personnages : il faut utiliser des
variables d'instance pour appliquer l'un ou l'autre comportement.
Afin de simplifier l'exemple, nous n'allons utiliser que des objets String.
La figure suivante correspond au diagramme des classes de notre programme.
Modification de nos classes
Vous avez remarqué que nos personnages posséderont des accessoires. Selon ceux-ci, nos personnages
feront des choses différentes. Voici les recommandations de notre chef bien-aimé :
le sniper peut utiliser son fusil de sniper ainsi qu'un fusil à pompe ;
le médecin a une trousse simple pour soigner, mais peut utiliser un pistolet ;
le chirurgien a une grosse trousse médicale, mais ne peut pas utiliser d'arme ;
tous les personnages hormis le chirurgien peuvent avoir des baskets pour courir;
Il va nous falloir des mutateurs (inutile de mettre les méthodes de renvoi (getXXX), nous ne nous servirons
que des mutateurs !) pour ces variables, insérons-les dans la superclasse ! Bon ! Les modifications sont
faites, les caprices de notre cher et tendre chef sont satisfaits ? Voyons cela tout de suite.
Personnage.java
Guerrier.java
Sniper.java
Civil.java
Medecin.java
Chirurgien.java
Vous constatez avec émerveillement que votre code fonctionne très bien. Les actions par défaut sont
respectées, les affectations d'actions aussi. Tout est parfait !
Vraiment ? Vous êtes sûrs de cela ? Pourtant, je vois du code dupliqué dans certaines classes ! En plus,
nous n'arrêtons pas de modifier nos classes. Dans le premier opus de « Z-Army », celles-ci fonctionnaient
pourtant très bien ! Qu'est-ce qui ne va pas ?
Là-dessus, votre patron rentre dans votre bureau pour vous dire : « Les actions de vos personnages doivent
être utilisables à la volée et, en fait, les personnages peuvent très bien apprendre au fil du jeu. » Les
changements s'accumulent, votre code devient de moins en moins lisible et réutilisable, bref c'est l'enfer sur
Terre.
Après toutes ces émotions, vous allez enfin disposer d'une solution à ce problème de modification du
code source ! Si vous vous souvenez de ce que j'ai dit, un des fondements de la programmation orientée
objet est l'encapsulation.
Le pattern strategy est basé sur ce principe simple. Bon, vous avez compris que le pattern strategy consiste
à créer des objets avec des données, des méthodes (voire les deux) : c'est justement ce qui change dans
votre programme !
Le principe de base de ce pattern est le suivant : « isolez ce qui varie dans votre programme et encapsulez-
le ! »
Déjà, quels sont les éléments qui ne cessent de varier dans notre programme ?
La méthode combattre().
La méthode seDeplacer().
La méthode soigner().
Ce qui serait vraiment grandiose, ce serait d'avoir la possibilité de ne modifier que les comportements et
non les objets qui ont ces comportements ! Non ?
Là, je vous arrête un moment : vous venez de fournir la solution. Vous avez dit : « ce qui serait vraiment
grandiose, ce serait d'avoir la possibilité de ne modifier que les comportements et non les objets qui ont
ces comportements ».
Lorsque je vous ai présenté les diagrammes UML, je vous ai fourni une astuce pour bien différencier les
liens entre les objets. Dans notre cas, nos classes héritant de Personnage héritent aussi de ses
comportements et, par conséquent, on peut dire que nos classes filles sont des Personnage.
Les comportements de la classe mère semblent ne pas être au bon endroit dans la hiérarchie. Vous ne savez
plus quoi en faire et vous vous demandez s'ils ont vraiment leur place dans cette classe ? Il vous suffit de
sortir ces comportements de la classe mère, de créer une classe abstraite ou une interface symbolisant ce
comportement et d'ordonner à votre classe Personnage d'avoir ces comportements. Le nouveau
diagramme des classes se trouve sur la figure suivante.
Nouveau diagramme des classes
Vous apercevez une nouvelle entité sur ce diagramme, l'interface, facilement reconnaissable, ainsi qu'une
nouvelle flèche symbolisant l'implémentation d'interface entre une classe concrète et une interface.
N'oubliez pas que votre code doit être souple et robuste et que — même si ce chapitre vous montre les
limites de l'héritage — le polymorphisme est inhérent à l'héritage (ainsi qu'aux implémentations
d'interfaces).
Il faut vous rendre compte qu'utiliser une interface de cette manière revient à créer un supertype de variable
; du coup, nous pourrons utiliser les classes héritant de ces interfaces de façon polymorphe sans nous
soucier de savoir la classe dont sont issus nos objets ! Dans notre cas, notre
classe Personnage comprendra des objets de type EspritCombatif, Soin et Deplacement !
Avant de nous lancer dans le codage de nos nouvelles classes, vous devez observer que leur nombre a
considérablement augmenté depuis le début. Afin de pouvoir gagner en clarté, nous allons gérer nos
différentes classes avec différents packages.
Comme nous l'avons remarqué tout au long de ce chapitre, les comportements de nos personnages sont trop
épars pour être définis dans notre superclasse Personnage. Vous l'avez dit vous-mêmes : il faudrait que
l'on ne puisse modifier que les comportements et non les classes héritant de notre superclasse !
Les interfaces nous servent à créer un supertype d'objet ; grâce à elles, nous utiliserons des objets de type :
package com.sdz.comportement;
package com.sdz.comportement;
package com.sdz.comportement;
Maintenant que nous avons défini des objets de comportements, nous allons pouvoir remanier notre
classePersonnage. Ajoutons les variables d'instance, les mutateurs et les constructeurs permettant
d'initialiser nos objets :
import com.sdz.comportement.*;
//Méthode de soin
public void soigner(){
//On utilise les objets de déplacement de façon polymorphe
soin.soigne();
}
Je ne vais pas vous donner les codes de toutes les classes. En voici seulement quelques-unes.
Guerrier.java
import com.sdz.comportement.*;
Civil.java
import com.sdz.comportement.*;
import com.sdz.comportement.*;
class Test{
public static void main(String[] args) {
Personnage[] tPers = {new Guerrier(), new Civil(), new Medecin()};
Vous pouvez voir que nos personnages ont tous un comportement par défaut qui leur convient bien ! Nous
avons spécifié, dans le cas où cela s'avère nécessaire, le comportement par défaut d'un personnage dans son
constructeur par défaut :
Voyons maintenant comment indiquer à nos personnages de faire autre chose. Eh oui, la façon dont nous
avons arrangé tout cela va nous permettre de changer dynamiquement le comportement de
chaquePersonnage. Que diriez-vous de faire faire une petite opération chirurgicale à notre
objet Guerrier ?
Pour ce faire, vous pouvez redéfinir son comportement de soin avec son mutateur présent dans la
superclasse en lui passant une implémentation correspondante !
import com.sdz.comportement.*;
class Test{
public static void main(String[] args) {
Personnage pers = new Guerrier();
pers.soigner();
pers.setSoin(new Operation());
pers.soigner();
}
}
En testant ce code, vous constaterez que le comportement de soin de notre objet a changé dynamiquement
sans que nous ayons besoin de changer la moindre ligne de son code source ! Le plus beau dans le fait de
travailler comme cela, c'est qu'il est tout à fait possible d'instancier des objets Guerrier avec des
comportements différents.
Une classe est définie comme abstraite avec le mot clé abstract.
Les classes abstraites sont à utiliser lorsqu'une classe mère ne doit pas être instanciée.
Si une classe contient une méthode abstraite, cette classe doit alors être déclarée abstraite.
Une interface s'implémente dans une classe en utilisant le mot clé implements.
Vous pouvez implémenter autant d'interfaces que vous voulez dans vos classes.
Vous devez redéfinir toutes les méthodes de l'interface (ou des interfaces) dans votre classe.
Le pattern strategy vous permet de rendre une hiérarchie de classes plus souple.
Préférez encapsuler des comportements plutôt que de les mettre d'office dans l'objet concerné.
LES EXCEPTIONS
Voici encore une notion très importante en programmation. Une exception est une erreur se produisant dans
un programme qui conduit le plus souvent à l'arrêt de celui-ci. Il vous est sûrement déjà arrivé d'obtenir un
gros message affiché en rouge dans la console d'Eclipse : eh bien, cela a été généré par une exception… qui
n'a pas été capturée.
Le fait de gérer les exceptions s'appelle aussi « la capture d'exception ». Le principe consiste à repérer un
morceau de code (par exemple, une division par zéro) qui pourrait générer une exception, de capturer
l'exception correspondante et enfin de la traiter, c'est-à-dire d'afficher un message personnalisé et de
continuer l'exécution.
Bon, vous voyez maintenant ce que nous allons aborder dans ce chapitre… Donc, allons-y !
Pour vous faire comprendre le principe des exceptions, je dois tout d'abord vous informer que Java contient
une classe nommée Exception dans laquelle sont répertoriés différents cas d'erreur. La division par zéro
dont je vous parlais plus haut en fait partie ! Si vous créez un nouveau projet avec seulement la
classe mainet y mettez le code suivant :
int j = 20, i = 0;
System.out.println(j/i);
System.out.println("coucou toi !");
… vous verrez apparaître un joli message d'erreur Java (en rouge) comme celui de la figure suivante.
ArithmeticException
Mais surtout, vous devez avoir constaté que lorsque l'exception a été levée, le programme s'est arrêté !
D'après le message affiché dans la console, le nom de l'exception qui a été déclenchée
estArithmeticException. Nous savons donc maintenant qu'une division par zéro est
uneArithmeticException. Nous allons pouvoir la capturer, avec un bloc try{…}catch{…}, puis
réaliser un traitement en conséquence. Ce que je vous propose maintenant, c'est d'afficher un message
personnalisé lors d'une division par 0. Pour ce faire, tapez le code suivant dans votre main :
int j = 20, i = 0;
try {
System.out.println(j/i);
} catch (ArithmeticException e) {
System.out.println("Division par zéro !");
}
System.out.println("coucou toi !");
}
En exécutant ce code, vous obtiendrez le résultat visible à la figure suivante.
Capture d'exception
Voyons un peu ce qui se passe :
Nous initialisons deux variables de type int, l'une à 0 et l'autre à un nombre quelconque.
Une exception de type ArithmeticException est levée lorsque le programme atteint cette
ligne.
Vous vous demandez sûrement à quoi sert le paramètre de la clause catch. Il permet de connaître le type
d'exception qui doit être capturé. Et l'objet — ici, e — peut servir à préciser notre message grâce à l'appel
de la méthode getMessage(). Faites à nouveau ce test, en remplaçant l'instruction du catch par celle-ci
:
Avant de voir comment créer nos propres exceptions, sachez que le bloc permettant de capturer ces
dernières offre une fonctionnalité importante. En fait, vous avez sans doute compris que lorsqu'une ligne de
code lève une exception, l'instruction dans le bloc try est interrompue et le programme se rend dans le
bloc catchcorrespondant à l'exception levée.
Prenons un cas de figure très simple : imaginons que vous souhaitez effectuer une action, qu'une exception
soit levée ou non (nous verrons lorsque nous travaillerons avec les fichiers qu'il faut systématiquement
fermer ceux-ci). Java vous permet d'utiliser une clause via le mot clé finally. Voyons ce que donne ce
code :
Nous allons perfectionner un peu la gestion de nos objets Ville et Capitale. Je vous propose de mettre
en œuvre une exception de notre cru afin d'interdire l'instanciation d'un
objet Ville ou Capitaleprésentant un nombre négatif d'habitants.
La procédure pour faire ce tour de force est un peu particulière. En effet, nous devons :
throws : ce mot clé permet de signaler à la JVM qu'un morceau de code, une méthode, une
classe… est potentiellement dangereux et qu'il faut utiliser un bloc try{…}catch{…}. Il est
suivi du nom de la classe qui va gérer l'exception.
throw : celui-ci permet tout simplement de lever une exception manuellement en instanciant
un objet de type Exception (ou un objet hérité). Dans l'exemple de
notre ArithmeticException, il y a quelque part dans les méandres de Java un throw new
ArithmeticException().
Pour mettre en pratique ce système, commençons par créer une classe qui va gérer nos exceptions. Celle-ci,
je vous le rappelle, doit hériter d'Exception :
nomVille = pNom;
nomPays = pPays;
nbreHabitant = pNbre;
this.setCategorie();
}
}
throws NombreHabitantException nous indique que si une erreur est capturée, celle-ci sera traitée
en tant qu'objet de la classe NombreHabitantException, ce qui nous renseigne sur le type de l'erreur
en question. Elle indique aussi à la JVM que le constructeur de notre objet Ville est potentiellement
dangereux et qu'il faudra gérer les exceptions possibles.
Si la condition if(nbre < 0) est remplie, throw new NombreHabitantException(); instancie
la classe NombreHabitantException. Par conséquent, si un nombre d'habitants est négatif, l'exception
est levée.
Maintenant que vous avez apporté cette petite modification, retournez dans votre classe main, effacez son
contenu, puis créez un objet Ville de votre choix. Vous devez tomber sur une erreur persistante, comme à
la figure suivante ; c'est tout à fait normal et dû à l'instruction throws.
Exception
non gérée
Cela signifie qu'à partir de maintenant, vu les changements dans le constructeur, il vous faudra gérer les
exceptions qui pourraient survenir dans cette instruction avec un bloc try{…}catch{}.
Ainsi, pour que l'erreur disparaisse, il nous faut entourer notre instanciation avec un
bloctry{…}catch{…}, comme à la figure suivante.
Correction du bug
Vous pouvez constater que l'erreur a disparu, que notre code peut être compilé et qu'il s'exécute
correctement.
Attention, il faut que vous soyez préparés à une chose : le code que j'ai utilisé fonctionne très bien, mais il y
a un autre risque, l'instance de mon objet Ville a été déclarée dans le bloc try{…}catch{…} et cela
peut causer beaucoup de problèmes.
Ce code :
System.out.println(v.toString());
}
… ne fonctionnera pas, tout simplement parce que la déclaration de l'objet Ville est faite dans un sous-
bloc d'instructions, celui du bloc try{…}. Et rappelez-vous : une variable déclarée dans un bloc
d'instructions n'existe que dans celui-ci ! Ici, la variable v n'existe pas en dehors de l'instruction try{…}.
Pour pallier ce problème, il nous suffit de déclarer notre objet en dehors du bloc try{…} et de l'instancier à
l'intérieur :
System.out.println(v.toString());
}
Mais que se passera-t-il si nous déclarons une Ville avec un nombre d'habitants négatif pour tester notre
exception ? En remplaçant « 12000 » par « -12000 » dans l'instanciation de notre objet ? C'est simple : en
plus d'une exception levée pour le nombre d'habitants négatif, vous obtiendrez aussi
uneNullPointerException.
Ce qui signifie que si l'instanciation échoue dans notre bloc try{}, le programme plante ! Pour résoudre ce
problème, on peut utiliser une simple clause finally avec, à l'intérieur, l'instanciation d'un
objet Villepar défaut si celui-ci est null :
Maintenant que nous avons vu la création d'une exception, il serait de bon ton de pouvoir récolter plus de
renseignements la concernant. Par exemple, il serait peut-être intéressant de réafficher le nombre
d'habitants que l'objet a reçu. Pour ce faire, nous n'avons qu'à créer un deuxième constructeur dans notre
classeNombreHabitantException qui prend un nombre d'habitants en paramètre :
Ce n'est pas mal, avouez-le ! Sachez également que l'objet passé en paramètre de la clause catch a des
méthodes héritées de la classe Exception : vous pouvez les utiliser si vous le voulez et surtout, si vous en
avez l'utilité. Nous utiliserons certaines de ces méthodes dans les prochains chapitres. Je vais vous faire
peur : ici, nous avons capturé une exception, mais nous pouvons en capturer plusieurs !
Bien entendu, ceci est valable pour toutes sortes d'exceptions, qu'elles soient personnalisées ou inhérentes à
Java ! Supposons que nous voulons lever une exception si le nom de la ville fait moins de 3 caractères.
Nous allons répéter les premières étapes vues précédemment, c'est-à-dire créer une
classeNomVilleException:
Dans le code suivant, nous ajoutons une condition dans le constructeur Ville :
if(pNom.length() < 3)
throw new NomVilleException("le nom de la ville est inférieur à 3 caractères ! nom = "
+ pNom);
else
{
nbreInstance++;
nbreInstanceBis++;
nomVille = pNom;
nomPays = pPays;
nbreHabitant = pNbre;
this.setCategorie();
}
}
Vous remarquez que les différentes erreurs dans l'instruction throws sont séparées par une virgule. Nous
sommes maintenant parés pour la capture de deux exceptions personnalisées. Regardez comment on gère
deux exceptions sur une instruction :
Ville v = null;
try {
v = new Ville("Re", 12000, "France");
}
System.out.println(v.toString());
Constatez qu'un deuxième bloc catch{} s'est glissé… Eh bien, c'est comme cela que nous gérerons
plusieurs exceptions !
Si vous mettez un nom de ville de moins de 3 caractères et un nombre d'habitants négatif, c'est l'exception
du nombre d'habitants qui sera levée en premier, et pour cause : il s'agit de la première condition dans notre
constructeur. Lorsque plusieurs exceptions sont gérées par une portion de code, pensez bien à mettre les
blocs catch dans un ordre pertinent.
Encore une fois, Java 7 apporte une nouveauté : il est possible de catcher plusieurs exceptions dans
l'instruction catch. Ceci se fait grâce à l'opérateur « | » qui permet d'informer la JVM que le bloc de code
est susceptible d'engendrer plusieurs types d'exception. C'est vraiment simple à utiliser et cela vous permet
d'avoir un code plus compact. Voici à quoi ressemble l'exemple vu plus haut avec un catch multiple :
Lorsqu'un événement que la JVM ne sait pas gérer apparaît, une exception est levée (exemple :
division par zéro). Une exception correspond donc à une erreur.
Si une exception est levée dans le bloc try, les instructions figurant dans le bloc catch seront
exécutées pour autant que celui-ci capture la bonne exception levée.
Vous pouvez ajouter autant de blocs catch que vous le voulez à la suite d'un bloc try, mais
respectez l'ordre : du plus pertinent au moins pertinent.
Dans une classe objet, vous pouvez prévenir la JVM qu'une méthode est dite « à risque » grâce
au mot clé throws.
Vous pouvez définir plusieurs risques d'exceptions sur une même méthode. Il suffit de séparer
les déclarations par une virgule.
Dans cette méthode, vous pouvez définir les conditions d'instanciation d'une exception et lancer
cette dernière grâce au mot clé throw suivi de l'instanciation.
Une instanciation lancée par le biais de l'instruction throw doit être déclarée avec throws au
préalable !
LES ÉNUMÉRATIONS
Les énumérations constituent une notion nouvelle depuis Java 5. Ce sont des structures qui définissent une
liste de valeurs possibles. Cela vous permet de créer des types de données personnalisés. Nous allons par
exemple construire le type Langage qui ne peut prendre qu'un certain nombre de valeurs : JAVA, PHP, C,
etc.
Vous aurez sans doute besoin, un jour ou l'autre, de données permettant de savoir ce que vous devez faire.
Beaucoup de variables statiques dans Java servent à cela, vous le verrez bientôt dans une prochaine partie.
Bien sûr, vous pourriez créer un objet qui vous sert de paramètre de la méthode. Eh bien c'est à cela que
servent les enum : fabriquer ce genre d'objet de façon plus simple et plus rapide.
Vous constatez aussi qu'il n'y a pas de déclaration de portée, ni de type : les énumérations s'utilisent comme
des variables statiques déclarées public : on écrira par exemple Langage.JAVA. De plus, vous pouvez
recourir à la méthode values() retournant la liste des déclarations de l'énumération dont vous trouverez
un exemple à la figure suivante et sur son code :
À présent, étoffons tout cela en redéfinissant justement cette méthode. Pour ce faire, nous allons ajouter un
paramètre dans notre énumération, un constructeur et ladite méthode redéfinie. Voici notre nouvelle
énumération (résultat en figure suivante) :
//Constructeur
Langage(String name){
this.name = name;
}
Voici le code du début de chapitre, revu pour préférer les énumérations aux variables statiques :
//Constructeur
Langage(String name, String editor){
this.name = name;
this.editor = editor;
}
l1.getEditor();
l2.getEditor();
}
}
Voyons le résultat de cet exemple à la figure suivante.
Vous pouvez compléter les comportements des objets d'une énumération en ajoutant des
méthodes.
LES COLLECTIONS D'OBJETS
Voici un chapitre qui va particulièrement vous plaire. Nous allons voir que nous ne sommes pas obligés de
stocker nos données dans des tableaux ! Ces fameuses collections d'objets sont d'ailleurs dynamiques : en
gros, elles n'ont pas de taille prédéfinie. Il est donc impossible de dépasser leur capacité !
Je ne passerai pas en revue tous les types et tous les objets Collection car ils sont nombreux, mais
nous verrons les principaux d'entre eux. Les objets que nous allons aborder ici sont tous dans
lepackagejava.util. Facile à retenir, non ?
Ce chapitre vous sera d'une grande utilité, car les collections sont primordiales dans les programmes Java.
Avant de vous présenter certains objets, je me propose de vous présenter la hiérarchie d'interfaces
composant ce qu'on appelle les collections. Oui, vous avez bien lu, il s'agit bien d'interfaces : celles-ci
encapsulent la majeure partie des méthodes utilisables avec toutes les implémentations concrètes. Voici
un petit diagramme de classes sur la figure suivante schématisant cette hiérarchie.
Hiérarchie d'interfaces
Vous pouvez voir qu'il existe plusieurs types de collections, que les interfaces List et Set implémentent
directement l'interface Collection et que l'interface Map gravite autour de cette hiérarchie, tout en
faisant partie des collections Java.
En lisant la suite de ce chapitre, vous constaterez que ces interfaces ont des particularités correspondant
à des besoins spécifiques. Les objets de type List servent à stocker des objets sans condition particulière
sur la façon de les stocker. Ils acceptent toutes les valeurs, même les valeurs null. Les types Set sont un
peu plus restrictifs, car ils n'autorisent pas deux fois la même valeur (le même objet), ce qui est pratique
pour une liste d'éléments uniques, par exemple. Les Map sont particulières, car elles fonctionnent avec un
système clé - valeur pour ranger et retrouver les objets qu'elles contiennent.
Maintenant que je vous ai brièvement expliqué les différences entre ces types, voyons comment utiliser
ces objets.
Les objets appartenant à la catégorie List sont, pour simplifier, des tableaux extensibles à volonté. On y
trouve les objets Vector, LinkedList et ArrayList. Vous pouvez y insérer autant d'éléments que
vous le souhaitez sans craindre de dépasser la taille de votre tableau. Ils fonctionnent tous de la même
manière : vous pouvez récupérer les éléments de la liste via leurs indices. De plus, les List contiennent
des objets. Je vous propose de voir deux objets de ce type qui, je pense, vous seront très utiles.
L'objet LinkedList
Une liste chaînée (LinkedList en anglais) est une liste dont chaque élément est lié aux éléments adjacents
par une référence à ces derniers. Chaque élément contient une référence à l'élément précédent et à l'élément
suivant, exceptés le premier, dont l'élément précédent vaut null, et le dernier, dont l'élément suivant vaut
également null.
La figure suivante représente un un schéma qui vous permettra de mieux vous représenter le
fonctionnement de cet objet :
Fonctionnement de la LinkedList
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
Il y a autre chose que vous devez savoir sur ce genre d'objet : ceux-ci implémentent l'interface Iterator.
Ainsi, nous pouvons utiliser cette interface pour lister notre LinkedList.
Un itérateur est un objet qui a pour rôle de parcourir une collection. C'est d'ailleurs son unique raison
d'être. Pour être tout à fait précis, l'utilisation des itérateurs dans Java fonctionne de la même manière
que lepattern du même nom. Tout comme nous avons pu le voir avec la pattern strategy, les design
patterns sont en fait des modèles de conception d'objets permettant une meilleure stabilité et une
réutilisabilité accrue. Les itérateurs en font partie.
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
while(li.hasNext())
System.out.println(li.next());
}
}
Les deux manières de procéder sont analogues !
Attention, je dois vous dire quelque chose sur les listes chaînées : vu que tous les éléments contiennent
une référence à l'élément suivant, de telles listes risquent de devenir particulièrement lourdes en
grandissant ! Cependant, elles sont adaptées lorsqu'il faut beaucoup manipuler une collection en
supprimant ou en ajoutant des objets en milieu de liste. Elles sont donc à utiliser avec précaution.
L'objet ArrayList
Voici un objet bien pratique. ArrayList est un de ces objets qui n'ont pas de taille limite et qui, en plus,
acceptent n'importe quel type de données, y compris null ! Nous pouvons mettre tout ce que nous
voulons dans un ArrayList, voici un morceau de code qui le prouve :
import java.util.ArrayList;
Contrairement aux LinkedList, les ArrayList sont rapides en lecture, même avec un gros volume
d'objets. Elles sont cependant plus lentes si vous devez ajouter ou supprimer des données en milieu de liste.
Pour résumer à l'extrême, si vous effectuez beaucoup de lectures sans vous soucier de l'ordre des éléments,
optez pour une ArrayList ; en revanche, si vous insérez beaucoup de données au milieu de la liste, optez
pour une Linkedlist.
Une collection de type Map est une collection qui fonctionne avec un couple clé - valeur. On y trouve les
objets Hashtable, HashMap, TreeMap, WeakHashMap… La clé, qui sert à identifier une entrée dans
notre collection, est unique. La valeur, au contraire, peut être associée à plusieurs clés.
Ces objets ont comme point faible majeur leur rapport conflictuel avec la taille des données à stocker. En
effet, plus vous aurez de valeurs à mettre dans un objet Map, plus celles-ci seront lentes et lourdes :
logique, puisque par rapport aux autres collections, il stocke une donnée supplémentaire par
enregistrement. Une donnée c'est de la mémoire en plus et, même si les ordinateurs actuels en ont
énormément, gardez en tête que « la mémoire, c'est sacré » (je vous rappelle que les applications Java ne
sont pas forcément destinées aux appareils bénéficiant de beaucoup de mémoire).
L'objet Hashtable
Vous pouvez également dire « table de hachage », si vous traduisez mot à mot… On parcourt cet objet
grâce aux clés qu'il contient en recourant à la classe Enumeration. L'objet Enumeration contient
notreHashtable et permet de le parcourir très simplement. Regardez, le code suivant insère les quatre
saisons avec des clés qui ne se suivent pas, et notre énumération récupère seulement les valeurs :
import java.util.Enumeration;
import java.util.Hashtable;
Enumeration e = ht.elements();
while(e.hasMoreElements())
System.out.println(e.nextElement());
}
}
Cet objet nous offre lui aussi tout un panel de méthodes utiles :
isEmpty() retourne « vrai » si l'objet est vide ;
containsKey(Object key) retourne « vrai » si la clé passée en paramètre est présente dans
laHashtable ;
put(Object key, Object value) ajoute le couple key - value dans l'objet ;
De plus, il faut savoir qu'un objet Hashtable n'accepte pas la valeur null et qu'il est Thread Safe,
c'est-à-dire qu'il est utilisable dans plusieurs threads (cela signifie que plusieurs éléments de votre
programme peuvent l'utiliser simultanément ; nous y reviendrons) simultanément sans qu'il y ait un risque
de conflit de données.
L'objet HashMap
En fait, les deux objets de type Map sont, à peu de choses près, équivalents.
Un Set est une collection qui n'accepte pas les doublons. Par exemple, elle n'accepte qu'une seule
foisnull, car deux valeurs null sont considérées comme un doublon. On trouve parmi les Set les
objetsHashSet, TreeSet, LinkedHashSet… Certains Set sont plus restrictifs que d'autres : il en
existe qui n'acceptent pas null, certains types d'objets, etc.
Les Set sont particulièrement adaptés pour manipuler une grande quantité de données. Cependant, les
performances de ceux-ci peuvent être amoindries en insertion. Généralement, on opte pour
un HashSet, car il est plus performant en temps d'accès, mais si vous avez besoin que votre collection
soit constamment triée, optez pour un TreeSet.
L'objet HashSet
C'est sans nul doute la plus utilisée des implémentations de l'interface Set. On peut parcourir ce type de
collection avec un objet Iterator ou extraire de cet objet un tableau d'Object :
import java.util.HashSet;
import java.util.Iterator;
Iterator it = hs.iterator();
while(it.hasNext())
System.out.println(it.next());
Voilà ! Nous avons vu quelque chose d'assez intéressant que nous pourrons utiliser dans peu de temps, mais
avant, nous avons encore du pain sur la planche. Dans le chapitre suivant nous verrons d'autres aspects de
nos collections.
Il y a principalement trois types de collection : les List, les Set et les Map.
Si vous insérez fréquemment des données en milieu de liste, utilisez une LinkedList.
Si vous voulez rechercher ou accéder à une valeur via une clé de recherche, optez pour une
collection de type Map.
Si vous avez une grande quantité de données à traiter, tournez-vous vers une liste de type Set.
LA GÉNÉRICITÉ EN JAVA
Pour assimiler ce concept, ajouté au JDK depuis la version 1.5, nous allons essentiellement travailler avec
des exemples tout au long de ce chapitre. Le principe de la généricité est de faire des classes qui
n'acceptent qu'un certain type d'objets ou de données de façon dynamique !
Avec ce que nous avons appris au chapitre précédent, vous avez sûrement poussé un soupir de
soulagement lorsque vous avez vu que ces objets acceptent tous les types de données. Par contre, un
problème de taille se pose : lorsque vous voudrez travailler avec ces données, vous allez devoir faire
un cast ! Et peut-être même un cast de cast, voire un cast de cast de cast…
C'est là que se situe le problème… Mais comme je vous le disais, depuis la version 1.5 du JDK, la généricité
est là pour vous aider !
Principe de base
Bon, pour vous montrer la puissance de la généricité, nous allons tout de suite voir un cas de classe qui ne
l'utilise pas.
Il existe un exemple très simple que vous pourrez retrouver aisément sur Internet, car il s'agit d'un des cas
les plus faciles permettant d'illustrer les bases de la généricité. Nous allons coder une classe Solo. Celle-ci
va travailler avec des références de type String. Voici le diagramme de classe de cette dernière en
figure suivante.
Objet générique
Et voici son code :
//Variable d'instance
private T valeur;
En effet, lorsque vous déclarez une variable de type primitif, vous pouvez utiliser ses classes enveloppes
(on parle aussi de classe wrapper) ; elles ajoutent les méthodes de la classe Object à vos types primitifs
ainsi que des méthodes permettant de caster leurs valeurs, etc. À ceci, je dois ajouter que depuis Java 5,
est géré ce qu'on appelle l'autoboxing, une fonctionnalité du langage permettant de transformer
automatiquement un type primitif en classe wrapper (on appelle ça le boxing) et inversement, c'est-à-
dire une classe wrapper en type primitif (ceci s'appelle l'unboxing). Ces deux fonctionnalités forment
l'autoboxing. Par exemple :
Vous devez savoir que la généricité peut être multiple ! Nous avons créé une classe Solo, mais rien ne
vous empêche de créer une classe Duo, qui elle prend deux paramètres génériques ! Voilà le code source
de cette classe :
//Retourne la valeur T
public T getValeur1() {
return valeur1;
}
//Définit la valeur T
public void setValeur1(T valeur1) {
this.valeur1 = valeur1;
}
//Retourne la valeur S
public S getValeur2() {
return valeur2;
}
//Définit la valeur S
public void setValeur2(S valeur2) {
this.valeur2 = valeur2;
}
}
Vous voyez que cette classe prend deux types de références qui ne sont pas encore définis.
Afin de mieux comprendre son fonctionnement, voici un code que vous pouvez tester :
Test de la
classe Duo
Vous voyez qu'il n'y a rien de bien méchant ici. Ce principe fonctionne exactement comme dans l'exemple
précédent. La seule différence réside dans le fait qu'il n'y a pas un, mais deux paramètres génériques !
Attends une minute… Lorsque je déclare une référence de type Duo<String, Boolean>, je ne peux
plus la changer en un autre type !
En fait, non. Si vous faites :
Généricité et collections
Vous pouvez aussi utiliser la généricité sur les objets servant à gérer des collections. C'est même l'un des
points les plus utiles de la généricité !
En effet, lorsque vous listiez le contenu d'un ArrayList par exemple, vous n'étiez jamais sûrs à 100 %
du type de référence sur lequel vous alliez tomber (normal, puisqu'un ArrayList accepte tous les types
d'objets)… Eh bien ce calvaire est terminé et le polymorphisme va pouvoir réapparaître, plus puissant que
jamais !
System.out.println("Liste de String");
System.out.println("------------------------------");
List<String> listeString= new ArrayList<String>();
listeString.add("Une chaîne");
listeString.add("Une autre");
listeString.add("Encore une autre");
listeString.add("Allez, une dernière");
System.out.println("\nListe de float");
System.out.println("------------------------------");
for(float f : listeFloat)
System.out.println(f);
}
Voyez le résultat de ce code sur la figure suivante.
ArrayList et généricité
La généricité sur les listes est régie par les lois vues précédemment : pas de type float dans
unArrayList<String>.
Vu qu'on y va crescendo, on pimente à nouveau le tout !
Héritage et généricité
Là où les choses sont pernicieuses, c'est quand vous employez des classes usant de la généricité avec des
objets comprenant la notion d'héritage ! L'héritage dans la généricité est l'un des concepts les plus
complexes en Java. Pourquoi ? Tout simplement parce qu'il va à l'encontre de ce que vous avez appris
jusqu'à présent…
Nous avons une classe Voiture dont hérite une autre classe VoitureSansPermis, ce qui nous
donnerait le diagramme représenté à la figure suivante.
Hiérarchie de classes
Jusque-là, c'est simplissime. Maintenant, ça se complique :
Imaginez deux secondes que l'instruction interdite soit permise ! Dans listVoiture, vous avez le
contenu de la liste des voitures sans permis, et rien ne vous empêche d'y ajouter une voiture. Là où le
problème prend toute son envergure, c'est lorsque vous voudrez sortir toutes les voitures sans permis de
votre variablelistVoiture. Eh oui ! Vous y avez ajouté une voiture ! Lors du balayage de la liste, vous
aurez, à un moment, une référence de type VoitureSansPermis à laquelle vous tentez d'affecter une
référence de type Voiture. Voilà pourquoi ceci est interdit.
Une des solutions consiste à utiliser le wildcard : « ? ». Le fait de déclarer une collection avec
lewildcard, comme ceci :
ArrayList<?> list;
… revient à indiquer que notre collection accepte n'importe quel type d'objet. Cependant, nous allons voir
un peu plus loin qu'il y a une restriction.
Je vais maintenant vous indiquer quelque chose d'important. Avec la généricité, vous pouvez aller encore
plus loin. Nous avons vu comment restreindre le contenu d'une de nos listes, mais nous pouvons aussi
l’élargir ! Si je veux par exemple qu'un ArrayList puisse avoir toutes les instances de Voiture et de ses
classes filles… comment faire ?
Ce qui suit s'applique aussi aux interfaces susceptibles d'être implémentées par une classe !
Attention les yeux, ça pique :
//Méthode générique !
static void afficher(ArrayList<? extends Voiture> list){
for(Voiture v : list)
System.out.println(v.toString());
}
Eh, attends ! On a voulu ajouter des objets dans notre collection et le programme ne compile plus !
Oui… Ce que je ne vous avais pas dit, c'est que dès que vous utilisez le wildcard, vos listes sont
verrouillées en insertion : elles se transforment en collections en lecture seule..
En fait, il faut savoir que c'est à la compilation du programme que Java ne vous laisse pas faire :
lewildcard signifie « tout objet », et dès l'utilisation de celui-ci, la JVM verrouillera la compilation du
programme afin de prévenir les risques d'erreurs. Dans notre exemple, il est combiné
avec extends(signifiant héritant), mais cela n'a pas d'incidence directe : c'est le wildcard la cause du
verrou (un objet générique comme notre objet Solo déclaré Solo<?> solo; sera également bloqué en
écriture).
//Liste de voiture
List<Voiture> listVoiture = new ArrayList<Voiture>();
listVoiture.add(new Voiture());
listVoiture.add(new Voiture());
affiche(listVoiture);
affiche(listVoitureSP);
}
//Avec cette méthode, on accepte aussi bien les collections de Voiture que les collection
de VoitureSansPermis
static void affiche(List<? extends Voiture> list){
for(Voiture v : list)
System.out.print(v.toString());
}
Avant que vous ne posiez la question, non, déclarer la
méthode affiche(List<Voiture> list) {…} ne vous permet pas de parcourir des listes
de VoitureSansPermis, même si celle-ci hérite de la classeVoiture.
Les méthodes déclarées avec un type générique sont verrouillées afin de n'être utilisées qu'avec ce type bien
précis, toujours pour les mêmes raisons ! Attendez : ce n'est pas encore tout. Nous avons vu comment
élargir le contenu de nos collections (pour la lecture), nous allons voir comment restreindre les collections
acceptées par nos méthodes.
La méthode :
La signification de l'instruction suivante est donc que la méthode autorise un objet de type List de
n'importe quelle superclasse de la classe Voiture (y compris Voiture elle-même).
affiche(listVoiture);
}
//Avec cette méthode, on accepte aussi bien les collections de Voiture que les collections
d'Object : superclasse de toutes les classes
import java.util.ArrayList;
import java.util.List;
La généricité est un concept très utile pour développer des objets travaillant avec plusieurs types de
données.
Vous passerez donc moins de temps à développer des classes traitant de façon identique des
données différentes.
Le wildcard (?) permet d'indiquer que n'importe quel type peut être traité et donc accepté !
Dès que le wildcard (?) est utilisé, cela revient à rendre ladite collection en lecture seule !
Vous pouvez élargir le champ d'acceptation d'une collection générique grâce au mot-clé extends.
L'instruction ? extends MaClasse autorise toutes les collections de classes ayant pour
supertypeMaClasse.
L'instruction ? super MaClasse autorise toutes les collections de classes ayant pour
typeMaClasse et tous ses supertypes !
Pour ce genre de cas, les méthodes génériques sont particulièrement adaptées et permettent
d'utiliser le polymorphisme dans toute sa splendeur !
Une entrée/sortie en Java consiste en un échange de données entre le programme et une autre source,
par exemple la mémoire, un fichier, le programme lui-même… Pour réaliser cela, Java emploie ce qu'on
appelle un stream (qui signifie « flux »). Celui-ci joue le rôle de médiateur entre la source des données et
sa destination. Nous allons voir que Java met à notre disposition toute une panoplie d'objets permettant
de communiquer de la sorte. Toute opération sur les entrées/sorties doit suivre le schéma suivant :
ouverture, lecture, fermeture du flux.
Je ne vous cache pas qu'il existe une foule d’objets qui ont chacun leur façon de travailler avec les flux.
Sachez que Java a décomposé les objets traitant des flux en deux catégories :
les objets travaillant avec des flux d'entrée (in), pour la lecture de flux ;
les objets travaillant avec des flux de sortie (out), pour l'écriture de flux.
Utilisation de java.io
L'objet File
Avant de commencer, créez un fichier avec l'extension que vous voulez et enregistrez-le à la racine de votre
projet Eclipse. Personnellement, je me suis fait un fichier test.txt dont voici le contenu :
if((i%4) == 0){
System.out.print("\n");
}
i++;
}
System.out.println("\n");
} catch (NullPointerException e) {
//L'instruction peut générer une NullPointerException
//s'il n'y a pas de sous-fichier !
}
}
}
}
Le résultat est bluffant (voir figure suivante) !
Test de l'objet File
Vous conviendrez que les méthodes de cet objet peuvent s'avérer très utiles ! Nous venons d'en essayer
quelques-unes et nous avons même listé les sous-fichiers et sous-dossiers de nos lecteurs à la racine du
PC.
Vous pouvez aussi effacer le fichier grâce la méthode delete(), créer des répertoires avec la
méthodemkdir() (le nom donné à ce répertoire ne pourra cependant pas contenir de point (« . »)) etc.
Maintenant que vous en savez un peu plus sur cet objet, nous pouvons commencer à travailler avec notre
fichier !
C'est par le biais des objets FileInputStream et FileOutputStream que nous allons pouvoir :
Ces classes héritent des classes abstraites InputStream et OutputStream, présentes dans le
packagejava.io.
Comme vous l'avez sans doute deviné, il existe une hiérarchie de classes pour les traitements in et une
autre pour les traitements out. Ne vous y trompez pas, les classes héritant d'InputStream sont
destinées à la lecture et les classes héritant d'OutputStream se chargent de l'écriture !
Vous auriez dit le contraire ? Comme beaucoup de gens au début. Mais c'est uniquement parce que vous
situez les flux par rapport à vous, et non à votre programme ! Lorsque ce dernier va lire des informations
dans un fichier, ce sont des informations qu'il reçoit, et par conséquent, elles s'apparentent à une entrée
: in(sachez tout de même que lorsque vous tapez au clavier, cette action est considérée comme un flux
d'entrée !).
Nous allons enfin commencer à travailler avec notre fichier. Le but est d'aller en lire le contenu et de le
copier dans un autre, dont nous spécifierons le nom dans notre programme, par le biais d'un programme
Java.
try {
// On instancie nos objets :
// fis va lire le fichier
// fos va écrire dans le nouveau !
fis = new FileInputStream(new File("test.txt"));
fos = new FileOutputStream(new File("test2.txt"));
}
System.out.println("Copie terminée !");
} catch (FileNotFoundException e) {
// Cette exception est levée si l'objet FileInputStream ne trouve
// aucun fichier
e.printStackTrace();
} catch (IOException e) {
// Celle-ci se produit lors d'une erreur d'écriture ou de lecture
e.printStackTrace();
} finally {
// On ferme nos flux de données dans un bloc finally pour s'assurer
// que ces instructions seront exécutées dans tous les cas même si
// une exception est levée !
try {
if (fis != null)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Pour que l'objet FileInputStream fonctionne, le fichier doit exister ! Sinon
l'exceptionFileNotFoundException est levée. Par contre, si vous ouvrez un flux en écriture
(FileOutputStream) vers un fichier inexistant, celui-ci sera créé automatiquement !
Notez bien les imports pour pouvoir utiliser ces objets. Mais comme vous le savez déjà, vous pouvez taper
votre code et faire ensuite CTRL + SHIFT + O pour que les imports soient automatiques.
À l'exécution de ce code, vous pouvez voir que le fichier test2.txt a bien été créé et qu'il contient
exactement la même chose que test.txt ! De plus, j'ai ajouté dans la console les données que votre
programme va utiliser (lecture et écriture).
Le bloc finally permet de s'assurer que nos objets ont bien fermé leurs liens avec leurs fichiers
respectifs, ceci afin de permette à Java de détruire ces objets pour ainsi libérer un peu de mémoire à votre
ordinateur.
En effet, les objets utilisent des ressources de votre ordinateur que Java ne peut pas libérer de lui-même,
vous devez être sûr que la vanne est fermée ! Ainsi, même si une exception est levée, le contenu du
bloc finally sera exécuté et nos ressources seront libérées. Par contre, pour alléger la lecture, je ne
mettrai plus ces blocs dans les codes à venir mais pensez bien à les mettre dans vos codes.
Les objets FileInputStream et FileOutputStream sont assez rudimentaires, car ils travaillent avec
un nombre déterminé d'octets à lire. Cela explique pourquoi ma condition de boucle était si tordue…
Lorsque vous voyez des caractères dans un fichier ou sur votre écran, ils ne veulent pas dire grand-chose
pour votre PC, car il ne comprend que le binaire (vous savez, les suites de 0 et de 1). Ainsi, afin de pouvoir
afficher et travailler avec des caractères, un système d'encodage (qui a d'ailleurs fort évolué) a été mis au
point.
Sachez que chaque caractère que vous saisissez ou que vous lisez dans un fichier correspond à un code
binaire, et ce code binaire correspond à un code décimal. Voyez la table de correspondance (on parle de la
table ASCII).
Cependant, au début, seuls les caractères de a à z, de A à Z et les chiffres de 0 à 9 (les 127 premiers
caractères de la table ASCII) étaient codés (UNICODE 1), correspondant aux caractères se trouvant dans la
langue anglaise. Mais ce codage s'est rapidement avéré trop limité pour des langues comportant des
caractères accentués (français, espagnol…). Un jeu de codage de caractères étendu a donc été mis en place
afin de pallier ce problème.
Chaque code binaire UNICODE 1 est codé sur 8 bits, soit 1 octet. Une variable de type byte, en Java,
correspond en fait à 1 octet et non à 1 bit !
Les objets que nous venons d'utiliser emploient la première version d'UNICODE 1 qui ne comprend pas les
caractères accentués, c'est pourquoi ces caractères ont un code décimal négatif dans notre fichier. Lorsque
nous définissons un tableau de byte à 8 entrées, cela signifie que nous allons lire 8 octets à la fois.
Vous pouvez voir qu'à chaque tour de boucle, notre tableau de byte contient huit valeurs correspondant
chacune à un code décimal qui, lui, correspond à un caractère (valeur entre parenthèses à côté du code
décimal).
Vous pouvez voir que les codes décimaux négatifs sont inconnus, car ils sont représentés par des « ? » ; de
plus, il y a des caractères invisibles (les 32 premiers caractères de la table ASCII sont invisibles !) dans
notre fichier :
Vous voyez que les traitements des flux suivent une logique et une syntaxe précises ! Lorsque nous avons
copié notre fichier, nous avons récupéré un certain nombre d'octets dans un flux entrant que nous avons
passé à un flux sortant. À chaque tour de boucle, les données lues dans le fichier source sont écrites dans le
fichier défini comme copie.
Il existe à présent des objets beaucoup plus faciles à utiliser, mais qui travaillent néanmoins avec les deux
objets que nous venons d'étudier. Ces objets font également partie de la hiérarchie citée précédemment.
Seulement, il existe une superclasse qui les définit.
Ces deux classes sont en fait des classes abstraites. Elles définissent un comportement global pour leurs
classes filles qui, elles, permettent d'ajouter des fonctionnalités aux flux d'entrée/sortie !
Vous pouvez voir qu'il existe quatre classes filles héritant de FilterInputStream (de même
pourFilterOutputStream (les classes dérivant de FilterOutputStream ont les mêmes
fonctionnalités, mais en écriture)):
Ces classes prennent en paramètre une instance dérivant des classes InputStream(pour les classes
héritant de FilterInputStream) ou de OutputStream (pour les classes héritant
deFilterOutputStream).
Puisque ces classes acceptent une instance de leur superclasse en paramètre, vous pouvez cumuler les
filtres et obtenir des choses de ce genre :
Télécharger le fichier
//On réinitialise
startTime = System.currentTimeMillis();
//Inutile d'effectuer des traitements dans notre boucle
while(bis.read(buf) != -1);
//On réaffiche
System.out.println("Temps de lecture avec BufferedInputStream : " +
System.currentTimeMillis() - startTime));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Et le résultat, visible à la figure suivante, est encore une fois bluffant.
La différence de temps est vraiment énorme : 1,578 seconde pour la première méthode et 0,094 seconde
pour la deuxième ! Vous conviendrez que l'utilisation d'un buffer permet une nette amélioration des
performances de votre code. Faisons donc sans plus tarder le test avec l’écriture :
try {
fis = new FileInputStream(new File("test.txt"));
fos = new FileOutputStream(new File("test2.txt"));
bis = new BufferedInputStream(new FileInputStream(new File("test.txt")));
bos = new BufferedOutputStream(new FileOutputStream(new File("test3.txt")));
byte[] buf = new byte[8];
while(fis.read(buf) != -1){
fos.write(buf);
}
//On affiche le temps d'exécution
System.out.println("Temps de lecture + écriture avec FileInputStream et
FileOutputStream : " + (System.currentTimeMillis() - startTime));
//On réinitialise
startTime = System.currentTimeMillis();
while(bis.read(buf) != -1){
bos.write(buf);
}
//On réaffiche
System.out.println("Temps de lecture + écriture avec BufferedInputStream et
BufferedOutputStream : " + (System.currentTimeMillis() - startTime));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Là, la différence est encore plus nette, comme le montre la figure suivante.
Je ne vais pas passer en revue tous les objets cités un peu plus haut, mais vu que vous risquez d’avoir
besoin des objets Data(Input/Output)Stream, nous allons les aborder rapidement, puisqu'ils
s'utilisent comme les objets BufferedInputStream. Je vous ai dit plus haut que ceux-ci ont des
méthodes de lecture pour chaque type primitif : il faut cependant que le fichier soit généré par le biais
d'unDataOutputStream pour que les méthodes fonctionnent correctement.
Nous allons donc créer un fichier de toutes pièces pour le lire par la suite.
System.out.println(dis.readBoolean());
System.out.println(dis.readByte());
System.out.println(dis.readChar());
System.out.println(dis.readDouble());
System.out.println(dis.readFloat());
System.out.println(dis.readInt());
System.out.println(dis.readLong());
System.out.println(dis.readShort());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
La figure suivante correspond au résultat de ce code.
Le code est simple, clair et concis. Vous avez pu constater que ce type d'objet ne manque pas de
fonctionnalités ! Jusqu'ici, nous ne travaillions qu'avec des types primitifs, mais il est également possible de
travailler avec des objets !
Vous devez savoir que lorsqu'on veut écrire des objets dans des fichiers, on appelle ça la « sérialisation » :
c'est le nom que porte l'action de sauvegarder des objets ! Cela fait quelque temps déjà que vous utilisez
des objets et, j'en suis sûr, vous avez déjà souhaité que certains d'entre eux soient réutilisables. Le
moment est venu de sauver vos objets d'une mort certaine ! Pour commencer, nous allons voir comment
sérialiser un objet de notre composition.
Voici la classe avec laquelle nous allons travailler :
//Package à importer
import java.io.Serializable;
Vous avez sûrement déjà senti comment vous allez vous servir de ces objets, mais travaillons tout de même
sur l’exemple que voici :
try {
System.out.println("Affichage des jeux :");
System.out.println("*************************\n");
System.out.println(((Game)ois.readObject()).toString());
System.out.println(((Game)ois.readObject()).toString());
System.out.println(((Game)ois.readObject()).toString());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
ois.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
La désérialisation d'un objet peut engendrer une ClassNotFoundException, pensez donc à la capturer !
Et voyez le résultat en figure suivante.
Sérialisation — désérialisation
Ce qu'il se passe est simple : les données de vos objets sont enregistrées dans le fichier. Mais que se
passerait-il si notre objet Game avait un autre objet de votre composition en son sein ? Voyons ça tout de
suite. Créez la classe Notice comme suit :
import java.io.Serializable;
Erreur de sérialisation
Eh non, votre code ne compile plus ! Il y a une bonne raison à cela : votre objet Notice n'est pas
sérialisable, une erreur de compilation est donc levée. Maintenant, deux choix s'offrent à vous :
1. soit vous faites en sorte de rendre votre objet sérialisable ;
2. soit vous spécifiez dans votre classe Game que la variable notice n'a pas à être sérialisée.
Pour la première option, c'est simple, il suffit d'implémenter l'interface sérialisable dans notre
classeNotice. Pour la seconde, il suffit de déclarer votre variable : transient ; comme ceci :
import java.io.Serializable;
CharArray(Writer/Reader) ;
String(Writer/Reader).
Ces deux types jouent quasiment le même rôle. De plus, ils ont les mêmes méthodes que leur classe mère.
Ces deux objets n'ajoutent donc aucune nouvelle fonctionnalité à leur objet mère.
Leur principale fonction est de permettre d'écrire un flux de caractères dans un buffer adaptatif : un
emplacement en mémoire qui peut changer de taille selon les besoins (nous n'en avons pas parlé dans le
chapitre précédent afin de ne pas l'alourdir, mais il existe des classes remplissant le même rôle que ces
classes-ci : ByteArray(Input/Output)Stream).
try {
caw.write("Coucou les Zéros");
//Appel à la méthode toString de notre objet de manière tacite
System.out.println(caw);
System.out.println(str);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Je vous laisse le soin d'examiner ce code ainsi que son effet. Il est assez commenté pour que vous en
compreniez toutes les subtilités. L'objet String(Writer/Reader) fonctionne de la même façon :
try {
sw.write("Coucou les Zéros");
//Appel à la méthode toString de notre objet de manière tacite
System.out.println(sw);
} catch (IOException e) {
e.printStackTrace();
}
}
}
En fait, il s'agit du même code, mais avec des objets différents ! Vous savez à présent comment écrire un
flux de texte dans un tampon de mémoire. Je vous propose maintenant de voir comment traiter les fichiers
de texte avec des flux de caractères.
Comme nous l'avons vu, les objets travaillant avec des flux utilisent des flux binaires.
La conséquence est que même si vous ne mettez que des caractères dans un fichier et que vous le
sauvegardez, les objets étudiés précédemment traiteront votre fichier de la même façon que s’il contenait
des données binaires ! Ces deux objets, présents dans le package java.io, servent à lire et écrire des
données dans un fichier texte.
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
try {
//Création de l'objet
fw = new FileWriter(file);
String str = "Bonjour à tous, amis Zéros !\n";
str += "\tComment allez-vous ? \n";
//On écrit la chaîne
fw.write(str);
//On ferme le flux
fw.close();
//Affichage
System.out.println(str);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Vous pouvez voir que l'affichage est bon et qu'un nouveau fichier (la lecture d'un fichier inexistant entraîne
l’exception FileNotFoundException, et l'écriture peut entraîner une IOException) vient de faire
son apparition dans le dossier contenant votre projet Eclipse !
Depuis le JDK 1.4, un nouveau package a vu le jour, visant à améliorer les performances des flux, buffers,
etc. traités par java.io. En effet, vous ignorez probablement que le package que nous explorons depuis
le début existe depuis la version 1.1 du JDK. Il était temps d'avoir une remise à niveau afin d'améliorer les
résultats obtenus avec les objets traitant les flux. C'est là que le package java.nio a vu le jour !
Utilisation de java.nio
Vous l'avez sûrement deviné, nio signifie « New I/O ». Comme je vous l'ai dit précédemment, ce package
a été créé afin d'améliorer les performances sur le traitement des fichiers, du réseau et des buffers. Il permet
de lire les données (nous nous intéresserons uniquement à l'aspect fichier) d'une façon différente. Vous
avez constaté que les objets du package java.io traitaient les données par octets. Les objets du
packagejava.nio, eux, les traitent par blocs de données : la lecture est donc accélérée !
Tout repose sur deux objets de ce nouveau package : les channels et les buffers. Les channels sont en fait
des flux, tout comme dans l'ancien package, mais ils sont amenés à travailler avec un buffer dont vous
définissez la taille. Pour simplifier au maximum, lorsque vous ouvrez un flux vers un fichier avec un
objetFileInputStream, vous pouvez récupérer un canal vers ce fichier. Celui-ci, combiné à un buffer,
vous permettra de lire votre fichier encore plus vite qu'avec un BufferedInputStream !
Reprenez le gros fichier que je vous ai fait créer dans la sous-section précédente : nous allons maintenant le
relire avec ce nouveau package en comparant le buffer conventionnel et la nouvelle façon de faire.
try {
//Création des objets
fis = new FileInputStream(new File("test.txt"));
bis = new BufferedInputStream(fis);
//Démarrage du chrono
long time = System.currentTimeMillis();
//Lecture
while(bis.read() != -1);
//Temps d'exécution
System.out.println("Temps d'exécution avec un buffer conventionnel : " +
(System.currentTimeMillis() - time));
//Démarrage du chrono
time = System.currentTimeMillis();
//Démarrage de la lecture
fc.read(bBuff);
//On prépare à la lecture avec l'appel à flip
bBuff.flip();
//Affichage du temps d'exécution
System.out.println("Temps d'exécution avec un nouveau buffer : " +
(System.currentTimeMillis() - time));
//Puisque nous avons utilisé un buffer de byte afin de récupérer les données
//Nous pouvons utiliser un tableau de byte
//La méthode array retourne un tableau de byte
byte[] tabByte = bBuff.array();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
La figure suivante vous montre le résultat.
Vous constatez que les gains en performance ne sont pas négligeables. Sachez aussi que ce nouveau
package est le plus souvent utilisé pour traiter les flux circulant sur les réseaux. Je ne m'attarderai pas sur le
sujet, mais une petite présentation est de mise. Ce package offre un buffer par type primitif pour la lecture
sur le channel, vous trouverez donc ces classes :
IntBuffer ;
CharBuffer ;
ShortBuffer ;
ByteBuffer ;
DoubleBuffer ;
FloatBuffer ;
LongBuffer.
Je ne l'ai pas fait durant tout le chapitre afin d'alléger un peu les codes, mais si vous voulez être sûrs que
votre flux est bien fermé, utilisez la clause finally. Par exemple, faites comme ceci :
try {
//On travaille avec nos objets
} catch (FileNotFoundException e) {
//Gestion des exceptions
} catch (IOException e) {
//Gestion des exceptions
}
finally{
if(ois != null)ois.close();
if(oos != null)oos.close();
}
}
}
Avec l'arrivée de Java 7, quelques nouveautés ont vu le jour pour la gestion des exceptions sur les flux.
Contrairement à la gestion de la mémoire (vos variables, vos classes, etc.) qui est déléguée au garbage
collector (ramasse miette), plusieurs types de ressources doivent être gérées manuellement. Les flux sur des
fichiers en font parti mais, d'un point de vue plus général, toutes les ressources que vous devez fermer
manuellement (les flux réseaux, les connexions à une base de données…). Pour ce genre de flux, vous avez
vu qu'il vous faut déclarer une variable en dehors d'un bloc try{…}catch{…} afin qu'elle soit accessible
dans les autres blocs d'instructions, le bloc finally par exemple.
Java 7 initie ce qu'on appelle vulgairement le « try-with-resources ». Ceci vous permet de déclarer les
ressources utilisées directement dans le bloc try(…), ces dernières seront automatiquement fermées à la
fin du bloc d'instructions ! Ainsi, si nous reprenons notre code de début de chapitre qui copie notre
fichiertest.txt vers test2.txt, nous aurons ceci :
System.out.println("");
}
L'une des grandes nouveautés de Java 7 réside dans NIO.2 avec un nouveau package java.nio.file en
remplacement de la classe java.io.File. Voici un bref listing de quelques nouveautés :
une meilleure gestion des exceptions : la plupart des méthodes de la classe File se contentent de
renvoyer une valeur nulle en cas de problème, avec ce nouveau package, des exceptions seront
levées permettant de mieux cibler la cause du (ou des) problème(s) ;
l'ajout de méthodes utilitaires tels que le déplacement/la copie de fichier, la lecture/écriture binaire
ou texte…
Je vous propose maintenant de jouer avec quelques nouveautés. Commençons par le commencement : ce
qui finira par remplacer la classe File. Afin d'être le plus souple et complet possible, les développeurs de
la plateforme ont créé une interface java.nio.file.Path dont le rôle est de récupérer et manipuler des
chemins de fichiers de dossier et une une classe java.nio.file.Files qui contient tout un tas de
méthodes qui simplifient certaines actions (copie, déplacement, etc.) et permet aussi de récupérer tout un
tas d'informations sur un chemin.
Afin d'illustrer ce nouveau mode de fonctionnement, je vous propose de reprendre le premier exemple de
ce chapitre, celui qui affichait différentes informations sur notre fichier de test.
//On récupère maintenant la liste des répertoires dans une collection typée
//Via l'objet FileSystem qui représente le système de fichier de l'OS hébergeant la JVM
Iterable<Path> roots = FileSystems.getDefault().getRootDirectories();
int i = 0;
for(Path nom : listing){
System.out.print("\t\t" + ((Files.isDirectory(nom)) ? nom+"/" : nom));
i++;
if(i%4 == 0)System.out.println("\n");
}
} catch (IOException e) {
e.printStackTrace();
}
}
Vous avez également la possibilité d'ajouter un filtre à votre listing de répertoire afin qu'il ne liste que
certains fichiers :
La copie de fichier
Pour copier le fichier test.txt vers un fichier test2.txt, il suffit de faire :
Le déplacement de fichier
Ceci est très pratique pour lire ou écrire dans un fichier. Voici comment ça se traduit :
//Ouverture en écriture :
try ( OutputStream output = Files.newOutputStream(source) ) { … }
PosixFileAttributeView ajoute les permissions POSIX du monde Unix au premier objet cité
;
Le pattern decorator
Vous avez pu remarquer que les objets de ce chapitre utilisent des instances d'objets de même supertype
dans leur constructeur. Rappelez-vous cette syntaxe :
DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream(
new File("sdz.txt"))));
La raison d'agir de la sorte est simple : c'est pour ajouter de façon dynamique des fonctionnalités à un objet.
En fait, dites-vous qu'au moment de récupérer les données de notre objet DataInputStream, celles-ci
vont d'abord transiter par les objets passés en paramètre. Ce mode de fonctionnement suit une certaine
structure et une certaine hiérarchie de classes : c'est le pattern decorator.
Ce pattern de conception permet d'ajouter des fonctionnalités à un objet sans avoir à modifier son code
source. Afin de ne pas trop vous embrouiller avec les objets étudiés dans ce chapitre, je vais vous fournir un
autre exemple, plus simple, mais gardez bien en tête que les objets du package java.io utilisent ce
pattern. Le but du jeu est d'obtenir un objet auquel nous pourrons ajouter des choses afin de le « décorer
»… Vous allez travailler avec un objet Gateau qui héritera d'une classe abstraite Patisserie. Le but du
jeu est de pouvoir ajouter des couches à notre gâteau sans avoir à modifier son code source.
Vous avez vu avec le pattern strategy que la composition (« A un ») est souvent préférable à l'héritage («
Est un ») : vous aviez défini de nouveaux comportements pour vos objets en créant un supertype d'objet par
comportement. Ce pattern aussi utilise la composition comme principe de base : vous allez voir que nos
objets seront composés d'autres objets. La différence réside dans le fait que nos nouvelles fonctionnalités ne
seront pas obtenues uniquement en créant de nouveaux objets, mais en associant ceux-ci à des objets
existants. Ce sera cette association qui créera de nouvelles fonctionnalités !
Tout cela démarre avec un concept fondamental : l'objet de base et les objets qui le décorent doivent être du
même type, et ce, toujours pour la même raison, le polymorphisme, le polymorphisme, et le
polymorphisme !
Vous allez comprendre. En fait, les objets qui vont décorer notre gâteau posséderont la même
méthodepreparer() que notre objet principal, et nous allons faire fondre cet objet dans les autres. Cela
signifie que nos objets qui vont servir de décorateurs comporteront une instance de type Patisserie ; ils
vont englober les instances les unes après les autres et du coup, nous pourrons appeler la
méthode preparer()de manière récursive !
Vous pouvez voir les décorateurs comme des poupées russes : il est possible de mettre une poupée dans une
autre. Cela signifie que si nous décorons notre gateau avec un objet CoucheChocolat et un
objetCoucheCaramel, la situation pourrait être symbolisée par la figure suivante.
L'objet CoucheCaramel contient l'instance de la classe CoucheChocolat qui, elle, contient l'instance
deGateau : en fait, on va passer notre instance d'objet en objet ! Nous allons ajouter les fonctionnalités des
objets « décorants » en appelant la méthode preparer() de l'instance se trouvant dans l'objet avant
d'effectuer les traitements de la même méthode de l'objet courant, comme à la figure suivante.
Nous verrons, lorsque nous parlerons de la classe Thread, que ce système ressemble fortement à la pile
d'invocations de méthodes. La figure suivante montre à quoi ressemble le diagramme de classes de notre
exemple.
Diagramme de classes
Vous remarquez sur ce diagramme que notre classe mère Patisserie est en fait la strategy (une classe
encapsulant un comportement fait référence au pattern strategy : on peut dire qu'elle est la strategy de notre
hiérarchie) de notre structure, c'est pour cela que nous pourrons appeler la méthode preparer() de façon
récursive afin d'ajouter des fonctionnalités à nos objets. Voici les différentes classes que j'ai utilisées (je n'ai
utilisé que des String afin de ne pas surcharger les sources, et pour que vous vous focalisiez plus sur la
logique que sur le code).
Patisserie.java
Gateau.java
Couche.java
CoucheChocolat.java
CoucheCaramel.java
CoucheBiscuit.java
Résultat du test
J'ai agrémenté l'exemple d'une couche de biscuit, mais je pense que tout cela est assez représentatif de la
façon dont fonctionnent des flux d'entrée/sortie en Java. Vous devriez réussir à saisir tout cela sans souci.
Le fait est que vous commencez maintenant à avoir en main des outils intéressants pour programmer, et
c'est sans compter les outils du langage : vous venez de mettre votre deuxième pattern de conception dans
votre mallette du programmeur.
Vous avez pu voir que l'invocation des méthodes se faisait en allant jusqu'au dernier élément pour remonter
ensuite la pile d'invocations. Pour inverser ce fonctionnement, il vous suffit d'inverser les appels dans la
méthode preparer() : affecter d'abord le nom de la couche et ensuite le nom du décorateur.
Les classes que nous avons étudiées dans ce chapitre sont héritées des classes suivantes :
La façon dont on travaille avec des flux doit respecter la logique suivante :
o ouverture de flux ;
o lecture/écriture de flux ;
o fermeture de flux.
Si un objet sérialisable comporte un objet d'instance non sérialisable, une exception sera levée
lorsque vous voudrez sauvegarder votre objet.
L'utilisation de buffers permet une nette amélioration des performances en lecture et en écriture de
fichiers.
Afin de pouvoir ajouter des fonctionnalités aux objets gérant les flux, Java utilise le pattern «
decorator ».
Ce pattern permet d'encapsuler une fonctionnalité et de l'invoquer de façon récursive sur les objets
étant composés de décorateurs.
JAVA ET LA RÉFLEXIVITÉ
La réflexivité, aussi appelée introspection, consiste à découvrir de façon dynamique des informations
relatives à une classe ou à un objet. C'est notamment utilisé au niveau de la machine virtuelle Java lors de
l'exécution du programme. En gros, la machine virtuelle stocke les informations relatives à une classe dans
un objet.
La réflexivité n'est que le moyen de connaître toutes les informations concernant une classe donnée. Vous
pourrez même créer des instances de classe de façon dynamique grâce à cette notion.
L'objet Class
Concrètement, que se passe-t-il ? Au chargement d'une classe Java, votre JVM crée automatiquement un
objet. Celui-ci récupère toutes les caractéristiques de votre classe ! Il s'agit d'un objet Class.
Exemple : vous avez créé trois nouvelles classes Java. À l'exécution de votre programme, la JVM va créer
un objet Class pour chacune d'elles. Comme vous devez vous en douter, cet objet possède une multitude
de méthodes permettant d'obtenir tous les renseignements possibles et imaginables sur une classe.
Dans ce chapitre, nous allons visiter la classe String. Créez un nouveau projet ainsi qu'une classe
contenant la méthode main. Voici deux façons de récupérer un objet Class :
La méthode getMethods() de l'objet Class nous retourne un tableau d'objets Method présents dans le
package java.lang.reflect. Vous pouvez soit faire l'import à la main, soit déclarer un tableau
d'objets Method et utiliser le raccourci Ctrl + Shift + O.
Class[] p = m[i].getParameterTypes();
for(int j = 0; j < p.length; j++)
System.out.println(p[j].getName());
System.out.println("----------------------------------\n");
}
}
Le résultat est visible sur la figure suivante. Il est intéressant de voir que vous obtenez toutes sortes
d'informations sur les méthodes, leurs paramètres, les exceptions levées, leur type de retour, etc.
Utilisation de l'objet Method
Connaître la liste des champs (variable de classe ou d'instance)
Ici, nous allons procéder de la même façon qu'avec la liste des méthodes sauf que cette fois, la méthode
invoquée retournera un tableau d'objets Field. Voici un code qui affiche la liste des champs de la
classeString.
Ici, nous utiliserons un objet Constructor pour lister les constructeurs de la classe :
public static void main(String[] args) {
Class c = new String().getClass();
Constructor[] construc = c.getConstructors();
System.out.println("Il y a " + construc.length + " constructeurs dans cette classe");
//On parcourt le tableau des constructeurs
for(int i = 0; i < construc.length; i++){
System.out.println(construc[i].getName());
System.out.println("-----------------------------\n");
}
}
Vous constatez que l'objet Class regorge de méthodes en tout genre !
Et si nous essayions d'exploiter un peu plus celles-ci ?
Instanciation dynamique
Nous allons voir une petite partie de la puissance de cette classe (pour l'instant). Dans un premier temps,
créez un nouveau projet avec une méthode main ainsi qu'une classe correspondant au diagramme en figure
suivante.
Classe Paire
Voici son code Java :
public Paire(){
this.valeur1 = null;
this.valeur2 = null;
System.out.println("Instanciation !");
}
Pour instancier un nouvel objet Paire, commençons par récupérer ses constructeurs. Ensuite, nous
préparons un tableau contenant les données à insérer, puis invoquons la méthode toString().
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
Et le résultat donne la figure suivante.
Instanciation dynamique
Nous pouvons maintenant appeler la méthode toString() du deuxième objet… Oh et puis soyons fous,
appelons-la sur les deux :
System.out.println("----------------------------------------");
System.out.println("Méthode " + m.getName() + " sur o2: " +m.invoke(o2, null));
System.out.println("Méthode " + m.getName() + " sur o: " +m.invoke(o, null));
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
Et le résultat en figure suivante.
Maintenant, vous n'allez pas utiliser ce genre de technique tous les jours. Cependant, il est possible que
vous ayez besoin, pour une raison quelconque, de stocker le nom d'une classe Java dans une base de
données afin, justement, de pouvoir l'utiliser plus tard. Dans ce cas, lorsque votre base de données vous
fournira le nom de la classe en question, vous pourrez la manipuler dynamiquement.
Avec un tel objet, vous pouvez connaître absolument tout sur votre classe.
L'objet Class utilise des sous-objets tels que Method, Field et Constructor qui
permettent de travailler avec vos différents objets ainsi qu'avec ceux présents dans Java.
Grâce à cet objet, vous pouvez créer des instances de vos classes Java sans utiliser new.
J'espère que cette partie vous a plu et que vous avez appris plein de bonne choses !
J'ai volontairement omis de parler des flux et des threads dans cette partie. Je préfère avoir des cas bien
concrets à vous soumettre pour ça...
Bon : je sais que beaucoup d'entre vous l'attendent avec impatience, alors voici la partie sur
laprogrammation événementielle !
Dans cette partie, nous aborderons les interfaces graphiques (on parle aussi
d'IHM pour InterfacesHomme Machine ou de GUI pour Graphical User Interfaces) et, par extension, la
programmation événementielle. Par là, vous devez comprendre que votre programme ne réagira plus à
des saisies au clavier mais à des événements provenant d'un composant graphique : un bouton, une liste,
un menu…
Le langage Java propose différentes bibliothèques pour programmer des IHM, mais dans cet ouvrage,
nous utiliserons essentiellement les packages javax.swing et java.awt présents d'office dans Java.
Ce chapitre vous permettra d'apprendre à utiliser l'objet JFrame, présent dans le
package javax.swing. Vous serez alors à même de créer une fenêtre, de définir sa taille, etc.
Le fonctionnement de base des IHM vous sera également présenté et vous apprendrez qu'en réalité, une
fenêtre n'est qu'une multitude de composants posés les uns sur les autres et que chacun possède un rôle qui
lui est propre.
Mais trêve de bavardages inutiles, commençons tout de suite !
L'objet JFrame
Avant de nous lancer à corps perdu dans cette partie, vous devez savoir de quoi nous allons nous servir.
Dans ce cours, nous traiterons de javax.swing et de java.awt. Nous n'utiliserons pas de
composants awt, nous travaillerons uniquement avec des composants swing ; en revanche, des objets
issus du packageawt seront utilisés afin d'interagir et de communiquer avec les composants swing. Par
exemple, un composant peut être représenté par un bouton, une zone de texte, une case à cocher, etc.
Afin de mieux comprendre comment tout cela fonctionne, vous devez savoir que lorsque le langage Java a
vu le jour, dans sa version 1.0, seul awt était utilisable ; swing n'existait pas, il est apparu dans la version
1.2 de Java (appelée aussi Java 2). Les composants awt sont considérés comme lourds (on dit
aussiHeavyWeight) car ils sont fortement liés au système d'exploitation, c'est ce dernier qui les gère. Les
composants swing, eux, sont comme dessinés dans un conteneur, ils sont dit légers (on dit
aussiLightWeight) ; ils n'ont pas le même rendu à l'affichage, car ce n'est plus le système d'exploitation
qui les gère. Il existe également d'autres différences, comme le nombre de composants utilisables, la
gestion des bordures...
Pour toutes ces raisons, il est très fortement recommandé de ne pas mélanger les
composants swing etawt dans une même fenêtre ; cela pourrait occasionner des conflits ! Si vous associez
les deux, vous aurez de très grandes difficultés à développer une IHM stable et valide. En
effet, swing et awt ont les mêmes fondements mais diffèrent dans leur utilisation.
Cette parenthèse fermée, nous pouvons entrer dans le vif du sujet. Je ne vous demande pas de créer un
projet contenant une classe main, celui-ci doit être prêt depuis des lustres ! Pour utiliser une fenêtre de
typeJFrame, vous devez l'instancier, comme ceci :
import javax.swing.JFrame;
import javax.swing.JFrame;
Première fenêtre
À toutes celles et ceux qui se disent que cette fenêtre est toute petite, je réponds : « Bienvenue dans le
monde de la programmation événementielle ! » Il faut que vous vous y fassiez, vos composants ne sont
pas intelligents : il va falloir leur dire tout ce qu'ils doivent faire.
que notre programme s'arrête réellement lorsqu'on clique sur la croix rouge, car, pour ceux qui ne
l'auraient pas remarqué, le processus Eclipse tourne encore même après la fermeture de la fenêtre.
Pour chacun des éléments que je viens d'énumérer, il y a aura une méthode à appeler afin que
notreJFrame sache à quoi s'en tenir. Voici d'ailleurs un code répondant à toutes nos exigences :
import javax.swing.JFrame;
Afin de ne pas avoir à redéfinir les attributs à chaque fois, je pense qu'il serait utile que nous possédions
notre propre objet. Comme ça, nous aurons notre propre classe !
Pour commencer, effaçons tout le code que nous avons écrit dans notre méthode main. Créons ensuite une
classe que nous allons appeler Fenetre et faisons-la hériter de JFrame. Nous allons maintenant créer
notre constructeur, dans lequel nous placerons nos instructions.
Cela nous donne :
import javax.swing.JFrame;
Nous avons déjà centré notre fenêtre, mais vous voudriez peut-être la positionner ailleurs. Pour cela, vous
pouvez utiliser la méthode setLocation(int x, int y). Grâce à cette méthode, vous pouvez
spécifier où doit se situer votre fenêtre sur l'écran. Les coordonnées, exprimées en pixels, sont basées sur un
repère dont l'origine est représentée par le coin supérieur gauche (figure suivante).
La première valeur de la méthode vous positionne sur l'axe x, 0 correspondant à l'origine ; les valeurs
positives déplacent la fenêtre vers la droite tandis que les négatives la font sortir de l'écran par la gauche.
La même règle s'applique aux valeurs de l'axe y, si ce n'est que les valeurs positives font descendre la
fenêtre depuis l'origine tandis que les négatives la font sortir par le haut de l'écran.
Il s'agit là encore d'une méthode qui prend un booléen en paramètre. Passer true laissera la fenêtre au
premier plan quoi qu'il advienne, false annulera cela. Cette méthode est setAlwaysOnTop(boolean
b).
Je ne vais pas faire le tour de toutes les méthodes maintenant, car de toute façon, nous allons nous servir de
bon nombre d'entre elles très prochainement.Cependant, je suppose que vous aimeriez bien remplir un peu
votre fenêtre. Je m'en doutais, mais avant il vous faut encore apprendre une bricole. En effet, votre fenêtre,
telle qu'elle apparaît, vous cache quelques petites choses !
Vous pensez, et c'est légitime, que votre fenêtre est toute simple, dépourvue de tout composant (hormis les
contours). Eh bien vous vous trompez ! Une JFrame est découpée en plusieurs parties superposées,
comme le montre la figure suivante.
la fenêtre ;
le RootPane (en vert), le conteneur principal qui contient les autres composants ;
le LayeredPane (en violet), qui forme juste un panneau composé du conteneur global et de la
barre de menu (MenuBar) ;
le content pane (en rose) : c'est dans celui-ci que nous placerons nos composants ;
le GlassPane (en transparence), couche utilisée pour intercepter les actions de l'utilisateur avant
qu'elles ne parviennent aux composants.
Pas de panique, nous allons nous servir uniquement du content pane. Pour le récupérer, il nous suffit
d'utiliser la méthode getContentPane() de la classe JFrame. Cependant, nous allons utiliser un
composant autre que le content pane : un JPanel dans lequel nous insérerons nos composants.
Il existe d'autres types de fenêtre : la JWindow, une JFrame sans bordure et non draggable(déplaçable), et
la JDialog, une fenêtre non redimensionnable. Nous n'en parlerons toutefois pas ici.
L'objet JPanel
Comme je vous l'ai dit, nous allons utiliser un JPanel, composant de type conteneur dont la vocation est
d'accueillir d'autres objets de même type ou des objets de type composant (boutons, cases à cocher…).
2. Instancier un JPanel puis lui spécifier une couleur de fond pour mieux le distinguer.
3. Avertir notre JFrame que ce sera notre JPanel qui constituera son content pane.
import java.awt.Color;
import javax.swing.JFrame;
import javax.swing.JPanel;
Premier JPanel
C'est un bon début, mais je vois que vous êtes frustrés car il n'y a pas beaucoup de changement par rapport
à la dernière fois. Eh bien, c'est maintenant que les choses deviennent intéressantes ! Avant de vous faire
utiliser des composants (des boutons, par exemple), nous allons nous amuser avec notre JPanel. Plus
particulièrement avec un objet dont le rôle est de dessiner et de peindre notre composant. Ça vous tente ?
Alors, allons-y !
L'objet Graphics
Nous allons commencer par l'objet Graphics.Cet objet a une particularité de taille : vous ne pouvez
l'utiliser que si et seulement si le système vous l'a donné via la méthode getGraphics() d'un
composantswing ! Pour bien comprendre le fonctionnement de nos futurs conteneurs (ou composants),
nous allons créer une classe héritée de JPanel : appelons-la Panneau. Nous allons faire un petit tour
d'horizon du fonctionnement de cette classe, dont voici le code :
import java.awt.Graphics;
import javax.swing.JPanel;
C'est très pratique pour personnaliser des composants, car vous n'aurez jamais à l'appeler vous-mêmes :
c'est automatique ! Tout ce que vous pouvez faire, c'est forcer l'objet à se repeindre ; ce n'est toutefois pas
cette méthode que vous invoquerez, mais nous y reviendrons.
Vous aurez constaté que cette méthode possède un argument et qu'il s'agit du fameux
objet Graphics tant convoité. Nous reviendrons sur l'instruction g.fillOval(20, 20, 75, 75),
mais vous verrez à quoi elle sert lorsque vous exécuterez votre programme.
import javax.swing.JFrame;
Une fois votre fenêtre affichée, étirez-la, réduisez-la… À présent, vous pouvez voir ce qu'il se passe lorsque
vous interagissez avec votre fenêtre : celle-ci met à jour ses composants à chaque changement d'état ou de
statut. L'intérêt de disposer d'une classe héritée d'un conteneur ou d'un composant, c'est que nous pouvons
redéfinir la façon dont est peint ce composant sur la fenêtre.
Après cette mise en bouche, explorons un peu plus les capacités de notre objet Graphics. Comme vous
avez pu le voir, ce dernier permet, entre autres, de tracer des ronds ; mais il possède tout un tas de
méthodes plus pratiques et amusantes les unes que les autres… Nous ne les étudierons pas toutes, mais
vous aurez déjà de quoi faire.
Pour commencer, reprenons la méthode utilisée précédemment : g.fillOval(20, 20, 75, 75). Si
nous devions traduire cette instruction en français, cela donnerait : « Trace un rond plein en commençant à
dessiner sur l'axe x à 20 pixels et sur l'axe y à 20 pixels, et fais en sorte qu'il occupe 75 pixels de large et 75
pixels de haut. »
Oui, mais si je veux que mon rond soit centré et qu'il le reste ?
C'est dans ce genre de cas qu'il est intéressant d'utiliser une classe héritée. Puisque nous sommes dans notre
objet JPanel, nous avons accès à ses données lorsque nous le dessinons.
En effet, il existe des méthodes dans les objets composants qui retournent leur largeur (getWidth()) et
leur hauteur (getHeight()). En revanche, réussir à centrer un rond dans un JPanel en toutes
circonstances demande un peu de calcul mathématique de base, une pincée de connaissances et un soupçon
de logique !
Reprenons notre fenêtre telle qu'elle se trouve en ce moment. Vous pouvez constater que les
coordonnées de départ correspondent au coin supérieur gauche du carré qui entoure ce cercle, comme le
montre la figure suivante.
Point de départ du cercle dessiné
Cela signifie que si nous voulons que notre cercle soit tout le temps centré, il faut que notre carré soit
centré, donc que le centre de celui-ci corresponde au centre de notre fenêtre ! La figure suivante est un
schéma représentant ce que nous devons obtenir.
Coordonnées recherchées
Ainsi, le principe est d'utiliser la largeur et la hauteur de notre composant ainsi que la largeur et la hauteur
du carré qui englobe notre rond ; c'est facile, jusqu'à présent…
Maintenant, pour trouver où se situe le point depuis lequel doit commencer le dessin, il faut soustraire la
moitié de la largeur du composant à la moitié de celle du rond afin d'obtenir la valeur sur l'axe x, et faire de
même (en soustrayant les hauteurs, cette fois) pour l'axe y. Afin que notre rond soit le plus optimisé
possible, nous allons donner comme taille à notre carré la moitié de la taille de notre fenêtre ; ce qui
revient, au final, à diviser la largeur et la hauteur de cette dernière par quatre. Voici le code correspondant :
import java.awt.Graphics;
import javax.swing.JPanel;
La méthode drawOval()
Il s'agit de la méthode qui permet de dessiner un rond vide. Elle fonctionne exactement de la même manière
que la méthode fillOval(). Voici un code mettant en œuvre cette méthode :
import java.awt.Graphics;
import javax.swing.JPanel;
Si vous spécifiez une largeur différente de la hauteur, ces méthodes dessineront une forme ovale.
La méthode drawRect()
Cette méthode permet de dessiner des rectangles vides. Bien sûr, son homologue fillRect() existe. Ces
deux méthodes fonctionnent de la même manière que les précédentes, voyez plutôt ce code :
import java.awt.Graphics;
import javax.swing.JPanel;
La méthode drawRoundRect()
Il s'agit du même élément que précédemment, hormis le fait que le rectangle sera arrondi. L'arrondi est
défini par la valeur des deux derniers paramètres.
import java.awt.Graphics;
import javax.swing.JPanel;
La méthode drawLine()
Cette méthode permet de tracer des lignes droites. Il suffit de lui spécifier les coordonnées de départ et
d'arrivée de la ligne. Dans ce code, je trace les diagonales du conteneur :
import java.awt.Graphics;
import javax.swing.JPanel;
La méthode drawPolygon()
Grâce à cette méthode, vous pouvez dessiner des polygones de votre composition. Eh oui, c'est à vous de
définir les coordonnées de tous les points qui les forment ! Voici à quoi elle ressemble
import java.awt.Graphics;
import javax.swing.JPanel;
int x2[] = {50, 60, 80, 90, 90, 80, 60, 50};
int y2[] = {60, 50, 50, 60, 80, 90, 90, 80};
g.fillPolygon(x2, y2, 8);
}
}
Voyez le résultat à la figure suivante.
Il existe également une méthode qui prend exactement les mêmes arguments mais qui, elle, trace plusieurs
lignes : drawPolyline().
Cette méthode va dessiner les lignes correspondant aux coordonnées définies dans les tableaux, sachant que
lorsque son indice s'incrémente, la méthode prend automatiquement les valeurs de l'indice précédent
comme point d'origine. Cette méthode ne fait pas le lien entre la première et la dernière valeur de vos
tableaux. Vous pouvez essayer le code précédent en remplaçant drawPolygon() par cette méthode.
La méthode drawString()
Voici la méthode permettant d'écrire du texte. Elle est très simple à utiliser : il suffit de lui passer en
paramètre la phrase à écrire et de lui spécifier à quelles coordonnées commencer.
import java.awt.Graphics;
import javax.swing.JPanel;
Vous pouvez aussi modifier la couleur (la modification s'appliquera également pour les autres méthodes)
et la police d'écriture. Pour redéfinir la police d'écriture, vous devez créer un objet Font. Le code suivant
illustre la façon de procéder.
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import javax.swing.JPanel;
La méthode drawImage()
un objet Image ;
un objet ImageIO ;
un objet File.
Vous allez voir que l'utilisation de ces objets est très simple. Il suffit de déclarer un objet de type Image et
de l'initialiser en utilisant une méthode statique de l'objet ImageIO qui, elle, prend un objet File en
paramètre. Ça peut sembler compliqué, mais vous allez voir que ce n'est pas le cas… Notre image sera
stockée à la racine de notre projet, mais ce n'est pas une obligation. Dans ce cas, faites attention au chemin
d'accès de votre image.
En ce qui concerne le dernier paramètre de la méthode drawImage, il s'agit de l'objet qui est censé
observer l'image. Ici, nous allons utiliser notre objet Panneau, donc this.
Cette méthode dessinera l'image avec ses propres dimensions. Si vous voulez qu'elle occupe l'intégralité de
votre conteneur, utilisez le constructeur suivant : drawImage(Image img, int x, int y, int
width, int height, Observer obs).
import java.awt.Graphics;
import java.awt.Image;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JPanel;
L'objet Graphics2D
Ceci est une amélioration de l'objet Graphics, et vous allez vite comprendre pourquoi.
Pour utiliser cet objet, il nous suffit en effet de caster l'objet Graphics en Graphics2D (Graphics2D
g2d = (Graphics2D) g), et de ne surtout pas oublier d'importer notre classe qui se trouve dans le
package java.awt. L'une des possibilités qu'offre cet objet n'est autre que celle de peindre des objets avec
des dégradés de couleurs. Cette opération n'est pas du tout difficile à réaliser : il suffit d'utiliser un
objetGradientPaint et une méthode de l'objet Graphics2D.
Nous n'allons pas reprendre tous les cas que nous avons vus jusqu'à présent, mais juste deux ou trois afin
que vous voyiez bien la différence. Commençons par notre objet GradientPaint ; voici comment
l'initialiser (vous devez mettre à jour vos imports en ajoutant import java.awt.GradientPaint) :
Ensuite, pour utiliser ce dégradé dans une forme, il faut mettre à jour notre objet Graphics2D, comme
ceci :
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JPanel;
Votre dégradé est oblique (rien ne m'échappe, à moi :-p). Ce sont les coordonnées choisies qui influent sur
la direction du dégradé. Dans notre exemple, nous partons du point de coordonnées (0, 0) vers le point de
coordonnées (30, 30). Pour obtenir un dégradé vertical, il suffit d'indiquer la valeur de la seconde
coordonnéex à 0, ce qui correspond à la figure suivante.
Dégradé horizontal
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.imageio.ImageIO;
import javax.swing.JPanel;
g2d.setPaint(gp);
g2d.fillRect(0, 0, 20, this.getHeight());
g2d.setPaint(gp2);
g2d.fillRect(20, 0, 20, this.getHeight());
g2d.setPaint(gp3);
g2d.fillRect(40, 0, 20, this.getHeight());
g2d.setPaint(gp4);
g2d.fillRect(60, 0, 20, this.getHeight());
g2d.setPaint(gp5);
g2d.fillRect(80, 0, 20, this.getHeight());
g2d.setPaint(gp6);
g2d.fillRect(100, 0, 40, this.getHeight());
}
}
Maintenant que vous savez utiliser les dégradés avec des rectangles, vous savez les utiliser avec toutes les
formes. Je vous laisse essayer cela tranquillement chez vous.
Pour créer des fenêtres, Java fournit les composants swing (dans javax.swing)
et awt (dansjava.awt).
Par défaut, une fenêtre a une taille minimale et n'est pas visible.
Un composant doit être bien paramétré pour qu'il fonctionne à votre convenance.
Lorsque vous ajoutez un JPanel principal à votre fenêtre, n'oubliez pas d'indiquer à votre fenêtre
qu'il constituera son content pane.
Pour redéfinir la façon dont l'objet est dessiné sur votre fenêtre, vous devez utiliser la
méthodepaintComponent() en créant une classe héritée.
C'est lui que vous allez utiliser pour dessiner dans votre conteneur.
Pour des dessins plus évolués, vous devez utiliser l'objet Graphics2D qui s'obtient en effectuant
un cast sur l'objet Graphics.
Dans ce chapitre, nous allons voir comment créer une animation simple. Il ne vous sera pas possible de
réaliser un jeu au terme de ce chapitre, mais je pense que vous y trouverez de quoi vous amuser un peu.
Nous réutiliserons cette animation dans plusieurs chapitres de cette troisième partie afin d'illustrer le
fonctionnement de divers composants graphiques. L'exemple est rudimentaire, mais il a l'avantage d'être
efficace et de favoriser votre apprentissage de la programmation événementielle.
Création de l'animation
une classe héritée de JPanel avec laquelle nous faisons de jolis dessins. Un rond, en
l'occurrence.
En utilisant ces deux classes, nous allons pouvoir créer un effet de déplacement.
Vous avez bien lu : j'ai parlé d'un effet de déplacement ! Le principe réside dans le fait que vous allez
modifier les coordonnées de votre rond et forcer votre objet Panneau à se redessiner. Tout cela - vous
l'avez déjà deviné - dans une boucle.
Jusqu'à présent, nous avons utilisé des valeurs fixes pour les coordonnées du rond, mais il va falloir
dynamiser tout ça. Nous allons donc créer deux variables privées de type int dans la classe Panneau :
appelons-les posX et posY. Dans l'animation sur laquelle nous allons travailler, notre rond viendra de
l'extérieur de la fenêtre. Partons du principe que celui-ci a un diamètre de cinquante pixels : il faut donc que
notre panneau peigne ce rond en dehors de sa zone d'affichage. Nous initialiserons donc nos deux variables
d'instance à « -50 ». Voici le code de notre classe Panneau :
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;
import java.awt.Dimension;
import javax.swing.JFrame;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
this.setContentPane(pan);
this.setVisible(true);
go();
}
La toute première fois, dans le constructeur de notre classe Fenetre, votre composant avait invoqué la
méthode paintComponent() et avait dessiné un rond aux coordonnées que vous lui aviez spécifiées. La
méthode repaint() ne fait rien d'autre qu'appeler à nouveau la méthode paintComponent() ; mais
puisque nous avons changé les coordonnées du rond par le biais des accesseurs, la position de celui-ci sera
modifiée à chaque tour de boucle.
La deuxième instruction, Thread.sleep(), est un moyen de suspendre votre code. Elle met en attente
votre programme pendant un laps de temps défini dans la méthode sleep() exprimé en millièmes de
seconde (plus le temps d'attente est court, plus l'animation est rapide). Thread est en fait un objet qui
permet de créer un nouveau processus dans un programme ou de gérer le processus principal.
Dans tous les programmes, il y a au moins un processus : celui qui est en cours d'exécution. Vous verrez
plus tard qu'il est possible de diviser certaines tâches en plusieurs processus afin de ne pas perdre du temps
et des performances. Pour le moment, sachez que vous pouvez effectuer des pauses dans vos programmes
grâce à cette instruction :
try{
Thread.sleep(1000); //Ici, une pause d'une seconde
}catch(InterruptedException e) {
e.printStackTrace();
}
Cette instruction est dite « à risque », vous devez donc l'entourer d'un bloc try{…}catch{…} afin de
capturer les exceptions potentielles. Sinon, erreur de compilation !
Maintenant que la lumière est faite sur cette affaire, exécutez ce code, vous obtenez la figure suivante.
Rendu final de l'animation
Bien sûr, cette image est le résultat final : vous devez avoir vu votre rond bouger. Sauf qu'il a laissé une
traînée derrière lui… L'explication de ce phénomène est simple : vous avez demandé à votre
objet Panneau de se redessiner, mais il a également affiché les précédents passages de votre rond ! Pour
résoudre ce problème, il faut effacer ces derniers avant de redessiner le rond.
Comment ? Dessinez un rectangle de n'importe quelle couleur occupant toute la surface disponible avant de
peindre votre rond. Voici le nouveau code de la classe Panneau :
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;
Améliorations
Voici l'un des moments délicats que j'attendais. Si vous vous rappelez bien ce que je vous ai expliqué sur le
fonctionnement des boucles, vous vous souvenez de mon avertissement à propos des boucles infinies. Eh
bien, ce que nous allons faire ici, c'est justement utiliser une boucle infinie.
Il existe plusieurs manières de réaliser une boucle infinie : vous avez le choix entre une
boucle for, whileou do… while. Regardez ces déclarations :
Jusqu'à présent, nous n'attachions aucune importance au bord que notre rond dépassait. Cela est terminé !
Dorénavant, nous séparerons le dépassement des coordonnées posX et posY de notre Panneau.
Pour les instructions qui vont suivre, gardez en mémoire que les coordonnées du rond correspondent en
réalité aux coordonnées du coin supérieur gauche du carré entourant le rond.
Voici la marche à suivre :
si la valeur de la coordonnée x du rond est inférieure à la largeur du composant et que le rond
avance, on continue d'avancer ;
sinon, on recule.
Comment savoir si l'on doit avancer ou reculer ? Grâce à un booléen, par exemple. Au tout début de notre
application, deux booléens seront initialisés à false, et si la coordonnée x est supérieure à la largeur
duPanneau, on recule ; sinon, on avance. Idem pour la coordonnée y.
Dans ce code, j'utilise deux variables de type int pour éviter de rappeler les
méthodesgetPosX() et getPosY().
Voici donc le nouveau code de la méthode go() :
//Sinon, on décrémente
else
pan.setPosX(--x);
Classe Panneau
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;
Classe Fenetre
import java.awt.Dimension;
import javax.swing.JFrame;
public Fenetre() {
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
this.setContentPane(pan);
this.setVisible(true);
go();
}
Cette méthode prend en paramètre un entier qui correspond à une valeur temporelle exprimée
en millièmes de seconde.
Vous pouvez utiliser des boucles infinies pour créer des animations.
Voici l'un des moments que vous attendiez avec impatience ! Vous allez enfin pouvoir utiliser un bouton
dans votre application. Cependant, ne vous réjouissez pas trop vite : vous allez effectivement insérer un
bouton, mais vous vous rendrez rapidement compte que les choses se compliquent dès que vous employez
ce genre de composant… Et c'est encore pire lorsqu'il y en a plusieurs !
Avant de commencer, nous devrons apprendre à positionner des composants dans une fenêtre. Il nous faut
en effet gérer la façon dont le contenu est affiché dans une fenêtre.
Comme indiqué dans le titre, nous allons utiliser la classe JButton issue du package javax.swing. Au
cours de ce chapitre, notre projet précédent sera mis à l'écart : oublions momentanément notre
objetPanneau.
une classe contenant une méthode main que nous appellerons Test ;
une classe héritée de JFrame (contenant la totalité du code que l'on a déjà écrit, hormis la
méthodego()), nous la nommerons Fenetre.
Dans la classe Fenetre, nous allons créer une variable d'instance de type JPanel et une autre de
typeJButton. Faisons de JPanel le content pane de notre Fenetre, puis définissons le libellé (on parle
aussi d'étiquette) de notre bouton et mettons-le sur ce qui nous sert de content pane (en
l'occurrence,JPanel).
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 150);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
//Ajout du bouton à notre content pane
pan.add(bouton);
this.setContentPane(pan);
this.setVisible(true);
}
}
Voyez le résultat en figure suivante.
Tout d'abord, pour utiliser le content pane d'une JFrame, il faut appeler la
méthode getContentPane(): nous ajouterons nos composants au content pane qu'elle retourne. Voici
donc le nouveau code :
import javax.swing.JButton;
import javax.swing.JFrame;
public Fenetre(){
this.setTitle("Bouton");
this.setSize(300, 150);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
//On ajoute le bouton au content pane de la JFrame
this.getContentPane().add(bouton);
this.setVisible(true);
}
}
La figure suivante montre que le résultat n'est pas du tout concluant.
Vous allez voir qu'il existe plusieurs sortes de layout managers, plus ou moins simples à utiliser, dont le
rôle est de gérer la position des éléments sur la fenêtre. Tous ces layout managers se trouvent dans le
packagejava.awt.
L'objet BorderLayout
Le premier objet que nous aborderons est le BorderLayout. Il est très pratique si vous voulez placer vos
composants de façon simple par rapport à une position cardinale de votre conteneur. Si je parle de
positionnement cardinal, c'est parce que vous devez utiliser les valeurs NORTH, SOUTH, EAST, WEST ou
encore CENTER. Mais puisqu'un aperçu vaut mieux qu'un exposé sur le sujet, voici un exemple à la figure
suivante mettant en œuvre un BorderLayout.
Exemple de BorderLayout
Cette fenêtre est composée de cinq JButton positionnés aux cinq endroits différents que propose
unBorderLayout. Voici le code de cette fenêtre :
import java.awt.BorderLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
Utiliser l'objet BorderLayout soumet vos composants à certaines contraintes. Pour une
position NORTHou SOUTH, la hauteur de votre composant sera proportionnelle à la fenêtre, mais il occupera
toute la largeur ; tandis qu'avec WEST et EAST, ce sera la largeur qui sera proportionnelle alors que toute la
hauteur sera occupée ! Et bien entendu, avec CENTER, tout l'espace est utilisé.
Vous devez savoir que CENTER est aussi le layout par défaut du content pane de la fenêtre, d'où la taille du
bouton lorsque vous l'avez ajouté pour la première fois.
L'objet GridLayout
Celui-ci permet d'ajouter des composants suivant une grille définie par un nombre de lignes et de colonnes.
Les éléments sont disposés à partir de la case située en haut à gauche. Dès qu'une ligne est remplie, on
passe à la suivante. Si nous définissons une grille de trois lignes et de deux colonnes, nous obtenons le
résultat visible sur la figure suivante.
import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
Sachez également que vous pouvez définir le nombre de lignes et de colonnes en utilisant ces méthodes :
Grâce à lui, vous pourrez ranger vos composants à la suite soit sur une ligne, soit sur une colonne. Le
mieux, c'est encore un exemple de rendu (voir figure suivante) avec un code.
Exemple de BoxLayout
Voici le code correspondant :
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public Fenetre(){
this.setTitle("Box Layout");
this.setSize(300, 120);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
this.getContentPane().add(b4);
this.setVisible(true);
}
}
Ce code est simple : on crée trois JPanel contenant chacun un certain nombre de JButton rangés en
ligne grâce à l'attribut LINE_AXIS. Ces trois conteneurs créés, nous les rangeons dans un quatrième où,
cette fois, nous les agençons dans une colonne grâce à l'attribut PAGE_AXIS. Rien de compliqué, vous en
conviendrez, mais vous devez savoir qu'il existe un moyen encore plus simple d'utiliser ce layout
: via l'objetBox. Ce dernier n'est rien d'autre qu'un conteneur paramétré avec un BoxLayout. Voici un
code affichant la même chose que le précédent :
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JFrame;
public Fenetre(){
this.setTitle("Box Layout");
this.setSize(300, 120);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
this.getContentPane().add(b4);
this.setVisible(true);
}
}
L'objet CardLayout
Vous allez à présent pouvoir gérer vos conteneurs comme un tas de cartes (les uns sur les autres), et
basculer d'un contenu à l'autre en deux temps, trois clics. Le principe est d'assigner des conteneurs au
layout en leur donnant un nom afin de les retrouver plus facilement, permettant de passer de l'un à l'autre
sans effort. La figure suivante est un schéma représentant ce mode de fonctionnement.
Schéma du CardLayout
Je vous propose un code utilisant ce layout. Vous remarquerez que j'ai utilisé des boutons afin de passer
d'un conteneur à un autre et n'y comprendrez peut-être pas tout, mais ne vous inquiétez pas, nous allons
apprendre à réaliser tout cela avant la fin de ce chapitre. Pour le moment, ne vous attardez donc pas trop sur
les actions : concentrez-vous sur le layout en lui-même.
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public Fenetre(){
this.setTitle("CardLayout");
this.setSize(300, 120);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
boutonPane.add(bouton);
boutonPane.add(bouton2);
//On définit le layout
content.setLayout(cl);
//On ajoute les cartes à la pile avec un nom pour les retrouver
content.add(card1, listContent[0]);
content.add(card2, listContent[1]);
content.add(card3, listContent[2]);
this.getContentPane().add(boutonPane, BorderLayout.NORTH);
this.getContentPane().add(content, BorderLayout.CENTER);
this.setVisible(true);
}
}
La figure suivante correspond aux résultats de ce code à chaque clic sur les boutons.
Schéma du CardLayout
L'objet GridBagLayout
Cet objet est certainement le plus difficile à utiliser et à comprendre (ce qui l'a beaucoup desservi auprès
des développeurs Java). Pour faire simple, ce layout se présente sous la forme d'une grille à la façon d'un
tableau Excel : vous devez positionner vos composants en vous servant des coordonnées des cellules (qui
sont définies lorsque vous spécifiez leur nombre). Vous devez aussi définir les marges et la façon dont vos
composants se répliquent dans les cellules... Vous voyez que c'est plutôt dense comme gestion du
positionnement. Je tiens aussi à vous prévenir que je n'entrerai pas trop dans les détails de ce layout afin de
ne pas trop compliquer les choses.
La figure suivante représente la façon dont nous allons positionner nos composants.
Positionnement avec le
GridBagLayout
Imaginez que le nombre de colonnes et de lignes ne soit pas limité comme il l'est sur le schéma (c'est un
exemple et j'ai dû limiter sa taille, mais le principe est là). Vous paramétreriez le composant avec des
coordonnées de cellules, en précisant si celui-ci doit occuper une ou plusieurs d'entre elles. Afin d'obtenir
un rendu correct, vous devriez indiquer au layout manager lorsqu'une ligne se termine, ce qui se fait en
spécifiant qu'un composant est le dernier élément d'une ligne, et vous devriez en plus spécifier au
composant débutant la ligne qu'il doit suivre le dernier composant de la précédente.
Je me doute que c'est assez flou et confus, je vous propose donc de regarder la figure suivante, qui est un
exemple de ce que nous allons obtenir.
Exemple de GridBagLayout
Tous les éléments que vous voyez sont des conteneurs positionnés suivant une matrice, comme expliqué ci-
dessus. Afin que vous vous en rendiez compte, regardez comment le tout est rangé sur la figure suivante.
Composition du GridBagLayout
Vous pouvez voir que nous avons fait en sorte d'obtenir un tableau de quatre colonnes sur trois lignes. Nous
avons positionné quatre éléments sur la première ligne, spécifié que le quatrième élément terminait celle-ci,
puis nous avons placé un autre composant au début de la deuxième ligne d'une hauteur de deux cases, en
informant le gestionnaire que celui-ci suivait directement la fin de la première ligne. Nous ajoutons un
composant de trois cases de long terminant la deuxième ligne, pour passer ensuite à un composant de deux
cases de long puis à un dernier achevant la dernière ligne.
Lorsque des composants se trouvent sur plusieurs cases, vous devez spécifier la façon dont ils s'étalent :
horizontalement ou verticalement.
Le moment est venu de vous fournir le code de cet exemple, mais je vous préviens, ça pique un peu les
yeux :
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
public Fenetre(){
this.setTitle("GridBagLayout");
this.setSize(300, 160);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
weightx : si la grille est plus large que l'espace demandé, l'espace est redistribué
proportionnellement aux valeurs de weightx des différentes colonnes.
weighty : si la grille est plus haute que l'espace demandé, l'espace est redistribué
proportionnellement aux valeurs de weighty des différentes lignes.
anchor : ancrage du composant dans la cellule, c'est-à-dire son alignement dans la cellule (en
bas à droite, en haut à gauche…). Voici les différentes valeurs utilisables :
insets : espace autour du composant. S'ajoute aux espacements définis par les
propriétés ipadxet ipady ci-dessous.
Dans mon exemple, je ne vous ai pas parlé de tous les attributs existants, mais si vous avez besoin d'un
complément d'information, n'hésitez pas à consulter le site d'Oracle.
L'objet FlowLayout
Celui-ci est certainement le plus facile à utiliser ! Il se contente de centrer les composants dans le
conteneur. Regardez plutôt la figure suivante.
Exemple de FlowLayout
On dirait bien que nous venons de trouver le layout manager défini par défaut dans les objets JPanel.
Lorsque vous insérez plusieurs composants dans ce gestionnaire, il passe à la ligne suivante dès que la
place est trop étroite. Voyez l'exemple de la figure suivante.
import java.awt.BorderLayout;
import java.awt.Color;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Fenetre extends JFrame{
private Panneau pan = new Panneau();
private JButton bouton = new JButton("mon bouton");
private JPanel container = new JPanel();
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
container.add(pan, BorderLayout.CENTER);
container.add(bouton, BorderLayout.SOUTH);
this.setContentPane(container);
this.setVisible(true);
go();
}
Pour ajouter un bouton dans une fenêtre, vous devez utiliser la méthode add() de son content
pane.
Il existe des objets permettant de positionner les composants sur un content pane ou un
conteneur : les layout managers.
Le layout manager par défaut du content pane d'un objet JFrame est le BorderLayout.
Nous avons vu dans le chapitre précédent les différentes façons de positionner des boutons et, par
extension, des composants (car oui, ce que nous venons d'apprendre pourra être réutilisé avec tous les
autres composants que nous verrons par la suite).
Maintenant que vous savez positionner des composants, il est grand temps de leur indiquer ce qu'ils
doivent faire. C'est ce que je vous propose d'aborder dans ce chapitre. Mais avant cela, nous allons voir
comment personnaliser un bouton. Toujours prêts ?
Créons une classe héritant de javax.swing.JButton que nous appellerons Bouton et redéfinissons
sa méthode paintComponent(). Vous devriez y arriver tout seuls. Cet exemple est représenté à la
figure suivante :
Bouton personnalisé
Voici la classe Bouton de cette application :
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JButton;
J'ai appliqué l'image (bien sûr, ladite image se trouve à la racine de mon projet !) sur l'intégralité du fond,
comme je l'ai montré lorsque nous nous amusions avec notre Panneau. Voici le code de cette
classeBouton :
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JButton;
Et si je vous proposais de changer l'aspect de votre objet lorsque vous cliquez dessus avec votre souris et
lorsque vous relâchez le clic ? Il existe des interfaces à implémenter qui permettent de gérer toutes sortes
d'événements dans votre IHM. Le principe est un peu déroutant au premier abord, mais il est assez simple
lorsqu'on a un peu pratiqué. N'attendons plus et voyons cela de plus près !
Avant de nous lancer dans l'implémentation, vous pouvez voir le résultat que nous allons obtenir sur les
deux figures suivantes.
Apparence du bouton au survol de la souris
Il va tout de même falloir passer par un peu de théorie avant d'arriver à ce résultat. Pour détecter les
événements qui surviennent sur votre composant, Java utilise ce qu'on appelle le design pattern observer.
Je ne vous l'expliquerai pas dans le détail tout de suite, nous le verrons à la fin de ce chapitre.
Vous vous en doutez, nous devrons implémenter l'interface MouseListener dans notre classe Bouton.
Nous devrons aussi préciser à notre classe qu'elle devra tenir quelqu'un au courant de ses changements
d'état par rapport à la souris. Ce quelqu'un n'est autre… qu'elle-même ! Eh oui : notre classe va s'écouter,
ce qui signifie que dès que notre objet observable (notre bouton) obtiendra des informations concernant
les actions effectuées par la souris, il indiquera à l'objet qui l'observe (c'est-à-dire à lui-même) ce qu'il doit
effectuer.
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JButton;
Dans notre cas, la méthode repaint() est appelée de façon implicite : lorsqu'un événement est
déclenché, notre objet se redessine automatiquement ! Comme lorsque vous redimensionniez votre fenêtre
dans les premiers chapitres.
Nous n'avons alors plus qu'à modifier notre image en fonction de la méthode invoquée. Notre objet
comportera les caractéristiques suivantes :
Pour ce faire, je vous propose de télécharger les fichiers PNG dont je me suis servi (rien ne vous empêche
de les créer vous-mêmes).
Je vous rappelle que dans le code qui suit, les images sont placées à la racine du projet.
Voici maintenant le code de notre classe Bouton personnalisée :
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JButton;
Vous possédez dorénavant un bouton personnalisé qui réagit au passage de votre souris. Je sais qu'il y aura
des « p'tits malins » qui cliqueront sur le bouton et relâcheront le clic en dehors du bouton : dans ce cas, le
fond du bouton deviendra orange, puisque c'est ce qui doit être effectué vu la
méthode mouseReleased(). Afin de pallier ce problème, nous allons vérifier que lorsque le clic est
relâché, la souris se trouve toujours sur le bouton.
Nous avons implémenté l'interface MouseListener ; il reste cependant un objet que nous n'avons pas
encore utilisé. Vous ne le voyez pas ? C'est le paramètre présent dans toutes les méthodes de cette interface
: oui, c'est MouseEvent !
Cet objet nous permet d'obtenir beaucoup d'informations sur les événements. Nous ne détaillerons pas tout
ici, mais nous verrons certains côtés pratiques de ce type d'objet tout au long de cette partie. Dans notre cas,
nous pouvons récupérer les coordonnées x et y du curseur de la souris par rapport au Bouton grâce aux
méthodes getX() et getY(). Cela signifie que si nous relâchons le clic en dehors de la zone où se trouve
notre objet, la valeur retournée par la méthode getY() sera négative.
Afin de gérer les différentes actions à effectuer selon le bouton sur lequel on clique, nous allons utiliser
l'interface ActionListener.
Nous n'allons pas implémenter cette interface dans notre classe Bouton mais dans notre
classeFenetre, le but étant de faire en sorte que lorsque l'on clique sur le bouton, il se passe quelque
chose dans notre application : changer un état, une variable, effectuer une incrémentation… Enfin,
n'importe quelle action !
Comme je vous l'ai expliqué, lorsque nous appliquons un addMouseListener(), nous informons l'objet
observé qu'un autre objet doit être tenu au courant de l'événement. Ici, nous voulons que ce soit notre
application (notre Fenetre) qui écoute notre Bouton, le but étant de pouvoir lancer ou arrêter
l'animation dans le Panneau.
Avant d'en arriver là, nous allons faire plus simple : nous nous pencherons dans un premier temps sur
l'implémentation de l'interface ActionListener. Afin de vous montrer toute la puissance de cette
interface, nous utiliserons un nouvel objet issu du package javax.swing : le JLabel. Cet objet se
comporte comme un libellé : il est spécialisé dans l'affichage de texte ou d'image. Il est donc idéal pour
notre premier exemple !
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
container.add(pan, BorderLayout.CENTER);
container.add(bouton, BorderLayout.SOUTH);
container.add(label, BorderLayout.NORTH);
this.setContentPane(container);
this.setVisible(true);
go();
}
//Le reste ne change pas
}
Vous pouvez voir que le texte de cet objet est aligné par défaut en haut à gauche. Il est possible de modifier
quelques paramètres tels que :
l'alignement du texte ;
la police à utiliser ;
la couleur du texte ;
d'autres paramètres.
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
container.add(pan, BorderLayout.CENTER);
container.add(bouton, BorderLayout.SOUTH);
container.add(label, BorderLayout.NORTH);
this.setContentPane(container);
this.setVisible(true);
go();
}
La figure suivante donne un aperçu de ce code.
Maintenant que notre libellé se présente exactement sous la forme que nous voulons, nous pouvons
implémenter l'interface ActionListener. Vous remarquerez que cette interface ne contient qu'une seule
méthode !
public Fenetre(){
//Ce morceau de code ne change pas
}
}
}
Nous allons maintenant informer notre objet Bouton que notre objet Fenetre l'écoute. Vous l'avez
deviné : ajoutons notre Fenetre à la liste des objets qui écoutent notre Bouton grâce à la
méthodeaddActionListener(ActionListener obj) présente dans la classe JButton, donc
utilisable avec la variable bouton. Ajoutons cette instruction dans le constructeur en passant this en
paramètre (puisque c'est notre Fenetre qui écoute le Bouton).
Une fois l'opération effectuée, nous pouvons modifier le texte du JLabel avec la
méthodeactionPerformed(). Nous allons compter le nombre de fois que l'on a cliqué sur le bouton :
ajoutons une variable d'instance de type int dans notre class et appelons-la compteur, puis dans la
méthodeactionPerformed(), incrémentons ce compteur et affichons son contenu dans notre libellé.
Voici le code de notre objet mis à jour :
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
container.add(pan, BorderLayout.CENTER);
container.add(bouton, BorderLayout.SOUTH);
Et nous ne faisons que commencer… Eh oui, nous allons maintenant ajouter un deuxième bouton à
notreFenetre, à côté du premier (vous êtes libres d'utiliser la classe personnalisée ou un
simple JButton). Pour ma part, j'utiliserai des boutons normaux ; en effet, dans notre classe personnalisée,
la façon dont le libellé est écrit dans notre bouton n'est pas assez souple et l'affichage peut donc être
décevant (dans certains cas, le libellé peut ne pas être centré)…
Bref, nous possédons à présent deux boutons écoutés par notre objet Fenetre.
Vous devez créer un deuxième JPanel qui contiendra nos deux boutons, puis l'insérer dans le content
pane en position BorderLayout.SOUTH. Si vous tentez de positionner deux composants au même
endroit grâce à un BorderLayout,seul le dernier composant ajouté apparaîtra : en effet, le composant
occupe toute la place disponible dans un BorderLayout !
Voici notre nouveau code :
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
container.add(pan, BorderLayout.CENTER);
bouton.addActionListener(this);
bouton2.addActionListener(this);
//…
}
La figure suivante illustre le résultat que nous obtenons.
À présent, le problème est le suivant : comment effectuer deux actions différentes dans la
méthodeactionPerformed() ?
En effet, si nous laissons la méthode actionPerformed() telle quelle, les deux boutons exécutent la
même action lorsqu'on les clique. Essayez, vous verrez le résultat.
Il existe un moyen de connaître l'élément ayant déclenché l'événement : il faut se servir de l'objet passé
en paramètre dans la méthode actionPerformed(). Nous pouvons exploiter la
méthode getSource() de cet objet pour connaître le nom de l'instance qui a généré l'événement.
Testez la méthodeactionPerformed() suivante et voyez si le résultat correspond à la figure suivante.
if(arg0.getSource() == bouton2)
label.setText("Vous avez cliqué sur le bouton 2");
}
Notre code fonctionne à merveille ! Cependant, cette approche n'est pas très orientée objet : si notre IHM
contient une multitude de boutons, la méthode actionPerformed() sera très chargée. Nous pourrions
créer deux objets à part, chacun écoutant un bouton, dont le rôle serait de réagir de façon appropriée pour
chaque bouton ; mais si nous avions besoin de modifier des données spécifiques à la classe contenant nos
boutons, il faudrait ruser afin de parvenir à faire communiquer nos objets… Pas terrible non plus.
En Java, on peut créer ce que l'on appelle des classes internes. Cela consiste à déclarer une classe à
l'intérieur d'une autre classe. Je sais, ça peut paraître tordu, mais vous allez bientôt constater que c'est très
pratique.
En effet, les classes internes possèdent tous les avantages des classes normales, de l'héritage d'une
superclasse à l'implémentation d'une interface. Elles bénéficient donc du polymorphisme et de la
covariance des variables. En outre, elles ont l'avantage d'avoir accès aux attributs de la classe dans laquelle
elles sont déclarées !
Dans le cas qui nous intéresse, cela permet de créer une implémentation de
l'interface ActionListenerdétachée de notre classe Fenetre, mais pouvant utiliser ses attributs. La
déclaration d'une telle classe se fait exactement de la même manière que pour une classe normale, si ce
n'est qu'elle se trouve déjà dans une autre classe. Nous procédons donc comme ceci :
public MaClasseExterne(){
//…
}
class MaClassInterne{
public MaClassInterne(){
//…
}
}
}
Grâce à cela, nous pourrons concevoir une classe spécialisée dans l'écoute des composants et qui effectuera
un travail bien déterminé. Dans notre exemple, nous créerons deux classes internes implémentant chacune
l'interface ActionListener et redéfinissant la méthode actionPerformed() :
Une fois ces opérations effectuées, il ne nous reste plus qu'à indiquer à chaque bouton « qui l'écoute » grâce
à la méthode addActionListener().
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
container.add(pan, BorderLayout.CENTER);
//Ce sont maintenant nos classes internes qui écoutent nos boutons
bouton.addActionListener(new BoutonListener());
bouton2.addActionListener(new Bouton2Listener());
Vous pouvez constater que nos classes internes ont même accès aux attributs déclarés privatedans notre
classe Fenetre.
Dorénavant, nous n'avons plus à nous soucier du bouton qui a déclenché l'événement, car nous disposons
de deux classes écoutant chacune un bouton. Nous pouvons souffler un peu : une grosse épine vient de nous
être retirée du pied.
Vous pouvez aussi faire écouter votre bouton par plusieurs classes. Il vous suffit d'ajouter ces classes
supplémentaires à l'aide d'addActionListener().
Eh oui, faites le test : créez une troisième classe interne et attribuez-lui le nom que vous voulez
(personnellement, je l'ai appelée Bouton3Listener). Implémentez-y l'interface ActionListener et
contentez-vous d'effectuer un simple System.out.println() dans la
méthode actionPerformed(). N'oubliez pas de l'ajouter à la liste des classes qui écoutent votre bouton
(n'importe lequel des deux ; j'ai pour ma part choisi le premier).
//Les imports…
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
container.add(pan, BorderLayout.CENTER);
bouton2.addActionListener(new Bouton2Listener());
//…
Les classes internes sont vraiment des classes à part entière. Elles peuvent également hériter d'une
superclasse. De ce fait, c'est presque comme si nous nous trouvions dans le cas d'un héritage multiple (ce
n'en est pas un, même si cela y ressemble). Ce code est donc valide :
public MaClasseExterne(){
//...
}
Bon, nous avons réglé le problème d'implémentation : nous possédons deux boutons qui sont écoutés. Il ne
nous reste plus qu'à lancer et arrêter notre animation à l'aide de ces boutons. Mais auparavant, nous allons
étudier une autre manière d'implémenter des écouteurs et, par extension, des classes devant redéfinir les
méthodes d'une classe abstraite ou d'une interface.
Les classes anonymes
Il n'y a rien de compliqué dans cette façon de procéder, mais je me rappelle avoir été déconcerté lorsque je
l'ai rencontrée pour la première fois.
Les classes anonymes sont le plus souvent utilisées pour la gestion d'événements ponctuels, lorsque créer
une classe pour un seul traitement est trop lourd. Rappelez-vous ce que j'ai utilisé pour définir le
comportement de mes boutons lorsque je vous ai présenté l'objet CardLayout : c'étaient des classes
anonymes. Pour rappel, voici ce que je vous avais amenés à coder :
Sachez aussi que les classes anonymes peuvent être utilisées pour implémenter des classes abstraites. Je
vous conseille d'effectuer de nouveaux tests en utilisant notre exemple du pattern strategy ; mais cette fois,
plutôt que de créer des classes, créez des classes anonymes.
Les classes anonymes sont soumises aux mêmes règles que les classes « normales » :
Cependant, ces classes possèdent des restrictions à cause de leur rôle et de leur raison d'être :
elles sont automatiquement déclarées final : on ne peut dériver de cette classe, l'héritage est donc
impossible !
Pour parvenir à gérer le lancement et l'arrêt de notre animation, nous allons devoir modifier un peu le code
de notre classe Fenetre. Il va falloir changer le libellé des boutons de notre IHM : le premier
affichera Go et le deuxième Stop. Pour éviter d'interrompre l'animation alors qu'elle n'est pas lancée et de
l'animer quand elle l'est déjà, nous allons tantôt activer et désactiver les boutons. Je m'explique :
au lancement, le bouton Go ne sera pas cliquable alors que le bouton Stop oui ;
si l'animation est interrompue, le bouton Stop ne sera plus cliquable, mais le bouton Go le sera.
Ne vous inquiétez pas, c'est très simple à réaliser. Il existe une méthode gérant ces changements d'état :
Voici un exemple :
Afin de bien gérer notre animation, nous devons améliorer notre méthode go(). Sortons donc de cette
méthode les deux entiers dont nous nous servions afin de recalculer les coordonnées de notre rond. La
boucle infinie doit dorénavant pouvoir être interrompue ! Pour réussir cela, nous allons déclarer un booléen
qui changera d'état selon le bouton sur lequel on cliquera ; nous l'utiliserons comme paramètre de notre
boucle.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
container.add(pan, BorderLayout.CENTER);
bouton.addActionListener(new BoutonListener());
bouton.setEnabled(false);
bouton2.addActionListener(new Bouton2Listener());
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
l'animation se lance ;
Explication de ce phénomène
Java gère les appels aux méthodes grâce à ce que l'on appelle vulgairement la pile.
Pour expliquer cela, prenons un exemple tout bête ; regardez cet objet :
Je suppose que vous avez remarqué avec stupéfaction que l'ordre des instructions est un peu bizarre. Voici
ce qu'il se passe :
celle-ci utilise la méthode 3 : une fois qu'elle a terminé, la JVM retourne dans la méthode 2 ;
Lors de tous les appels, on dit que la JVM empile les invocations sur la pile. Une fois que la dernière
méthode empilée a terminé de s'exécuter, la JVM la dépile.
La figure suivante présente un schéma résumant la situation.
Empilage et dépilage de
méthodes
Dans notre programme, imaginez que la méthode actionPerformed() soit représentée par la méthode
2, et que notre méthode go() soit représentée par la méthode 3. Lorsque nous entrons dans la méthode 3,
nous entrons dans une boucle infinie… Conséquence directe : nous ne ressortons jamais de cette méthode
et la JVM ne dépile plus !
Afin de pallier ce problème, nous allons utiliser un nouveau thread. Grâce à cela, la méthode go() se
trouvera dans une pile à part.
Attends : on arrive pourtant à arrêter l'animation alors qu'elle se trouve dans une boucle infinie.
Pourquoi ?
Tout simplement parce que nous ne demandons d'effectuer qu'une simple initialisation de variable dans la
gestion de notre événement ! Si vous créez une deuxième méthode comprenant une boucle infinie et que
vous l'invoquez lors du clic sur le bouton Stop, vous aurez exactement le même problème.
Je ne vais pas m'éterniser là-dessus, nous verrons cela dans un prochain chapitre. À présent, je pense qu'il
est de bon ton de vous parler du mécanisme d'écoute d'événements, le fameux pattern observer.
Le design pattern Observer est utilisé pour gérer les événements de vos IHM. C'est une technique de
programmation. La connaître n'est pas absolument indispensable, mais cela vous aide à mieux comprendre
le fonctionnement de Swing et AWT. C'est par ce biais que vos composants effectueront quelque chose
lorsque vous les cliquerez ou les survolerez.
Sachant que vous êtes des développeurs Java chevronnés, un de vos amis proches vous demande si vous
êtes en mesure de l'aider à réaliser une horloge digitale en Java. Il a en outre la gentillesse de vous fournir
les classes à utiliser pour la création de son horloge. Votre ami a l'air de s'y connaître, car ce qu'il vous a
fourni est bien structuré.
package com.sdz.vue;
import java.awt.BorderLayout;
import java.awt.Font;
import javax.swing.JFrame;
import javax.swing.JLabel;
import com.sdz.model.Horloge;
public Fenetre(){
//On initialise la JFrame
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
this.setResizable(false);
this.setSize(200, 80);
//On initialise l'horloge
this.horloge = new Horloge();
//On initialise le JLabel
Font police = new Font("DS-digital", Font.TYPE1_FONT, 30);
this.label.setFont(police);
this.label.setHorizontalAlignment(JLabel.CENTER);
//On ajoute le JLabel à la JFrame
this.getContentPane().add(this.label, BorderLayout.CENTER);
}
package com.sdz.model;
import java.util.Calendar;
Je ne vois pas où est le problème : il n'a qu'à passer son instance de JLabel dans son objetHorloge, et le
tour est joué !
En réalité, votre ami, dans son infinie sagesse, souhaite - je le cite - que l'horloge ne dépende pas de son
interface graphique, juste au cas où il devrait passer d'une IHM swing à une IHM awt.
Il est vrai que si l'on passe l'objet d'affichage dans l'horloge, dans le cas où l'on change le type de l'IHM,
toutes les classes doivent être modifiées ; ce n'est pas génial. En fait, lorsque vous procédez de la sorte, on
dit que vous couplez des objets : vous rendez un ou plusieurs objets dépendants d'un ou de plusieurs autres
objets (entendez par là que vous ne pourrez plus utiliser les objets couplés indépendamment des objets
auxquels ils sont attachés).
Le couplage entre objets est l'un des problèmes principaux relatifs à la réutilisation des objets. Dans notre
cas, si vous utilisez l'objet Horloge dans une autre application, vous serez confrontés à plusieurs
problèmes étant donné que cet objet ne s'affiche que dans un JLabel.
C'est là que le pattern observer entre en jeu : il fait communiquer des objets entre eux sans qu'ils se
connaissent réellement ! Vous devez être curieux de voir comment il fonctionne, je vous propose donc de
l'étudier sans plus tarder.
Ce sont deux points cruciaux, mais un autre élément, que vous ne connaissez pas encore, va vous plaire :
tout se fait automatiquement !
Comment les choses vont-elles alors se passer ? Réfléchissons à ce que nous voulons que notre horloge
digitale effectue : elle doit pouvoir avertir l'objet servant à afficher l'heure lorsqu'il doit rafraîchir son
affichage. Puisque les horloges du monde entier se mettent à jour toutes les secondes, il n'y a aucune raison
pour que la nôtre ne fasse pas de même.
Ce qui est merveilleux avec ce pattern, c'est que notre horloge ne se contentera pas d'avertir un seul objet
que sa valeur a changé : elle pourra en effet mettre plusieurs observateurs au courant !
En fait, pour faire une analogie, interprétez la relation entre les objets implémentant le pattern observer
comme un éditeur de journal et ses clients (voir figure suivante).
Livreur de journaux
Grâce à ce schéma, vous pouvez sentir que notre objet défini comme observable pourra être surveillé par
plusieurs objets : il s'agit d'une relation dite de un à plusieurs vers l'objet Observateur. Avant de vous
expliquer plus en détail le fonctionnement de ce pattern, jetez un œil au diagramme de classes de notre
application en figure suivante.
Diagramme de classes du pattern observer
Ce diagramme indique que ce ne sont pas les instances d'Horloge ou de JLabel que nous allons utiliser,
mais des implémentations d'interfaces.
En effet, vous savez que les classes implémentant une interface peuvent être définies par le type de
l'interface. Dans notre cas, la classe Fenetre implémentera l'interface Observateur : nous pourrons la
voir comme une classe du type Observateur. Vous avez sans doute remarqué que la deuxième interface
- celle dédiée à l'objet Horloge - possède trois méthodes :
une permettant d'ajouter des observateurs (nous allons donc gérer une collection d'observateurs) ;
Grâce à cela, nos objets ne sont plus liés par leurs types, mais par leurs interfaces ! L'interface qui apportera
les méthodes de mise à jour, d'ajout d'observateurs, etc. travaillera donc avec des objets de
typeObservateur.
Ainsi, le couplage ne s'effectue plus directement, il s'opère par le biais de ces interfaces. Ici, il faut que nos
deux interfaces soient couplées pour que le système fonctionne. De même que, lorsque je vous ai présenté
le pattern decorator, nos classes étaient très fortement couplées puisqu'elles devaient travailler ensemble :
nous devions alors faire en sorte de ne pas les séparer.
à partir de là, notre objet Horloge fera le reste : à chaque changement, nous appellerons la
méthode mettant tous les observateurs à jour.
Le code source de ces interfaces se trouve ci-dessous (notez que j'ai créé un
package com.sdz.observer).
Observateur.java
package com.sdz.observer;
Observer.java
package com.sdz.observer;
Horloge.java
package com.sdz.model;
import java.util.ArrayList;
import java.util.Calendar;
import com.sdz.observer.Observable;
import com.sdz.observer.Observateur;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Fenetre.java
package com.sdz.vue;
import java.awt.BorderLayout;
import java.awt.Font;
import javax.swing.JFrame;
import javax.swing.JLabel;
import com.sdz.model.Horloge;
import com.sdz.observer.Observateur;
public Fenetre(){
//On initialise la JFrame
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
this.setResizable(false);
this.setSize(200, 80);
Vous pouvez voir que lorsque l'heure change, la méthode updateObservateur() est invoquée. Celle-ci
parcourt la collection d'objets Observateur et appelle sa méthode update(String hour). La
méthode étant redéfinie dans notre classe Fenetre afin de mettre JLabel à jour, l'heure s'affiche !
J'ai mentionné que ce pattern est utilisé dans la gestion événementielle d'interfaces graphiques. Vous avez
en outre remarqué que leurs syntaxes sont identiques. En revanche, je vous ai caché quelque chose : il
existe des classes Java permettant d'utiliser le pattern observer sans avoir à coder les interfaces.
En réalité, il existe une classe abstraite Observable et une interface Observer fournies dans les classes
Java.
Celles-ci fonctionnent de manière quasiment identique à notre façon de procéder, seuls quelques détails
diffèrent. Personnellement, je préfère de loin utiliser un pattern observer « fait maison ».
Pourquoi cela ? Tout simplement parce que l'objet que l'on souhaite observer doit hériter de la
classeObservable. Par conséquent, il ne pourra plus hériter d'une autre classe étant donné que Java ne
gère pas l'héritage multiple. La figure suivante présente la hiérarchie de classes du pattern observer présent
dans Java.
Diagramme de classes du pattern
observer de Java
Vous remarquez qu'il fonctionne presque de la même manière que celui que nous avons développé. Il y a
toutefois une différence dans la méthode update(Observable obs, Object obj) : sa signature a
changé.
Cette méthode prend ainsi deux paramètres :
un objet Observable ;
un Object représentant une donnée supplémentaire que vous souhaitez lui fournir.
Vous connaissez le fonctionnement de ce pattern, il vous est donc facile de comprendre le schéma. Je vous
invite cependant à effectuer vos propres recherches sur son implémentation dans Java : vous verrez qu'il
existe des subtilités (rien de méchant, cela dit).
Terminons par une version améliorée de notre bouton qui reprend ce que nous avons appris :
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JButton;
Lorsque vous implémentez une interface < … >Listener, vous indiquez à votre classe qu'elle
doit se préparer à observer des événements du type de l'interface. Vous devez donc spécifier qui
doit observer et ce que la classe doit observer grâce aux méthodes de type add< …
>Listener(< … >Listener).
Une classe interne est une classe se trouvant à l'intérieur d'une classe.
Une telle classe a accès à toutes les données et méthodes de sa classe externe.
La JVM traite les méthodes appelées en utilisant une pile qui définit leur ordre d'exécution.
Une méthode est empilée à son invocation, mais n'est dépilée que lorsque toutes ses instructions
ont fini de s'exécuter.
Le pattern observer permet d'utiliser des objets faiblement couplés. Grâce à ce pattern, les objets
restent indépendants.
TP : UNE CALCULATRICE
Ah ! Ça faisait longtemps… Un petit TP ! Dans celui-ci, nous allons - enfin, vous allez - pouvoir réviser
tout ce qui a été vu au cours de cette partie :
les fenêtres ;
les conteneurs ;
les boutons ;
les interactions ;
Élaboration
Nous allons tout de suite voir ce dont notre calculatrice devra être capable :
Faire des calculs à la chaîne, par exemple : 1 + 2 + ... ; lorsqu'on clique à nouveau sur un
opérateur, il faut afficher le résultat du calcul précédent.
un booléen pour savoir si nous devons effacer ce qui figure à l'écran et écrire un nouveau
nombre ;
nous allons utiliser une variable de type double pour nos calculs ;
Pour alléger le nombre de classes internes, vous pouvez en créer une qui se chargera d'écrire ce qui doit être
affiché à l'écran. Utilisez la méthode getSource() pour savoir sur quel bouton on a cliqué.
Je ne vais pas tout vous dire, il faut que vous cherchiez par vous-mêmes : la réflexion est très importante !
En revanche, vous devez savoir que la correction que je vous fournis n'est pas la correction. Il y a plusieurs
solutions possibles. Je vous propose seulement l'une d'elles.
Allez, au boulot !
Correction
Vous avez bien réfléchi ? Vous vous êtes brûlé quelques neurones ? Vous avez mérité votre correction !
Regardez bien comment tout interagit, et vous comprendrez comment fonctionne ce code.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public Calculatrice(){
this.setSize(240, 260);
this.setTitle("Calculette");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
this.setResizable(false);
//On initialise le conteneur avec tous les composants
initComposant();
//On ajoute le conteneur
this.setContentPane(container);
this.setVisible(true);
}
Tout d'abord, qu'est-ce qu'un .jar ? C'est une extension propre aux archives Java (Java ARchive). Ce type
de fichier contient tout ce dont a besoin la JVM pour lancer un programme. Une fois votre archive créée, il
vous suffit de double-cliquer sur celle-ci pour lancer l'application. C'est le meilleur moyen de distribuer
votre programme.
C'est exact pour peu que vous ayez ajouté les exécutables de votre JRE (présents dans le répertoirebin)
dans votre variable d'environnement PATH ! Si ce n'est pas le cas, refaites un tour dans le premier chapitre
du livre, section « Compilation en ligne de commande », et remplacez le répertoire du JDK par celui du
JRE (si vous n'avez pas téléchargé le JDK ; sinon, allez récupérer ce dernier).
La création d'un .jar est un jeu d'enfant. Commencez par effectuer un clic droit sur votre projet et
choisissez l'option Export, comme le montre la figure suivante.
Exporter son
projet
Vous voici dans la gestion des exports. Eclipse vous demande quel type d'export vous souhaitez réaliser,
comme à la figure suivante.
Dans le premier cadre, sélectionnez tous les fichiers qui composeront votre exécutable .jar.
Dans le second cadre, indiquez à Eclipse l'endroit où créer l'archive et le nom vous souhaitez
lui donner.
La page suivante n'est pas très pertinente ; je la mets cependant en figure suivante afin de ne perdre
personne.
Choix
du niveau d'erreurs tolérable
Cliquez sur Next : vous arrivez sur la page qui vous demande de spécifier l'emplacement de la
méthodemain dans votre programme (figure suivante).
Choix du point de
départ du programme
Cliquez sur Browse… pour afficher un pop-up listant les fichiers des programmes contenant une
méthodemain. Ici, nous n'en avons qu'une (voir figure suivante). Souvenez-vous qu'il est possible que
plusieurs méthodes main soient déclarées, mais une seule sera exécutée !
Notre
méthode main
Sélectionnez le point de départ de votre application et validez. La figure suivante correspond à ce que vous
devriez obtenir.
Récapitulatif d'export
Vous pouvez maintenant cliquer sur Finish et voir s'afficher un message ressemblant à celui de la figure
suivante.
Message
lors de l'export
Ce type de message n'est pas alarmant : il vous signale qu'il existe des éléments qu'Eclipse ne juge pas très
clairs. Ils n'empêcheront toutefois pas votre application de fonctionner, contrairement à un message d'erreur
que vous repérerez facilement : il est en rouge.
Une fois cette étape validée, vous pouvez voir avec satisfaction qu'un fichier .jar a bien été généré dans le
dossier spécifié, comme à la figure suivante.
Les threads sont des fils d'exécution de notre programme. Lorsque nous en créons plusieurs, nous pouvons
exécuter des tâches simultanément.
Nous en étions restés à notre animation qui bloque, et je vous avais dit que la solution était d'utiliser un
deuxième Thread. Dans ce chapitre, nous allons voir comment créer une (ou plusieurs) nouvelle(s) pile(s)
de fonctions grâce à ces fameux threads. Il existe une classe Thread dans Java permettant leur gestion.
Vous allez voir qu'il existe deux façons de créer un nouveau thread.
Voyez un thread comme une machine bien huilée capable d'effectuer les tâches que vous lui spécifiez. Une
fois instancié, un thread attend son lancement. Dès que c'est fait, il invoque sa méthode run() qui va lui
permettre de connaître les tâches qu'il a à effectuer.
Nous allons maintenant apprendre à créer un nouveau thread. Je l'avais mentionné dans l'introduction, il
existe deux manières de faire :
Comme je vous le disais, nous allons opter pour la première solution. Tout ce que nous avons à faire, c'est
redéfinir la méthode run() de notre objet afin qu'il sache ce qu'il doit faire. Puisque nous allons en utiliser
plusieurs, autant pouvoir les différencier : nous allons leur donner des noms.
Créons donc une classe gérant tout cela qui contient un constructeur comprenant un String en paramètre
pour spécifier le nom du thread. Cette classe doit également comprendre une méthode getName() afin de
retourner ce nom. La classe Thread se trouvant dans le package java.lang, aucune
instructionimport n'est nécessaire. En voici le code :
Notez qu'avec les processeurs multi-coeurs aujourd'hui, il est désormais possible d'exécuter deux tâches
exactement en même temps. Tout dépend donc de votre ordinateur.
Un thread peut présenter plusieurs états :
TERMINATED : lorsque le thread a effectué toutes ses tâches ; on dit aussi qu'il est « mort ».
Vous ne pouvez alors plus le relancer par la méthode start().
TIMED_WAITING : lorsque le thread est en pause (quand vous utilisez la méthode sleep(),
par exemple).
BLOCKED : lorsque l'ordonnanceur place un thread en sommeil pour en utiliser un autre, il lui
impose cet état.
Un thread est considéré comme terminé lorsque la méthode run() est ôtée de sa pile d'exécution. En effet,
une nouvelle pile d'exécution contient à sa base la méthode run() de notre thread. Une fois celle-ci
dépilée, notre nouvelle pile est détruite !
En fait, le thread principal crée un second thread qui se lance et construit une pile dont la base est sa
méthode run() ; celle-ci appelle une méthode, l'empile, effectue toutes les opérations demandées, et une
fois qu'elle a terminé, elle dépile cette dernière. La méthode run() prend fin, la pile est alors détruite.
Nous allons modifier notre classe TestThread afin d'afficher les états de nos threads que nous pouvons
récupérer grâce à la méthode getState().
Dans le jeu d'essais, vous pouvez voir les différents statuts qu'ont pris les threads. Ainsi, le premier est dans
l'état BLOCKED lorsque le second est en cours de traitement, ce qui justifie ce que je vous disais : les
threads ne s'exécutent pas en même temps !
Vous pouvez voir aussi que les opérations effectuées par nos threads sont en fait codées dans la
méthoderun(). Reprenez l'image que j'ai montrée précédemment : « un thread est une machine bien huilée
capable d'effectuer les tâches que vous lui spécifiez ». Faire hériter un objet de Thread permet de créer un
nouveau thread très facilement. Vous pouvez cependant procéder différemment : redéfinir uniquement ce
que doit effectuer le nouveau thread grâce à l'interface Runnable. Dans ce cas, ma métaphore prend tout
son sens : vous ne redéfinissez que ce que doit faire la machine, et non pas la machine tout entière !
Ne redéfinir que les tâches que le nouveau thread doit effectuer comprend un autre avantage : la classe dont
nous disposons n'hérite d'aucune autre ! Eh oui : dans notre test précédent, la classe TestThread ne
pourra plus hériter d'une classe, tandis qu'avec une implémentation de Runnable, rien n'empêche notre
classe d'hériter de JFrame, par exemple…
Trêve de bavardages : codons notre implémentation de Runnable. Vous ne devriez avoir aucun problème
à y parvenir, sachant qu'il n'y a que la méthode run() à redéfinir.
Afin d'illustrer cela, nous allons utiliser un exemple que j'ai trouvé intéressant lorsque j'ai appris à me servir
des threads : nous allons créer un objet CompteEnBanque contenant une somme d'argent par défaut
(disons 100), une méthode pour retirer de l'argent (retraitArgent()) et une méthode retournant le
solde (getSolde()). Cependant, avant de retirer de l'argent, nous vérifierons que nous ne sommes pas à
découvert… Notre thread va effectuer autant d'opérations que nous le souhaitons. La figure suivante
représente le diagramme de classes résumant la situation.
Thread
et compte en banque
Je résume :
RunImpl.java
CompteEnBanque.java
return this.solde;
}
Test.java
Tout est dans le titre ! En fait, ce qu'il faut faire, c'est indiquer à la JVM qu'un thread est en train d'utiliser
des données qu'un autre thread est susceptible d'altérer.
Ainsi, lorsque l'ordonnanceur met en sommeil un thread qui traitait des données utilisables par un autre
thread, ce premier thread garde la priorité sur les données et tant qu'il n'a pas terminé son travail, les autres
threads n'ont pas la possibilité d'y toucher.
Cela s'appelle synchroniser les threads. Cette opération est très délicate et demande beaucoup de
compétences en programmation… Voici à quoi ressemble notre
méthode retraitArgent()synchronisée :
Je récapitule une nouvelle fois, en me servant d'un exemple simple. Je serai représenté par le thread A, vous
par le thread B, et notre boulangerie favorite par la méthode synchronisée M. Voici ce qu'il se passe :
c'est là que le thread B (vous) cherche aussi à utiliser la méthode M ; cependant, elle est déjà
occupée par un thread (moi) ;
l'action revient sur moi (thread A) ; au moment de payer, je dois chercher de la monnaie dans
ma poche ;
l'action revient sur le thread B (vous)… mais la méthode M n'est toujours pas libérée du thread
A, vous êtes donc remis en attente ;
on revient sur le thread A qui arrive enfin à payer et à quitter la boulangerie : la méthode M est
maintenant libérée ;
Dans un contexte informatique, il peut être pratique et sécurisé d'utiliser des threads et des méthodes
synchronisées lors d'accès à des services distants tels qu'un serveur d'applications ou un SGBD (Système de
Gestion de Base de Données).
Je vous propose maintenant de retourner à notre animation, qui n'attend plus qu'un petit thread pour
fonctionner correctement !
À partir d'ici, il n'y a rien de bien compliqué. Il nous suffit de créer un nouveau thread lorsqu'on clique sur
le bouton Go en lui passant une implémentation de Runnable en paramètre qui, elle, va appeler la
méthode go() (n'oublions pas de remettre le booléen de contrôle à true).
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public Fenetre(){
//Le constructeur n'a pas changé
}
La version 7 de Java met à disposition des développeurs plusieurs classes qui permettent de mettre en
application ce qu'on appelle « le pattern Fork/Join ». Ce dernier n'est rien de plus que la mise en application
d'un vieil adage que vous devez connaître : divisez pour mieux régner ! Dans certains cas, il serait bon de
pouvoir découper une tâche en plusieurs sous-tâches, faire en sorte que ces sous-tâches s'exécutent en
parallèle et pouvoir récupérer le résultat de tout ceci une fois que tout est terminé. C'est exactement ce qu'il
est possible de faire avec ces nouvelles classes. Je vous préviens, c'est un peu difficile à comprendre mais
c'est vraiment pratique.
la machine qui exécutera la tâche devra posséder un processeur à plusieurs cœurs (2, 4 ou plus)
;
s'assurer qu'il y a un réel gain de performance ! Dans certains cas, découper une tâche rend le
traitement plus long.
En guise d'exemple, je vous propose de coder une recherche de fichiers (simplifiée au maximum pour ne
pas surcharger le code). Voici les classes que nous allons utiliser, pour le moment sans la gestion Fork/Join
:
ScanException.java
FolderScanner.java
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
public FolderScanner(){ }
public FolderScanner(Path p, String f){
path = p;
filter = f;
}
/**
* Méthode qui se charge de scanner les dossiers de façon récursive
* @throws ScanException
*/
public long sequentialScan() throws ScanException{
//Si le chemin n'est pas valide, on lève une exception
if(path == null || path.equals(""))
throw new ScanException("Chemin à scanner non valide (vide ou null) !");
return result;
}
}
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main {
try {
Long start = System.currentTimeMillis();
Long resultat = fs.sequentialScan();
Long end = System.currentTimeMillis();
System.out.println("Il y a " + resultat + " fichier(s) portant l'extension " +
filtre);
System.out.println("Temps de traitement : " + (end - start));
} catch (ScanException e) {
e.printStackTrace();
}
}
}
Lorsque je lance ce code le temps de traitement est vraiment long (j'ai beaucoup de dossiers dans mes
documents ), comme le montre la figure suivante.
RecursiveTask<V> : identique à la classe précédente mais retourne une valeur, de type <V>,
en fin de traitement. C'est cette classe que nous allons utiliser pour pouvoir nous retourner le
nombre de fichiers trouvés.
Nous allons devoir utiliser, en plus de l'objet de découpage, un objet qui aura pour rôle de superviser
l'exécution des tâches et sous-tâches afin de pouvoir fusionner les threads en fin de traitement
:ForkJoinPool.
Avant de vous présenter le code complet, voici comment ça fonctionne. Les objets qui permettent le
découpage en sous-tâches fournissent trois méthodes qui permettent cette gestion :
compute() : méthode abstraite à redéfinir dans l'objet héritant afin de définir le traitement à
effectuer ;
fork() : méthode qui crée un nouveau thread dans le pool de thread (ForkJoinPool) ;
Ces classes nécessitent que vous redéfinissiez la méthode compute() afin de définir ce qu'il y a à faire. La
figure suivante est un schéma représentant la façon dont les choses se passent.
Plusieurs sous-
tâches s'exécutent
Concrètement, avec notre exemple, voici ce qu'il va se passer :
notre objet qui sert à scanner le contenu va vérifier le contenu pour voir s'il n'y a pas de sous-
dossiers ;
pour chaque sous-dossier, nous allons créer une nouvelle tâche et la lancer ;
nous allons compter le nombre de fichiers qui correspond à nos critères dans le dossier en cours
de scan ;
nous allons récupérer le nombre de fichiers trouvés par les exécutions en tâche de fond ;
Pour que vous compreniez bien, voici une partie de mon dossier Mes Documents :
Une partie de mon dossier « Mes Documents »
Et voici concrètement ce qu'il va se passer :
Voici comment va s'exécuter le code
Nous pouvons maintenant voir la partie code.
FolderScanner.java
public FolderScanner(){ }
public FolderScanner(Path p, String f){
path = p;
filter = f;
}
/**
* Notre méthode de scan en mode mono thread
* @throws ScanException
*/
public long sequentialScan() throws ScanException{
//Si le chemin n'est pas valide, on lève une exception
if(path == null || path.equals(""))
throw new ScanException("Chemin à scanner non valide (vide ou null) !");
return result;
}
/**
* Méthode que nous allons utiliser pour les traitements
* en mode parallèle.
* @throws ScanException
*/
public long parallelScan() throws ScanException{
//List d'objet qui contiendra les sous-tâches créées et lancées
List<FolderScanner> list = new ArrayList<>();
//On compte maintenant les fichiers, correspondant au filtre, présents dans ce dossier
try(DirectoryStream<Path> listing = Files.newDirectoryStream(path, this.filter)){
for(Path nom : listing){
result++;
}
} catch (IOException e) {
e.printStackTrace();
}
/**
* Méthode qui défini l'action à faire
* dans notre cas, nous lan çons le scan en mode parallèles
*/
protected Long compute() {
long resultat = 0;
try {
resultat = this.parallelScan();
} catch (ScanException e) {
e.printStackTrace();
}
return resultat;
}
}
Et voici maintenant notre classe de test :
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ForkJoinPool;
//Création de notre tâche principale qui se charge de découper son travail en sous-
tâches
FolderScanner fs = new FolderScanner(chemin, filtre);
Dans cet exemple nous avons créé dynamiquement autant de threads que nécessaires pour traiter nos
tâches. Vous n'aurez peut-être pas besoin de faire ceci pour des problèmes où seulement 2 ou 3 sous-tâches
suffisent, surtout si vous le savez à l'avance. L'idée maîtresse revient à définir un seuil au delà duquel le
traitement se fera en mode Fork/join, sinon, il se fera dans un seul thread (je vous rappelle qu'il se peut que
ce mode de fonctionnement soit plus lent et consommateur qu'en mode normal). Voici comment procéder
dans ce genre de cas :
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
Les opérations que vous souhaitez effectuer dans une autre pile d'exécution sont à placer dans
la méthode run(), qu'il s'agisse d'une classe héritant de Thread ou d'une implémentation
deRunnable.
Pour protéger l'intégrité des données accessibles à plusieurs threads, utilisez le mot
clésynchronized dans la déclaration de vos méthodes.
Un thread est déclaré mort lorsqu'il a dépilé la méthode run() de sa pile d'exécution.
Continuons à explorer les objets que nous propose swing. Ils sont variés et s'utilisent souvent de la
même manière que les boutons. En fait, maintenant que nous avons compris le fonctionnement du
pattern observer, nous travaillerons avec des interfaces et devrons donc implémenter des méthodes pour
gérer les événements avec nos composants.
Allons-y !
Première utilisation
Comme à l'accoutumée, nous utiliserons d'abord cet objet dans un contexte exempt de tout code superflu.
Créons donc un projet avec une classe contenant la méthode main() et une classe héritée de JFrame.
Dans cet exemple, nous aurons bien sûr besoin d'une liste, faites-en une. Cependant, vous ne manquerez
pas de constater que notre objet est ridiculement petit. Vous connaissez le remède : il suffit de lui spécifier
une taille !
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
combo.setPreferredSize(new Dimension(100, 20));
Sachez que lorsque l'objet affiche les éléments ajoutés, il appelle leur méthode toString(). Dans cet
exemple, nous avons utilisé des objets String, mais essayez avec un autre objet et vous constaterez le
résultat…
Voici le nouveau code :
public Fenetre(){
//…
combo.setPreferredSize(new Dimension(100, 20));
combo.addItem("Option 1");
combo.addItem("Option 2");
combo.addItem("Option 3");
combo.addItem("Option 4");
//…
}
}
Vous pouvez voir ce que ça donne à la figure suivante.
Pour initialiser une JComboBox, vous pouvez utiliser le constructeur prenant un tableau d'objets en
paramètre afin de renseigner tous les éléments d'un coup. Ceci est donc équivalent au code précédent :
String[] tab = {"Option 1", "Option 2", "Option 3", "Option 4"};
combo = new JComboBox(tab);
Vous pouvez assigner un choix par défaut avec la méthode setSelectedIndex(int index). Vous
avez aussi la possibilité de changer la couleur du texte, la couleur de fond ou la police, exactement comme
avec unJLabel.
Depuis Java 7, l'objet JComboBox peut être paramétré avec un type générique, comme ceci
:JComboBox<String> combo = new JComboBox<String>(); ce qui permet de mieux gérer le
contenu de nos listes et ainsi mieux récupérer les valeurs de ces dernières.
Un autre objet dont nous ne parlerons pas accepte aussi un type paramétré, l'objet JList<E>. Celui-ci
étant très proche de l'objet JComboBox<E>, nous n'en parlerons pas ici mais maintenant vous savez qu'il
existe.
Maintenant que nous savons comment fonctionne cet objet, nous allons apprendre à communiquer avec lui.
L'interface ItemListener
Cette interface possède une méthode à redéfinir. Celle-ci est appelée lorsqu'un élément a changé d'état.
Puisqu'un exemple est toujours plus éloquent, voici un code implémentant cette interface :
public Fenetre(){
//Le début ne change pas
//Ajout du listener
combo.addItemListener(new ItemState());
combo.setPreferredSize(new Dimension(100, 20));
combo.setForeground(Color.blue);
Vous voyez que lorsque nous cliquons sur une autre option, notre objet commence par modifier l'état de
l'option précédente (l'état passe en DESELECTED) avant de changer celui de l'option choisie (celle-ci passe
à l'état SELECTED). Nous pouvons donc suivre très facilement l'état de nos éléments grâce à cette
interface ; cependant, pour plus de simplicité, nous utiliserons l'interface ActionListener afin de
récupérer l'option sélectionnée.
Vous constatez qu'en utilisant cette méthode, nous pouvons récupérer l'option sur laquelle l'action a été
effectuée. L'appel de la méthode getSelectedItem() retourne la valeur de l'option sélectionnée ; une
fois récupérée, nous pouvons travailler avec notre liste !
Maintenant que nous savons comment récupérer les informations dans une liste, je vous invite à
continuer notre animation.
Comme le titre l'indique, nous allons faire en sorte que notre animation ne se contente plus d'afficher un
rond : nous pourrons désormais choisir la forme que nous voulons afficher. Bien sûr, je ne vais pas vous
faire réaliser toutes les formes possibles et imaginables ; je vous en fournis quelques-unes et, si le cœur
vous en dit, vous pouvez ajouter des formes de votre composition.
Très bien : pour réaliser cela, nous devons dynamiser un peu notre classe Panneau afin qu'elle peigne
une forme en fonction de notre choix.
Pour y parvenir, nous allons ajouter une variable d'instance de type String qui contiendra l'intitulé de la
forme que nous souhaitons dessiner - appelons-la forme - ainsi qu'un mutateur permettant de redéfinir
cette variable.
Notre méthode paintComponent() doit pouvoir dessiner la forme demandée ; ainsi, trois cas de figure
se profilent :
soit nous intégrons les instructions if dans cette méthode et l'objet Graphics dessinera en
fonction de la variable ;
soit nous développons une méthode privée appelée dans la méthode paintComponent() et qui
dessinera la forme demandée ;
soit nous utilisons le pattern strategy afin d'encapsuler la façon dont nous dessinerons nos formes
dans notre animation.
Le pattern strategy est de loin la meilleure solution, mais afin de ne pas alourdir nos exemples, nous
travaillerons « à l'ancienne ».
Nous allons donc développer une méthode privée - appelons-la draw(Graphics g) - qui aura pour tâche
de dessiner la forme voulue. Nous passerons l'objet Graphics dans la méthode paintComponent() de
sorte que cette dernière puisse l'utiliser ; c'est donc dans cette méthode que nous placerons nos conditions.
le carré ;
le triangle ;
Cela signifie que notre liste contiendra ces quatre choix et que le rond figurera en premier lieu. Nous
créerons aussi une implémentation d'ActionListener dans une classe interne pour gérer les actions
de notre liste. Je l'ai appelée FormeListener (c'est fou ce que je suis original).
Essayez de réaliser ces formes vous-mêmes : il n'y a là rien de compliqué, je vous assure ! Bon, l'étoile est
peut-être un peu plus complexe que les autres, mais ce n'est pas insurmontable.
Classe Panneau
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JPanel;
Classe Fenetre
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
container.add(pan, BorderLayout.CENTER);
bouton.addActionListener(new BoutonListener());
bouton2.addActionListener(new Bouton2Listener());
bouton2.setEnabled(false);
JPanel south = new JPanel();
south.add(bouton);
south.add(bouton2);
container.add(south, BorderLayout.SOUTH);
combo.addItem("ROND");
combo.addItem("CARRE");
combo.addItem("TRIANGLE");
combo.addItem("ETOILE");
combo.addActionListener(new FormeListener());
Première utilisation
Créez un projet vide avec une classe contenant une méthode main() et une classe héritant de JFrame.
Cela fait, nous allons utiliser notre nouvel objet. Celui-ci peut être instancié avec un String en paramètre
qui servira de libellé.
Nous pouvons également cocher la case par défaut en appelant la méthode setSelected(Boolean
bool) à laquelle nous passons true. Cet objet possède, comme tous les autres, une multitude de
méthodes nous simplifiant la vie ; je vous invite aussi à fouiner un peu…
Nous créerons directement une implémentation de l'interface ActionListener, vous connaissez bien la
démarche. Contrôlons également que notre objet est coché à l'aide de la méthode isSelected() qui
retourne un booléen. Voici un code mettant tout cela en œuvre :
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
JPanel top = new JPanel();
check1.addActionListener(new StateListener());
check2.addActionListener(new StateListener());
top.add(check1);
top.add(check2);
container.add(top, BorderLayout.NORTH);
this.setContentPane(container);
this.setVisible(true);
}
Ici, je me suis amusé à cocher et décocher mes cases. Il n'y a rien de bien difficile, ça devient routinier, non
?
Nous allons utiliser cet objet afin que nos formes changent de taille et proposent un pseudo-effet de
morphing.
Premièrement, la taille de notre forme est fixe, il nous faut changer cela. Allez, hop, une variable de
typeint dans notre classe Panneau - disons drawSize - initialisée à 50. Tout comme avec le
déplacement, nous devons savoir lorsqu'il faut augmenter ou réduire la taille de notre forme : nous
utiliserons donc la même méthode que celle que nous avions développée à ce moment-là.
En ce qui concerne la taille, si on la réduit ou l'augmente d'une unité à chaque rafraîchissement, l'effet de
morphing sera ultra rapide. Donc, pour ralentir l'effet, nous utiliserons une méthode retournant 1 ou 0 selon
le nombre de rafraîchissements. Cela implique que nous aurons besoin d'une variable pour les dénombrer.
Nous effectuerons une augmentation ou une réduction toutes les dix fois.
Pour bien séparer les deux cas de figure, nous insérerons une deuxième méthode de dessin dans la
classePanneau qui aura pour rôle de dessiner le morphing ; appelons-la drawMorph(Graphics g).
Lorsque nous cocherons la case, le morphing s'activera, et il se désactivera une fois décochée. La
classePanneau devra donc disposer d'un mutateur pour le booléen de morphing.
Souvenez-vous que nous gérons la collision avec les bords dans notre classe Fenetre. Cependant, en «
mode morphing », la taille de notre forme n'est plus constante : il faudra gérer ce nouveau cas de figure
dans notre méthode go(). Notre classe Panneau devra posséder un accesseur permettant de retourner
la taille actuelle de la forme.
Vous avez désormais toutes les clés en main pour réussir cette animation.
La figure suivante donne un aperçu de ce que vous devriez obtenir (je n'ai représenté que le rond et le
triangle, mais ça fonctionne avec toutes les formes).
Morphing
Fichier Panneau.java
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JPanel;
if(this.forme.equals("ROND")){
g.fillOval(posX, posY, drawSize, drawSize);
}
if(this.forme.equals("CARRE")){
g.fillRect(posX, posY, drawSize, drawSize);
}
if(this.forme.equals("TRIANGLE")){
int s1X = posX + drawSize/2;
int s1Y = posY;
int s2X = posX + drawSize;
int s2Y = posY + drawSize;
int s3X = posX;
int s3Y = posY + drawSize;
int[] ptsX = {s1X, s2X, s3X};
int[] ptsY = {s1Y, s2Y, s3Y};
g.fillPolygon(ptsX, ptsY, 3);
}
if(this.forme.equals("ETOILE")){
int s1X = posX + drawSize/2;
int s1Y = posY;
int s2X = posX + drawSize;
int s2Y = posY + drawSize;
g.drawLine(s1X, s1Y, s2X, s2Y);
int s3X = posX;
int s3Y = posY + drawSize/3;
g.drawLine(s2X, s2Y, s3X, s3Y);
int s4X = posX + drawSize;
int s4Y = posY + drawSize/3;
g.drawLine(s3X, s3Y, s4X, s4Y);
int s5X = posX;
int s5Y = posY + drawSize;
g.drawLine(s4X, s4Y, s5X, s5Y);
g.drawLine(s5X, s5Y, s1X, s1Y);
}
}
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
container.add(pan, BorderLayout.CENTER);
bouton.addActionListener(new BoutonListener());
bouton2.addActionListener(new Bouton2Listener());
bouton2.setEnabled(false);
JPanel south = new JPanel();
south.add(bouton);
south.add(bouton2);
container.add(south, BorderLayout.SOUTH);
combo.addItem("ROND");
combo.addItem("CARRE");
combo.addItem("TRIANGLE");
combo.addItem("ETOILE");
combo.addActionListener(new FormeListener());
morph.addActionListener(new MorphListener());
if(!backX) pan.setPosX(++x);
else pan.setPosX(--x);
if(!backY) pan.setPosY(++y);
else pan.setPosY(--y);
pan.repaint();
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Le voici, le cousin éloigné… Le principe est de proposer au moins deux choix, mais de ne permettre d'en
sélectionner qu'un à la fois. L'instanciation se fait de la même manière que pour un JCheckBox ; d'ailleurs,
nous utiliserons l'exemple du début de ce chapitre en remplaçant les cases à cocher par des boutons radio.
Voici le code correspondant :
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
JPanel top = new JPanel();
jr1.addActionListener(new StateListener());
jr2.addActionListener(new StateListener());
top.add(jr1);
top.add(jr2);
container.add(top, BorderLayout.NORTH);
this.setContentPane(container);
this.setVisible(true);
}
Vous pouvez voir que cet objet s'utilise de la même manière que le précédent. Le problème, ici, c'est que
nous pouvons sélectionner les deux options (alors que ce n'est normalement pas possible). Pour qu'un seul
bouton radio soit sélectionné à la fois, nous devons définir un groupe de boutons à l'aide
de ButtonGroup. Nous y ajouterons nos boutons radio, et seule une option pourra alors être sélectionnée.
public Fenetre(){
//Les autres instructions
jr1.setSelected(true);
jr1.addActionListener(new StateListener());
jr2.addActionListener(new StateListener());
//On ajoute les boutons au groupe
bg.add(jr1);
bg.add(jr2);
top.add(jr1);
top.add(jr2);
container.add(top, BorderLayout.NORTH);
this.setContentPane(container);
this.setVisible(true);
}
Test des
boutons radio
Les champs de texte : l'objet JTextField
Première utilisation
Je pense que vous savez ce que vous avez à faire. Si ce n'est pas déjà fait, créez un nouveau projet
contenant les classes habituelles. Comme l'indique le titre de cette partie, nous allons utiliser
l'objet JTextField. Vous vous en doutez, cet objet propose lui aussi des méthodes de
redimensionnement, de changement de couleur… De ce fait, je commence avec un exemple complet. Lisez
et testez ce code :
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
JPanel top = new JPanel();
Font police = new Font("Arial", Font.BOLD, 14);
jtf.setFont(police);
jtf.setPreferredSize(new Dimension(150, 30));
jtf.setForeground(Color.BLUE);
top.add(label);
top.add(jtf);
container.add(top, BorderLayout.NORTH);
this.setContentPane(container);
this.setVisible(true);
}
}
Cela donne la figure suivante.
Nous pouvons initialiser le contenu avec la méthode setText(String str) ou le récupérer grâce à la
méthode getText().
Il existe un objet très ressemblant à celui-ci, en un peu plus étoffé. En fait, cet objet permet de créer
unJTextField formaté pour recevoir un certain type de données saisies (date, pourcentage etc.). Voyons
cela tout de suite.
Un objet plus restrictif : le JFormattedTextField
Grâce à ce type d'objet, nous pourrons éviter beaucoup de contrôles et de casts sur le contenu de nos zones
de texte. Si vous avez essayé de récupérer le contenu du JTextField utilisé ci-dessus (lors du clic sur un
bouton, par exemple), vous avez dû vous rendre compte que le texte qu'il contenait importait peu, mais un
jour, vous aurez sans doute besoin d'une zone de texte qui n'accepte qu'un certain type de données. Avec
l'objet JFormattedTextField, nous nous en approchons (mais vous verrez que vous pourrez faire
encore mieux). Cet objet retourne une valeur uniquement si celle-ci correspond à ce que vous avez autorisé.
Je m'explique : si vous voulez que votre zone de texte contienne par exemple des entiers et rien d'autre,
c'est possible ! En revanche, ce contrôle ne s'effectue que lorsque vous quittez le champ en question. Vous
pouvez ainsi saisir des lettres dans un objet n'acceptant que des entiers, mais la méthode getText() ne
renverra alors rien, car le contenu sera effacé, les données ne correspondent pas aux attentes de l'objet.
Voici un code et deux exemples, ainsi que leur rendu (figure suivante).
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
JPanel top = new JPanel();
Font police = new Font("Arial", Font.BOLD, 14);
jtf.setFont(police);
jtf.setPreferredSize(new Dimension(150, 30));
jtf.setForeground(Color.BLUE);
jtf2.setPreferredSize(new Dimension(150, 30));
b.addActionListener(new BoutonListener());
top.add(label);
top.add(jtf);
top.add(jtf2);
top.add(b);
this.setContentPane(top);
this.setVisible(true);
}
Vous voyez qu'en plus, notre objet met automatiquement la saisie en forme lorsqu'elle est valide : il espace
les nombres tous les trois chiffres afin d'en faciliter la lecture.
NumberFormat avec :
o getIntegerInstance()
o getPercentInstance()
o getNumberInstance()
DateFormat avec
o getTimeInstance()
o getDateInstance()
MessageFormat
Sans entrer dans les détails, vous pouvez aussi utiliser un objet MaskFormatter qui permet d'attribuer un
format de longueur fixe à votre zone de texte. C'est très pratique lorsque vous souhaitez introduire un
numéro de téléphone, un numéro de sécurité sociale etc.
Vous devez définir ce format avec un paramètre lors de l'instanciation du masque à l'aide
de métacaractères. Ceux-ci indiquent à votre objet MaskFormatter ce que le contenu de votre zone de
texte contiendra. Voici la liste de ces métacaractères :
# : indique un chiffre ;
L'instanciation d'un tel objet peut lever une ParseException. Vous devez donc l'entourer d'un
bloc try{…}catch(ParseException e){…}.
Voici à quoi ressemblerait un format téléphonique :
try{
MaskFormatter tel = new MaskFormatter("## ## ## ## ##");
//Ou encore
MaskFormatter tel2 = new MaskFormatter("##-##-##-##-##");
//Vous pouvez ensuite le passer à votre zone de texte
JFormattedTextField jtf = new JFormattedTextField(tel2);
}catch(ParseException e){e.printStackTrace();}
Vous voyez qu'il n'y a là rien de compliqué. Je vous invite à essayer cela dans le code précédent, vous
constaterez qu'avec le métacaractère utilisé dans notre objet MaskFormatter, nous sommes obligés de
saisir des chiffres. La figure suivante montre le résultat après avoir cliqué sur le bouton.
Essai avec un MaskFormatter
Je ne sais pas pour le numéro de téléphone américain, mais le numéro français est loin d'être un numéro de
téléphone valide. Nous voici confrontés à un problème qui nous hantera tant que nous programmerons :
l'intégrité de nos données !
Comme le montre l'exemple précédent, nous pouvons suggérer à l'utilisateur ce qu'il doit renseigner comme
données dans les champs, mais nous ne devons pas lui faire aveuglément confiance ! C'est simple : on part
du principe de ne jamais faire confiance à l'utilisateur.
Nous sommes donc obligés d'effectuer une multitude de contrôles supplémentaires. Pour ce faire, nous
pouvons :
dans le cas où nous n'utilisons pas de MaskFormatter, vérifier en plus que les saisies sont
numériques ;
etc.
En gros, nous devons vérifier l'intégrité de nos données (dans le cas qui nous intéresse, l'intégrité de nos
chaînes de caractères) pendant ou après la saisie. Je ne vous cache pas que cela prendra une grande part de
votre temps lorsque vous coderez vos propres logiciels, mais c'est le métier qui veut ça.
Avant de terminer ce chapitre (assez conséquent, je l'avoue), je vous propose de voir comment nous
pouvons récupérer les événements du clavier. Nous avons appris à interagir avec la souris, mais pas avec le
clavier.
Contrôle du clavier : l'interface KeyListener
l'interface ItemListener qui écoute les événements sur une liste déroulante.
Voici à présent l'interface KeyListener. Comme l'indique le titre, elle nous permet d'intercepter les
événements clavier lorsque l'on :
Vous savez ce qu'il vous reste à faire : créer une implémentation de cette interface dans votre projet. Créez
une classe interne qui l'implémente et utilisez l'astuce d'Eclipse pour générer les méthodes nécessaires.
Comme vous vous en doutez, l'objet KeyEvent nous permettra d'obtenir des informations sur les touches
qui ont été utilisées. Parmi celles-ci, nous utiliserons :
Voici le code source que nous allons utiliser (il est presque identique aux précédents, rassurez-vous) :
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 150);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
top.add(label);
top.add(jtf);
top.add(b);
this.setContentPane(top);
this.setVisible(true);
}
Vous pouvez maintenant vous rendre compte de l'ordre dans lequel les événements du clavier sont gérés :
en premier, lorsqu'on presse la touche, en deuxième, lorsqu'elle est tapée, et enfin, lorsqu'elle est relâchée.
Dans le cas qui nous intéresse, nous souhaitons que lorsque l'utilisateur saisit un caractère interdit, celui-ci
soit automatiquement retiré de la zone de saisie. Pour cela, nous procéderons à un traitement spécifique
dans la méthode keyReleased(KeyEvent event).
Si vous avez effectué beaucoup de tests de touches, vous avez dû remarquer que les codes des touches
correspondant aux chiffres du pavé numérique sont compris entre 96 et 105.
À partir de là, c'est simple : il nous suffit de supprimer le caractère tapé de la zone de saisie si son code
n'est pas compris dans cet intervalle. Toutefois, un problème se pose avec cette méthode : ceux qui
possèdent un ordinateur portable sans pavé numérique ne pourront rien saisir alors qu'il est possible
d'obtenir des chiffres en appuyant sur MAJ + & , é , ' , ( ou - .
Ce souci nous amène à opter pour une autre solution : nous créerons une méthode dont le type de retour
sera un booléen nous indiquant si la saisie est numérique ou non. Comment ? Tout simplement en exécutant
unInteger.parseInt(value), le tout enveloppé dans
un try{…}catch(NumberFormatException ex){}. Si nous essayons de convertir un caractère « a »
en entier, l'exception sera levée et nous retournerons alors false (true dans le cas contraire).
Vous pouvez ajouter des éléments dans une liste avec la méthode addItem(Object obj).
//…
JOptionPane jop = new JOptionPane();
int option = jop.showConfirmDialog(null, "Voulez-vous lancer l'animation ?", "Lancement de
l'animation", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
if(option == JOptionPane.OK_OPTION){
animated = true;
t = new Thread(new PlayAnimation());
t.start();
bouton.setEnabled(false);
bouton2.setEnabled(true);
}
//…
//…
JOptionPane jop = new JOptionPane();
int option = jop.showConfirmDialog(null, "Voulez-vous arrêter l'animation ?", "Arrêt de
l'animation", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
en revanche, plutôt que d'afficher directement la boîte, nous affectons le résultat que
renvoie la méthode showConfirmDialog() à une variable de type int ;
nous nous servons de cette variable afin de savoir quel bouton a été cliqué (oui ou non).
En fait, lorsque vous cliquez sur l'un des deux boutons présents dans cette boîte, vous pouvez
affecter une valeur de type int :
En effectuant un test sur la valeur de notre entier, nous pouvons en déduire le bouton sur lequel
on a cliqué et agir en conséquence ! La figure suivante représente deux copies d'écran du
résultat obtenu.
JOptionPane avec notre animation
Les boîtes de saisie
Je suis sûr que vous avez deviné à quoi peuvent servir ces boîtes. Oui, tout à fait, nous allons
pouvoir y saisir du texte ! Et mieux encore : nous pourrons même obtenir une boîte de dialogue
qui propose des choix dans une liste déroulante. Vous savez déjà que nous allons utiliser
l'objet JOptionPane, et les plus curieux d'entre vous ont sûrement dû jeter un œil aux autres
méthodes proposées par cet objet… Ici, nous allons utiliser la
méthode showInputDialog(Component parent, String message, String title, int
messageType), qui retourne une chaîne de caractères.
import javax.swing.JOptionPane;
import javax.swing.JOptionPane;
Liste
dans une boîte de dialogue
Cette méthode retourne un objet de type Object, comme si vous récupériez la valeur
directement dans la combo ! Du coup, n'oubliez pas de faire un cast.
Voici maintenant une variante de ce que vous venez de voir : nous allons utiliser ici la
méthodeshowOptionDialog(). Celle-ci fonctionne à peu près comme la méthode précédente,
sauf qu'elle prend un paramètre supplémentaire et que le type de retour n'est pas un objet mais
un entier.
Ce type de boîte propose un choix de boutons correspondant aux éléments passés en
paramètres (tableau deString) au lieu d'une combo ; elle prend aussi une valeur par défaut,
mais retourne l'indice de l'élément dans la liste au lieu de l'élément lui-même.
Je pense que vous vous y connaissez assez pour comprendre le code suivant :
import javax.swing.JOptionPane;
Boîte multi-boutons
Voilà, vous en avez terminé avec les boîtes de saisie. Cependant, vous avez dû vous demander
s'il n'était pas possible d'ajouter des composants à ces boîtes. C'est vrai : vous pourriez avoir
besoin de plus de renseignements, sait-on jamais… Je vous propose donc de voir comment
créer vos propres boîtes de dialogue !
Je me doute que vous êtes impatients de faire vos propres boîtes de dialogue. Comme il est vrai
que dans certains cas, vous en aurez besoin, allons-y gaiement ! Je vais vous révéler un secret
bien gardé : les boîtes de dialogue héritent de la classe JDialog. Vous avez donc deviné que
nous allons créer une classe dérivée de cette dernière.
Commençons par créer un nouveau projet. Créez une nouvelle classe dans Eclipse, appelons-
la ZDialog, faites-la hériter de la classe citée précédemment, et mettez-y le code suivant :
import javax.swing.JDialog;
import javax.swing.JFrame;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
public Fenetre(){
this.setTitle("Ma JFrame");
this.setSize(300, 100);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
this.getContentPane().setLayout(new FlowLayout());
this.getContentPane().add(bouton);
bouton.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent arg0) {
ZDialog zd = new ZDialog(null, "Coucou les ZérOs", true);
}
});
this.setVisible(true);
}
Je pense que vous avez deviné le rôle des paramètres du constructeur, mais je vais tout de
même les expliciter :
JFrame Parent correspond à l'objet parent ;
boolean modal correspond à la modalité ; true : boîte modale, false : boîte non
modale.
Rien de compliqué… Il est donc temps d'ajouter des composants à notre objet. Par contre, vous
conviendrez que si nous prenons la peine de construire un tel composant, nous attendons plus
qu'une simple réponse à une question ouverte (oui/non), une chaîne de caractères ou encore un
choix dans une liste… Nous en voulons bien plus ! Plusieurs saisies, avec plusieurs listes en
même temps !
Vous avez vu que nous devrons récupérer les informations choisies dans certains cas, mais pas
dans tous : nous allons donc devoir déterminer ces différents cas, ainsi que les choses à faire.
Partons du fait que notre boîte comprendra un bouton OK et un bouton Annuler : dans le cas où
l'utilisateur clique sur OK, on récupère les informations, si l'utilisateur clique sur Annuler, on ne
récupère rien. Et il faudra aussi tenir compte de la modalité de notre boîte : la
méthode setVisible(false); met fin au dialogue ! Ceci signifie également que le dialogue
s'entame au moment où l'instructionsetVisible(true); est exécutée. C'est pourquoi nous
allons sortir cette instruction du constructeur de l'objet et la mettre dans une méthode à part.
Maintenant, il faut que l'on puisse indiquer à notre boîte de renvoyer les informations ou non.
C'est pour cela que nous allons utiliser un booléen - appelons-le sendData - initialisé à false,
mais qui passera àtrue si on clique sur OK.
Comment récupérer les informations saisies dans notre boîte depuis notre fenêtre, vu que nous
voulons obtenir plusieurs informations ?
C'est vrai qu'on ne peut retourner qu'une valeur à la fois. Mais il peut y avoir plusieurs solutions à
ce problème :
o créer un objet dont le rôle est de collecter les informations dans votre boîte et de
retourner cet objet ;
o etc.
Nous allons opter pour un objet qui collectera les informations et que nous retournerons à la fin
de la méthode showZDialog(). Avant de nous lancer dans sa création, nous devons savoir ce
que nous allons mettre dans notre boîte… J'ai choisi de vous faire programmer une boîte
permettant de spécifier les caractéristiques d'un personnage de jeu vidéo :
Pour ce qui est du placement des composants, l'objet JDialog se comporte exactement comme
un objet JFrame (BorderLayout par défaut, ajout d'un composant au conteneur…).
Nous pouvons donc créer notre objet contenant les informations de notre boîte de dialogue, je l'ai
appeléZDialogInfo.
public ZDialogInfo(){}
public ZDialogInfo(String nom, String sexe, String age, String cheveux, String taille){
this.nom = nom;
this.sexe = sexe;
this.age = age;
this.cheveux = cheveux;
this.taille = taille;
}
L'avantage avec cette méthode, c'est que nous n'avons pas à nous soucier d'une éventuelle
annulation de la saisie : l'objet d'information renverra toujours quelque chose.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.ButtonGroup;
import javax.swing.JTextField;
//Le nom
JPanel panNom = new JPanel();
panNom.setBackground(Color.white);
panNom.setPreferredSize(new Dimension(220, 60));
nom = new JTextField();
nom.setPreferredSize(new Dimension(100, 25));
panNom.setBorder(BorderFactory.createTitledBorder("Nom du personnage"));
nomLabel = new JLabel("Saisir un nom :");
panNom.add(nomLabel);
panNom.add(nom);
//Le sexe
JPanel panSexe = new JPanel();
panSexe.setBackground(Color.white);
panSexe.setPreferredSize(new Dimension(220, 60));
panSexe.setBorder(BorderFactory.createTitledBorder("Sexe du personnage"));
sexe = new JComboBox();
sexe.addItem("Masculin");
sexe.addItem("Féminin");
sexe.addItem("Indéterminé");
sexeLabel = new JLabel("Sexe : ");
panSexe.add(sexeLabel);
panSexe.add(sexe);
//L'âge
JPanel panAge = new JPanel();
panAge.setBackground(Color.white);
panAge.setBorder(BorderFactory.createTitledBorder("Age du personnage"));
panAge.setPreferredSize(new Dimension(440, 60));
tranche1 = new JRadioButton("15 - 25 ans");
tranche1.setSelected(true);
tranche2 = new JRadioButton("26 - 35 ans");
tranche3 = new JRadioButton("36 - 50 ans");
tranche4 = new JRadioButton("+ de 50 ans");
ButtonGroup bg = new ButtonGroup();
bg.add(tranche1);
bg.add(tranche2);
bg.add(tranche3);
bg.add(tranche4);
panAge.add(tranche1);
panAge.add(tranche2);
panAge.add(tranche3);
panAge.add(tranche4);
//La taille
JPanel panTaille = new JPanel();
panTaille.setBackground(Color.white);
panTaille.setPreferredSize(new Dimension(220, 60));
panTaille.setBorder(BorderFactory.createTitledBorder("Taille du personnage"));
tailleLabel = new JLabel("Taille : ");
taille2Label = new JLabel(" cm");
taille = new JTextField("180");
taille.setPreferredSize(new Dimension(90, 25));
panTaille.add(tailleLabel);
panTaille.add(taille);
panTaille.add(taille2Label);
okBouton.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent arg0) {
zInfo = new ZDialogInfo(nom.getText(), (String)sexe.getSelectedItem(), getAge(),
(String)cheveux.getSelectedItem() ,getTaille());
setVisible(false);
}
control.add(okBouton);
control.add(cancelBouton);
this.getContentPane().add(panIcon, BorderLayout.WEST);
this.getContentPane().add(content, BorderLayout.CENTER);
this.getContentPane().add(control, BorderLayout.SOUTH);
}
}
J'ai ajouté une image, mais vous n'y êtes nullement obligés ! Voici le code source permettant de
tester cette boîte :
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
public class Fenetre extends JFrame {
private JButton bouton = new JButton("Appel à la ZDialog");
public Fenetre(){
this.setTitle("Ma JFrame");
this.setSize(300, 100);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
this.getContentPane().setLayout(new FlowLayout());
this.getContentPane().add(bouton);
bouton.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent arg0) {
ZDialog zd = new ZDialog(null, "Coucou les ZérOs", true);
ZDialogInfo zInfo = zd.showZDialog();
JOptionPane jop = new JOptionPane();
jop.showMessageDialog(null, zInfo.toString(), "Informations personnage",
JOptionPane.INFORMATION_MESSAGE);
}
});
this.setVisible(true);
}
Les menus
Vous vous rappelez que j'ai mentionné qu'une MenuBar fait partie de la composition de
l'objet JFrame. Le moment est venu pour vous d'utiliser un composant de ce genre. Néanmoins,
celui-ci appartient au package java.awt. Dans ce chapitre nous utiliserons son homologue,
l'objet JMenuBar, issu dans le package javax.swing. Pour travailler avec des menus, nous
aurons besoin :
Afin de permettre des interactions avec nos futurs menus, nous allons devoir implémenter
l'interfaceActionListener que vous connaissez déjà bien. Ces implémentations serviront à
écouter les objetsJMenuItem : ce sont ces objets qui déclencheront l'une ou l'autre opération.
Les JMenu, eux, se comportent automatiquement : si on clique sur un titre de menu, celui-ci se
déroule tout seul et, dans le cas où nous avons un tel objet présent dans un autre JMenu, une
autre liste se déroulera toute seule !
Je vous propose d'enlever tous les composants (boutons, combos, etc.) de notre animation et de
gérer tout cela par le biais d'un menu.
Avant de nous lancer dans cette tâche, voici une application de tout cela, histoire de vous
familiariser avec les concepts et leur syntaxe.
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JRadioButtonMenuItem;
public ZFenetre(){
this.setSize(400, 200);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
this.test1_2.add(jrmi1);
this.test1_2.add(jrmi2);
Vous voyez qu'il n'y a rien de difficile dans l'élaboration d'un menu. Je vous propose donc d'en
créer un pour notre animation. Allons-y petit à petit : nous ne gérerons les événements que par la
suite. Pour le moment, nous allons avoir besoin :
d'un menu Forme afin de sélectionner le type de forme utiliser (sous-menu + une radio
par forme) et de permettre d'activer le mode morphing (case à cocher) ;
d'un menu À propos avec un joli « ? » qui va ouvrir une boîte de dialogue.
N'effacez surtout pas les implémentations pour les événements : retirez seulement les
composants qui les utilisent. Ensuite, créez votre menu !
Voici un code qui ne devrait pas trop différer de ce que vous avez écrit :
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButtonMenuItem;
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
container.add(pan, BorderLayout.CENTER);
this.setContentPane(container);
this.initMenu();
this.setVisible(true);
}
//Menu forme
bg.add(carre);
bg.add(triangle);
bg.add(rond);
bg.add(etoile);
typeForme.add(rond);
typeForme.add(carre);
typeForme.add(triangle);
typeForme.add(etoile);
rond.setSelected(true);
forme.add(typeForme);
forme.add(morph);
//Menu À propos
aPropos.add(aProposItem);
if(option == JOptionPane.OK_OPTION){
lancer.setEnabled(false);
arreter.setEnabled(true);
animated = true;
t = new Thread(new PlayAnimation());
t.start();
}
}
}
Il ne reste plus qu'à faire communiquer nos menus et notre animation ! Pour cela, rien de plus
simple, il suffit d'indiquer à nos MenuItem qu'on les écoute. En fait, cela revient à faire comme si
nous cliquions sur des boutons (à l'exception des cases à cocher et des radios où, là, nous
pouvons utiliser une implémentation d'ActionListener ou de ItemListener), nous utiliserons
donc la première méthode.
Afin que l'application fonctionne bien, j'ai apporté deux modifications mineures dans la
classe Panneau. J'ai ajouté une instruction dans une condition :
Voici le code de notre animation avec un beau menu pour tout contrôler :
//Les imports
public class Fenetre extends JFrame{
public Fenetre(){
//Le constructeur est inchangé
}
animation.addSeparator();
quitter.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent event){
System.exit(0);
}
});
animation.add(quitter);
//Menu Forme
bg.add(carre);
bg.add(triangle);
bg.add(rond);
bg.add(etoile);
typeForme.add(rond);
typeForme.add(carre);
typeForme.add(triangle);
typeForme.add(etoile);
rond.setSelected(true);
forme.add(typeForme);
//Menu À propos
/**
* Écouteur du menu Quitter
* @author CHerby
*/
class StopAnimationListener implements ActionListener{
public void actionPerformed(ActionEvent e) {
//Idem
}
}
/**
* Écoute les menus Forme
* @author CHerby
*/
class FormeListener implements ActionListener{
public void actionPerformed(ActionEvent e) {
pan.setForme(((JRadioButtonMenuItem)e.getSource()).getText());
}
}
/**
* Écoute le menu Morphing
* @author CHerby
*/
class MorphListener implements ActionListener{
public void actionPerformed(ActionEvent e) {
//Si la case est cochée, activation du mode morphing
if(morph.isSelected()) pan.setMorph(true);
//Sinon rien !
else pan.setMorph(false);
}
}
}
Comme je l'ai indiqué dans le dialogue du menu À propos, je crois qu'il est temps d'ajouter des
raccourcis clavier à notre application ! Vous êtes prêts ?
À nouveau, il est très simple d'insérer des raccourcis clavier. Pour ajouter un « accélérateur »
(raccourcis clavier des éléments de menu) sur un JMenu, nous appellerons la
méthode setAccelerator(); et pour ajouter un mnémonique (raccourcis permettant de simuler
le clic sur un point de menu) sur un JMenuItem, nous nous servirons de la
méthode setMnemonic();.
forme.setMnemonic('F');
menuBar.add(forme);
aPropos.setMnemonic('P');
menuBar.add(aPropos);
//Ajout de la barre de menus sur la fenêtre
this.setJMenuBar(menuBar);
}
Nous avons à présent les lettres correspondant au mnémonique soulignées dans nos menus. Et
il y a mieux : si vous tapez ALT + <la lettre> , le menu correspondant se déroule ! La figure
suivante correspond à ce que j'obtiens.
Sachez que vous pouvez aussi mettre des mnémoniques sur les objets JMenuItem. Je dois
également vous dire qu'il existe une autre façon d'ajouter un mnémonique sur un JMenu (mais
c'est uniquement valable avec un JMenu) : en passant le mnémonique en deuxième paramètre
du constructeur de l'objet, comme ceci :
Ajoutez cette ligne de code au début de la méthode initMenu() (vous aurez besoin des
packagesjavax.swing.KeyStroke et java.awt.event.ActionEvent) :
Dans le code qui suit, je crée un accélérateur CTRL + L pour le menu Lancer et un
accélérateur CTRL + SHIFT + A pour le menu Arrêter :
lancer.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, KeyEvent.CTRL_MASK));
animation.add(lancer);
//Ajout du listener pour arrêter l'animation
arreter.addActionListener(new StopAnimationListener());
arreter.setEnabled(false);
arreter.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.CTRL_DOWN_MASK +
KeyEvent.SHIFT_DOWN_MASK));
animation.add(arreter);
La figure suivante présente le résultat obtenu.
Combinaison de touches pour un
accélérateur
J'imagine que vous êtes perturbés par KeyEvent.VK_L et les appels du même genre. En fait, la
classeKeyEvent répertorie tous les codes de toutes les touches du clavier. Une grande majorité
d'entre eux sont sous la forme VK_<le caractère ou le nom de la touche>. Lisez-le ainsi
: Value of Key <nom de la touche>.
À part certaines touches de contrôle comme CTRL , ALT , SHIFT , etc. vous pouvez facilement
retrouver le code d'une touche grâce à cet objet !
Ensuite, vous avez dû remarquer qu'en tapant KeyEvent.CTRL_DOWN_MASK, Eclipse vous a
proposé quasiment la même chose (figure suivante).
Versions différentes
Vous pouvez aisément voir qu'Eclipse vous dit que la version CTRL_DOWN_MASK est la plus
récente et qu'il est vivement conseillé de l'utiliser ! Vous voilà donc avec un menu comprenant
des mnémoniques et des accélérateurs. Il est maintenant temps de voir comment créer un menu
contextuel !
Vous avez déjà fait le plus dur, je suis sûr que vous n'allez pas tarder à vous en rendre compte.
Nous allons simplement utiliser un autre objet, un JPopupMenu, dans lequel nous mettrons
nos JMenuItem ou/etJMenu. Bon il faudra tout de même indiquer à notre menu contextuel
comment et où s'afficher, mais vous verrez que c'est très simple. Maintenant que vous
commencez à bien connaître les bases de la programmation événementielle, nous passons à la
vitesse supérieure !
Dans le cas d'opérations identiques à celles accessibles par le menu, nous devrons créer
des objets qui s'étendent à ces deux menus.
Le menu contextuel ne doit s'afficher que dans la zone où l'animation s'exécute, pas dans
le menu !
Nous allons mettre dans notre menu contextuel les actions « Lancer l'animation », « Arrêter
l'animation » ainsi que deux nouveautés :
Avant d'implémenter les deux nouvelles fonctionnalités, nous allons travailler sur les deux
premières.
Comme je vous l'ai dit plus haut, nous allons utiliser le même objet qui écoute pour les deux
menus. Il nous faudra créer une véritable instance de ces objets et signaler à l'application que
ces objets écoutent non seulement le menu du haut, mais aussi le menu contextuel.
Nous avons parfaitement le droit de le faire : plusieurs objets peuvent écouter un même
composant et plusieurs composants peuvent avoir le même objet qui les écoute ! Vous êtes
presque prêts à créer votre menu contextuel, il ne vous manque que ces informations :
comment lui spécifier qu'il doit le faire uniquement suite à un clic droit.
Le déclenchement de l'affichage du pop-up doit se faire lors d'un clic de souris. Vous connaissez
une interface qui gère ce type d'événement : l'interface MouseListener. Nous allons donc
indiquer à notre panneau qu'un objet du type de cette interface va l'écouter !
Tout comme dans le chapitre sur les zones de saisie, il existe une classe qui contient toutes les
méthodes de ladite interface : la classe MouseAdapter. Vous pouvez implémenter celle-ci afin
de ne redéfinir que la méthode dont vous avez besoin ! C'est cette solution que nous allons
utiliser.
Si vous préférez, vous pouvez utiliser l'événement mouseClicked, mais je pensais plutôt
àmouseReleased(), pour une raison simple à laquelle vous n'avez peut-être pas pensé : si ces
deux événements sont quasiment identiques, dans un certain cas, seul
l'événement mouseClicked() sera appelé. Il s'agit du cas où vous cliquez sur une zone,
déplacez votre souris en dehors de la zone tout en maintenant le clic et relâchez le bouton de la
souris. C'est pour cette raison que je préfère utiliser la méthodemouseReleased(). Ensuite, pour
préciser où afficher le menu contextuel, nous allons utiliser la méthodeshow(Component
invoker, int x, int y); de la classe JPopupMenu :
Component invoker : désigne l'objet invoquant le menu contextuel, dans notre cas,
l'instance dePanneau.
Souvenez-vous que vous pouvez déterminer les coordonnées de la souris grâce à l'objet passé
en paramètre de la méthode mouseReleased(MouseEvent event).
Je suis sûr que vous savez comment vous y prendre pour indiquer au menu contextuel de
s'afficher et qu'il ne vous manque plus qu'à détecter le clic droit. C'est là que
l'objet MouseEvent va vous sauver la mise ! En effet, il possède une
méthode isPopupTrigger() qui renvoie vrai s'il s'agit d'un clic droit. Vous avez toutes les
cartes en main pour élaborer votre menu contextuel (rappelez-vous que nous ne gérons pas
encore les nouvelles fonctionnalités).
Vous avez fini ? Nous pouvons comparer nos codes ? Je vous invite à consulter le code ci-
dessous (il ne vous montre que les nouveautés).
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
couleur.add(rouge);
couleur.add(bleu);
couleur.add(vert);
jpm.add(launch);
jpm.add(stop);
jpm.add(couleur);
jpm.add(background);
//La méthode qui va afficher le menu
jpm.show(pan, event.getX(), event.getY());
}
}
});
container.add(pan, BorderLayout.CENTER);
this.setContentPane(container);
this.initMenu();
this.setVisible(true);
if(option == JOptionPane.OK_OPTION){
lancer.setEnabled(false);
arreter.setEnabled(true);
animated = true;
t = new Thread(new PlayAnimation());
t.start();
}
}
}
/**
* Écouteur du menu Quitter
* @author CHerby
*/
class StopAnimationListener implements ActionListener{
Menu contextuel
Je sens que vous êtes prêts pour mettre les nouvelles options en place, même si je me doute
que certains d'entre vous ont déjà fait ce qu'il fallait. Allez, il n'est pas très difficile de coder ce
genre de choses (surtout que vous êtes habitués, maintenant). Dans notre classe Panneau, nous
utilisons des couleurs prédéfinies. Ainsi, il nous suffit de mettre ces couleurs dans des variables
et de permettre leur modification.
Rien de difficile ici, voici donc les codes sources de nos deux classes.
Panneau.java
import java.awt.Color;
//Les autres imports
Fenetre.java
public Fenetre(){
this.setTitle("Animation");
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
container.setBackground(Color.white);
container.setLayout(new BorderLayout());
rougeBack.addActionListener(bgColor);
bleuBack.addActionListener(bgColor);
vertBack.addActionListener(bgColor);
blancBack.addActionListener(bgColor);
//On crée et on passe l'écouteur pour afficher le menu contextuel
//Création d'une implémentation de MouseAdapter
//avec redéfinition de la méthode adéquate
pan.addMouseListener(new MouseAdapter(){
public void mouseReleased(MouseEvent event){
//Seulement s'il s'agit d'un clic droit
if(event.isPopupTrigger()){
background.add(blancBack);
background.add(rougeBack);
background.add(bleuBack);
background.add(vertBack);
couleur.add(blanc);
couleur.add(rouge);
couleur.add(bleu);
couleur.add(vert);
jpm.add(launch);
jpm.add(stop);
jpm.add(couleur);
jpm.add(background);
container.add(pan, BorderLayout.CENTER);
this.setContentPane(container);
this.initMenu();
this.setVisible(true);
}
if(e.getSource() == vertBack)
pan.setCouleurFond(Color.green);
else if (e.getSource() == bleuBack)
pan.setCouleurFond(Color.blue);
else if(e.getSource() == rougeBack)
pan.setCouleurFond(Color.red);
else
pan.setCouleurFond(Color.white);
}
}
Vous conviendrez que les menus et les menus contextuels peuvent s'avérer vraiment utiles et
ergonomiques ! En plus, ils sont relativement simples à implémenter (et à utiliser). Cependant,
vous avez sans doute remarqué qu'il y a beaucoup de clics superflus, que ce soit pour utiliser un
menu ou menu contextuel : il faut au moins un clic pour afficher leur contenu (sauf dans le cas de
l'accélérateur).
Pour contrer ce genre de chose, il existe un concept très puissant : la barre d'outils !
La figure suivante représente un exemple de barre d'outils (il s'agit de la partie encadrée).
Exemple de barre d'outils
Pour faire simple, la barre d'outils sert à effectuer des actions disponibles dans le menu, mais
sans devoir fouiller dans celui-ci ou mémoriser le raccourci clavier (accélérateur) qui y est lié. Elle
permet donc des actions rapides.
Elle est généralement composée d'une multitude de boutons, une image apposée sur chacun
d'entre eux symbolisant l'opération qu'il peut effectuer.
Pour créer et utiliser une barre d'outils, nous allons utiliser l'objet JToolBar. Je vous rassure tout
de suite, cet objet fonctionne comme un menu classique, à une différence près : celui-ci prend
des boutons (JButton) en arguments, et il n'y a pas d'endroit spécifique où incorporer votre
barre d'outils (il faudra l'expliciter lors de sa création).
Tout d'abord, il nous faut des images à mettre sur nos boutons… J'en ai fait de toutes simples
(figure suivante), mais libre à vous d'en choisir d'autres.
Au niveau des actions à gérer, pour le lancement de l'animation et l'arrêt, il faudra penser à éditer
le comportement des boutons de la barre d'outils comme on l'a fait pour les deux actions du
menu contextuel. Concernant les boutons pour les formes, c'est un peu plus délicat. Les autres
composants qui éditaient la forme de notre animation étaient des boutons radios. Or, ici, nous
avons des boutons standard. Outre le fait qu'il va falloir une instance précise de la
classe FormeListener, nous aurons à modifier un peu son comportement…
Il nous faut savoir si l'action vient d'un bouton radio du menu ou d'un bouton de la barre d'outils :
c'est l'objetActionEvent qui nous permettra d'accéder à cette information. Nous n'allons pas
tester tous les boutons radio un par un, pour ces composants, le système utilisé jusque-là était
très bien. Non, nous allons simplement vérifier si celui qui a déclenché l'action est
un JRadioButtonMenuItem, et si c'est le cas, nous testerons les boutons.
Il va falloir qu'on pense à mettre à jour le bouton radio sélectionné dans le menu. Et là, pour votre
plus grand bonheur, je connais une astuce qui marche pas mal du tout : lors du clic sur un bouton
de la barre d'outils, il suffit de déclencher l'événement sur le bouton radio correspondant ! Dans la
classe AbstractButton, dont héritent tous les boutons, il y a la méthode doClick(). Cette
méthode déclenche un événement identique à un vrai clic de souris sur le composant ! Ainsi,
plutôt que de gérer la même façon de faire à deux endroits, nous allons rediriger l'action
effectuée sur un composant vers un autre.
Vous avez toutes les cartes en main pour réaliser votre barre d'outils. N'oubliez pas que vous
devez spécifier sa position sur le conteneur principal ! Bon. Faites des tests, comparez, codez,
effacez… au final, vous devriez avoir quelque chose comme ceci :
import javax.swing.JToolBar;
//Nos imports habituels
public Fenetre(){
//La seule nouveauté est la méthode ci-dessous
this.initToolBar();
this.setVisible(true);
}
this.toolBar.add(play);
this.toolBar.add(cancel);
this.toolBar.addSeparator();
this.square.addActionListener(fListener);
this.square.setBackground(fondBouton);
this.toolBar.add(square);
this.tri.setBackground(fondBouton);
this.tri.addActionListener(fListener);
this.toolBar.add(tri);
this.star.setBackground(fondBouton);
this.star.addActionListener(fListener);
this.toolBar.add(star);
this.add(toolBar, BorderLayout.NORTH);
}
if(option == JOptionPane.OK_OPTION){
lancer.setEnabled(false);
arreter.setEnabled(true);
play.setEnabled(false);
cancel.setEnabled(true);
animated = true;
t = new Thread(new PlayAnimation());
t.start();
}
}
}
/**
* Écouteur du menu Quitter
* @author CHerby
*/
class StopAnimationListener implements ActionListener{
play.setEnabled(true);
cancel.setEnabled(false);
}
}
}
Elle n'est pas jolie, votre IHM, maintenant ? Vous avez bien travaillé, surtout qu'à présent, je vous
explique peut-être les grandes lignes, mais je vous force à aussi réfléchir par vous-mêmes ! Eh
oui, vous avez appris à penser en orienté objet et connaissez les points principaux de la
programmation événementielle. Maintenant, il vous reste simplement à acquérir des détails
techniques spécifiques (par exemple, la manière d'utiliser certains objets).
Pour ceux qui l'auraient remarqué, la barre d'outils est déplaçable ! Si vous cliquez sur la zone
mise en évidence à la figure suivante, vous pourrez la repositionner.
Zone de déplacement
Il suffit de maintenir le clic et de faire glisser votre souris vers la droite, la gauche ou encore le
bas. Vous verrez alors un carré se déplacer et, lorsque vous relâcherez le bouton, votre barre
aura changé de place, comme le montre la figure suivante.
Elles sont fortes ces barres d'outils, tout de même ! En plus de tout ça, vous pouvez utiliser autre
chose qu'un composant sur une barre d'outils...
Nous avons vu précédemment comment centraliser des actions sur différents composants. Il
existe une classe abstraite qui permet de gérer ce genre de choses, car elle peut s'adapter à
beaucoup de composants (en général à ceux qui ne font qu'une action, comme un bouton, une
case à cocher, mais pas une liste).
Le rôle de cette classe est d'attribuer automatiquement une action à un ou plusieurs composants.
Le principal avantage de ce procédé est que plusieurs composants travaillent avec une
implémentation de la classeAbstractAction, mais son gros inconvénient réside dans le fait que
vous devrez programmer une implémentation par action :
etc.
Cela peut être très lourd à faire, mais je laisse votre bon sens déterminer s'il est pertinent
d'utiliser cette méthode ou non !
//…
//…
Vous pouvez voir que cela peut être très pratique. Désormais, si vous ajoutez une action sur une
barre d'outils, celle-ci crée automatiquement un bouton correspondant ! Utiliser les actions
abstraites plutôt que des implémentations de telle ou telle interface est un choix qui vous revient.
Nous pouvons d'ailleurs très bien appliquer ce principe au code de notre animation, mais vous
constaterez qu'il s'alourdira, nous éviterons donc de le faire… Mais comme je vous le disais, c'est
une question de choix et de conception.
La méthode citée ci-dessus retourne un entier correspondant au bouton sur lequel vous
avez cliqué.
La méthode showInputDialog() affiche une boîte attendant une saisie clavier ou une
sélection dans une liste.
Cette méthode retourne soit un String dans le cas d'une saisie, soit un Object dans le
cas d'une liste.
Celle-ci retourne l'indice de l'élément sur lequel vous avez cliqué ou un indice négatif
dans tous les autres cas.
Les boîtes de dialogue sont dites « modales » : aucune interaction hors de la boîte n'est
possible tant que celle-ci n'est pas fermée !
Pour faire une boîte de dialogue personnalisée, vous devez créer une classe héritée
de JDialog.
L'objet servant à insérer une barre de menus sur vos IHM swing est un JMenuBar.
Dans cet objet, vous pouvez mettre des objets JMenu afin de créer un menu déroulant.
Afin d'interagir avec vos points de menu, vous pouvez utiliser une implémentation de
l'interfaceActionListener.
Pour faciliter l'accès aux menus de la barre de menus, vous pouvez ajouter
des mnémoniques à ceux-ci.
L'ajout d'accélérateurs permet de déclencher des actions, le plus souvent par des
combinaisons de touches.
Afin de récupérer les codes des touches du clavier, vous devrez utiliser un
objet KeyStroke ainsi qu'un objet KeyEvent.
Un menu contextuel fonctionne comme un menu normal, à la différence qu'il s'agit d'un
objetJPopupMenu. Vous devez toutefois spécifier le composant sur lequel doit s'afficher le
menu contextuel.
quitter.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, KeyEvent.CTRL_DOWN_MASK));
fichier.add(nouveau);
fichier.addSeparator();
fichier.add(quitter);
fichier.setMnemonic('F');
carre.addActionListener(fListener);
rond.addActionListener(fListener);
forme.add(rond);
forme.add(carre);
rouge.addActionListener(cListener);
vert.addActionListener(cListener);
bleu.addActionListener(cListener);
couleur.add(rouge);
couleur.add(vert);
couleur.add(bleu);
edition.setMnemonic('E');
edition.add(forme);
edition.addSeparator();
edition.add(couleur);
menuBar.add(fichier);
menuBar.add(edition);
this.setJMenuBar(menuBar);
}
toolBar.add(square);
toolBar.add(circle);
toolBar.addSeparator();
toolBar.add(red);
toolBar.add(blue);
toolBar.add(green);
this.getContentPane().add(toolBar, BorderLayout.NORTH);
}
Voici ce que vous pouvez faire afin de rendre cette application plus attractive :
etc.
Dans ce chapitre, nous allons voir de nouveaux conteneurs. Ils seront soit complémentaires
au JPanel que vous connaissez bien maintenant, soit à tout autre type de conteneur ayant ses propres
spécificités.
Il y a plusieurs objets qui peuvent vous aider à mieux gérer le contenu de vos IHM ; ceux qui seront
abordés ici vont, je pense, vous rendre un sacré service… Toutefois, laissez-moi vous mettre en garde : ici,
nous n'aborderons pas les objets dans le détail, nous ne ferons même qu'en survoler certains. Le fait est
que vous êtes dorénavant à même d'approfondir tel ou tel sujet en Java.
Autres conteneurs
L'objet JSplitPane
Avant de vous faire un laïus (un petit, je vous rassure), regardez la figure suivante. Elle représente des
fenêtres avec un JSplitPane.
import java.awt.BorderLayout;
import java.awt.Color;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
public Fenetre(){
this.setLocationRelativeTo(null);
this.setTitle("Gérer vos conteneur");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(200, 200);
Split vertical
Autre point, les deux autres paramètres ne sont pas nécessairement des JPanel. Ici, j'ai utilisé des JPanel,
mais vous pouvez en fait utiliser n'importe quelle classe dérivant de JComponent (conteneur, bouton,
case à cocher…) : elle n'est pas belle, la vie ?
Je ne vous avais donc pas menti : cet objet est vraiment très simple d'utilisation, mais je ne vais pas vous
laisser tout de suite. Vous ne l'avez peut-être pas remarqué mais ces objets ne peuvent pas faire disparaître
entièrement les côtés. Dans notre cas, la fenêtre est petite, mais vous aurez peut-être l'occasion d'avoir une
grande IHM et d'agrandir ou de rétrécir fréquemment vos contenus.
L'objet JSplitPane dispose d'une méthode qui permet de rendre la barre de séparation « intelligente »…
enfin presque. Ladite méthode ajoute deux petits boutons sur votre barre et, lorsque vous cliquerez
dessus, fera rétrécir le côté vers lequel pointe la flèche dans le bouton. L'illustration de mes propos se
trouve à la figure suivante.
Flèches de positionnement
Pour avoir ces deux boutons en plus sur votre barre, il vous suffit d'invoquer la
méthodesplit.setOneTouchExpandable(true); (mon objet s'appelle toujours split) et le tour
est joué ! Amusez-vous à cliquer sur ces boutons et vous verrez à quoi ils servent.
Avant de vous laisser fouiner un peu à propos de cet objet, vous devez savoir que vous pouvez définir une
taille de séparateur grâce à la méthode split.setDividerSize(int size) ; la figure suivante vous
montre ce que j'ai obtenu avec une taille de 35 pixels.
Agrandissement du splitter
Vous pouvez également définir où doit s'afficher la barre de séparation. Ceci se fait grâce à la
méthodesetDividerLocation(int location); ou setDividerLocation(double
location);.
Avant de vous montrer un exemple de code utilisant cette méthode, vous avez dû comprendre que, vu que
cet objet peut accepter en paramètres des sous-classes de JComponent, il pouvait aussi accepter
desJSplitPane ! La figure suivante vous montre ce que j'ai pu obtenir.
Multiple splitter
import java.awt.BorderLayout;
import java.awt.Color;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
public Fenetre(){
this.setLocationRelativeTo(null);
this.setTitle("Gérer vos conteneur");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(200, 200);
L'objet JScrollPane
Afin que vous puissiez mieux juger l'utilité de l'objet que nous allons utiliser ici, nous allons voir un nouvel
objet de texte : le JTextArea. Cet objet est très simple : c'est une forme de JTextField, mais en plus
grand ! Nous pouvons directement écrire dans ce composant, celui-ci ne retourne pas directement à la ligne
si vous atteignez le bord droit de la fenêtre.
Pour vérifier si les lettres tapées au clavier sont bien dans notre objet, vous pouvez récupérer le texte saisi
grâce à la méthode getText(). Voici un code d'exemple :
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTextArea;
public Fenetre(){
this.setLocationRelativeTo(null);
this.setTitle("Gérer vos conteneur");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(200, 200);
Exemple de JScrollPane
Vous voyez le petit ascenseur à droite et en bas de la fenêtre ? Avec ça, finis les problèmes de taille de vos
conteneurs ! Voici le code que j'ai utilisé pour obtenir ce résultat :
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
public Fenetre(){
this.setLocationRelativeTo(null);
this.setTitle("Gérer vos conteneur");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(200, 200);
Le paramètre de ces méthodes est un entier défini dans la classe JScrollPane, il peut prendre les valeurs
suivantes :
Les mêmes entiers existent pour le scroll horizontal, mais vous devrez alors
remplacer VERTICAL parHORIZONTAL ! Vous devez tout de même savoir que cet objet en utilise un autre
: un JScrollBar. Les deux barres de défilement sont deux instances de cet objet…
Nous avons vu comment séparer un conteneur, comment agrandir un conteneur, nous allons maintenant
voir comment ajouter dynamiquement des conteneurs !
L'objet JTabbedPane
Dans ce chapitre, vous allez apprendre à créer plusieurs « pages » dans votre IHM… Jusqu'à maintenant,
vous ne pouviez pas avoir plusieurs contenus dans votre fenêtre, à moins de leur faire partager l'espace
disponible.
Il existe une solution toute simple qui consiste à créer des onglets et, croyez-moi, c'est aussi très simple à
faire. L'objet à utiliser est un JTabbedPane. Afin d'avoir un exemple plus ludique, j'ai constitué une
classe héritée de JPanel afin de créer des onglets ayant une couleur de fond différente… Cette classe ne
devrait plus vous poser de problèmes :
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import javax.swing.JPanel;
public Panneau(){}
public Panneau(Color color){
this.color = color;
this.message = "Contenu du panneau N°" + (++COUNT);
}
public void paintComponent(Graphics g){
g.setColor(this.color);
g.fillRect(0, 0, this.getWidth(), this.getHeight());
g.setColor(Color.white);
g.setFont(new Font("Arial", Font.BOLD, 15));
g.drawString(this.message, 10, 20);
}
}
J'ai utilisé cet objet afin de créer un tableau de Panneau. Chaque instance est ensuite ajoutée à mon
objet gérant les onglets via sa méthode add(String title, JComponent comp).
import java.awt.Color;
import javax.swing.JFrame;
import javax.swing.JTabbedPane;
public Fenetre(){
this.setLocationRelativeTo(null);
this.setTitle("Gérer vos conteneurs");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(400, 200);
}
//On passe ensuite les onglets au content pane
this.getContentPane().add(onglet);
this.setVisible(true);
}
Plusieurs onglets
Vous constatez que l'utilisation de cet objet est très simple, là aussi… Je vais tout de même vous présenter
quelques méthodes bien utiles. Par exemple, vous pouvez ajouter une image en guise d'icône à côté du titre
de l'onglet. Ce qui pourrait nous donner la figure suivante.
public Fenetre(){
this.setLocationRelativeTo(null);
this.setTitle("Gérer vos conteneurs");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(400, 200);
this.getContentPane().add(pan, BorderLayout.SOUTH);
this.setVisible(true);
}
Ces deux objets sont très souvent associés et permettent de réaliser des applications multifenêtres, comme à
la figure suivante.
Exemple d'une application
multifenêtre
//CTRL + SHIFT + O pour générer les imports nécessaires
public Bureau(){
this.setSize(400, 300);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton ajouter = new JButton("Ajouter une fenêtre interne");
ajouter.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent event){
++nbreFenetre;
xy += 2;
desktop.add(new MiniFenetre(nbreFenetre), nbreFenetre);
}
});
this.getContentPane().add(desktop, BorderLayout.CENTER);
this.getContentPane().add(ajouter, BorderLayout.SOUTH);
}
Pour faire simple, c'est une JFrame, mais sans les contours permettant de réduire, fermer ou agrandir la
fenêtre ! Il est souvent utilisé pour faire des splash screens (ce qui s'affiche au lancement d'Eclipse, par
exemple…).
public Window(){
setSize(220, 165);
setLocationRelativeTo(null);
JPanel pan = new JPanel();
JLabel img = new JLabel(new ImageIcon("planète.jpeg"));
img.setVerticalAlignment(JLabel.CENTER);
img.setHorizontalAlignment(JLabel.CENTER);
pan.setBorder(BorderFactory.createLineBorder(Color.blue));
pan.add(img);
getContentPane().add(pan);
}
}
Le JEditorPane
Voici un objet sympathique mais quelque peu limité par la façon dont il gère son contenu HTML (voir
figure suivante) ! Il permet de réaliser des textes riches (avec une mise en page). Il y a aussi
le JTextPane qui vous permet très facilement de faire un mini-éditeur de texte (enfin, tout est relatif…).
public Fenetre(){
this.setSize(600, 400);
this.setTitle("Conteneur éditable");
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.getContentPane().add(onglet, BorderLayout.CENTER);
this.setVisible(true);
}
Le JSlider
Ce composant vous permet d'utiliser un système de mesure pour une application : redimensionner une
image, choisir le tempo d'un morceau de musique, l'opacité d'une couleur, etc. La figure suivante montre
à quoi il ressemble.
Un JSlider
Le code source :
slide.setMaximum(100);
slide.setMinimum(0);
slide.setValue(30);
slide.setPaintTicks(true);
slide.setPaintLabels(true);
slide.setMinorTickSpacing(10);
slide.setMajorTickSpacing(20);
slide.addChangeListener(new ChangeListener(){
public void stateChanged(ChangeEvent event){
label.setText("Valeur actuelle : " + ((JSlider)event.getSource()).getValue());
}
});
this.getContentPane().add(slide, BorderLayout.CENTER);
this.getContentPane().add(label, BorderLayout.SOUTH);
}
public static void main(String[] args){
Slide slide = new Slide();
slide.setVisible(true);
}
}
La JProgressBar
Elle vous permet de réaliser une barre de progression pour des traitements longs. La figure suivante en est
un exemple.
Une JProgressBar
public Progress(){
this.setSize(300, 80);
this.setTitle("*** JProgressBar ***");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
this.getContentPane().add(bar, BorderLayout.CENTER);
Nous n'avons pas beaucoup abordé ce point tout au long du livre, mais je vous laisse découvrir les
joyeusetés qu'offre Java en la matière… Voici comment ajouter des bordures à vos composants :
public BorderDemo(){
this.setTitle("Les bordures font la fête !");
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(550, 200);
this.getContentPane().add(pan);
}
L'objet JSplitPane vous permet de scinder un conteneur en deux parties via un splitter
déplaçable.
L'objet JScrollPane vous permet d'avoir un conteneur ou un objet contenant du texte de s'étirer
selon son contenu, en hauteur comme en largeur.
L'objet JTabbedPane vous permet d'obtenir une interface composée d'autant d'onglets que vous
le désirez et gérable de façon dynamique.
Les onglets peuvent être disposés aux quatre coins d'une fenêtre.
Les objets JDesktopPane combinés à des objets JInternalFrame vous permettent de créer
une application multifenêtre.
L'objet JWindow est une JFrame sans les contrôles d'usage. Elle sert à afficher une image de
lancement de programme, comme Eclipse par exemple.
L'objet JEditorPane vous permet de créer un éditeur HTML et d'afficher le rendu du code écrit.
Vous pouvez gérer des mesures ou des taux via l'objet JSlider. En déplaçant le curseur, vous
pourrez faire croître une valeur afin de l'utiliser.
Vous pouvez enjoliver la plupart de vos composants avec des bordures en utilisant
l'objetBorderFactory qui vous permettra de créer différents types de traits.
LES ARBRES ET LEUR STRUCTURE
Autant les objets vus dans le chapitre précédent étaient simples, autant celui que nous allons voir est assez
compliqué. Cela ne l'empêche pas d'être très pratique et très utilisé.
Vous devez tous déjà avoir vu un arbre. Non pas celui du monde végétal, mais celui qui permet d'explorer
des dossiers. Nous allons voir comment utiliser et exploiter un tel objet et interagir avec lui : ne vous
inquiétez pas, tout partira de zéro…
Tout d'abord, pour ceux qui ne verraient pas de quoi je parle, la figure suivante vous montre ce qu'on
appelle un arbre (JTree).
Exemple d'arbre
La chose bien pratique avec cet objet c'est que, même s'il ne ressemble pas à un chêne ou à un autre arbre, il
est composé de la même façon ! En fait, lorsque vous regardez bien un arbre, celui-ci est constitué de
plusieurs sous-ensembles :
des racines ;
un tronc ;
des branches ;
des feuilles.
L'objet JTree se base sur la même architecture. Vous aurez donc :
une racine : le répertoire le plus haut dans la hiérarchie ; ici, seul « Racine » est considéré
comme une racine ;
une ou plusieurs feuilles : éléments se trouvant en bas de la hiérarchie, ici « Sous-fichier enfant
n° 1-2-3-4 » ou encore « Nœud n° 1-3-5-7 » sont des feuilles.
this.setVisible(true);
}
//Que nous plaçons sur le ContentPane de notre JFrame à l'aide d'un scroll
this.getContentPane().add(new JScrollPane(arbre));
}
//Que nous plaçons sur le ContentPane de notre JFrame à l'aide d'un scroll
this.getContentPane().add(new JScrollPane(arbre));
}
Cela devrait vous donner la figure suivante.
this.setVisible(true);
}
this.racine.add(lecteur);
}
//Nous créons, avec notre hiérarchie, un arbre
arbre = new JTree(this.racine);
//Que nous plaçons sur le ContentPane de notre JFrame à l'aide d'un scroll
this.getContentPane().add(new JScrollPane(arbre));
}
if(file.isFile())
return new DefaultMutableTreeNode(file.getName());
else{
File[] list = file.listFiles();
if(list == null)
return new DefaultMutableTreeNode(file.getName());
Bon : vous arrivez à créer et afficher un arbre. Maintenant, voyons comment interagir avec !
Vous connaissez la musique maintenant, nous allons encore implémenter une interface ! Celle-ci se
nommeTreeSelectionListener. Elle ne contient qu'une méthode à redéfinir
:valueChanged(TreeSelectionEvent event).
this.setVisible(true);
}
this.racine.add(lecteur);
}
//Nous créons, avec notre hiérarchie, un arbre
arbre = new JTree(this.racine);
arbre.setRootVisible(false);
arbre.addTreeSelectionListener(new TreeSelectionListener(){
Nous avons réussi à afficher le nom du dernier nœud cliqué, mais nous n'allons pas nous arrêter là… Il peut
être intéressant de connaître le chemin d'accès du nœud dans l'arbre ! Surtout dans notre cas, puisque nous
listons le contenu de notre disque.
Nous pouvons donc obtenir des informations supplémentaires sur une feuille ou une branche en recourant à
un objet File, par exemple. L'objet TreeEvent passé en paramètre de la méthode de l'interface vous
apporte de précieux renseignements, dont la méthode getPath() qui vous retourne un objet TreePath.
Ce dernier contient les objets correspondant aux nœuds du chemin d'accès à un point de l'arbre. Ne vous
inquiétez pas, vous n'avez pas à changer beaucoup de choses pour obtenir ce résultat.
En fait, je n'ai modifié que la classe anonyme qui gère l'événement déclenché sur l'arbre. Voici la nouvelle
version de cette classe anonyme :
arbre.addTreeSelectionListener(new TreeSelectionListener(){
Essayez de le faire vous-mêmes dans un premier temps, sachant que j'ai obtenu quelque chose comme la
figure suivante.
J'espère que vous n'avez pas eu trop de mal à faire ce petit exercice… Vous devriez maintenant commencer
à savoir utiliser ce type d'objet, mais avant de passer à autre chose, je vous propose d'apprendre à
personnaliser un peu l'affichage de notre arbre.
Icônes personnalisées
Et voici le code qui m'a permis d'arriver à ce résultat :
public Fenetre(){
this.setSize(600, 350);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("Les arbres");
//On invoque la méthode de construction de l'arbre
initRenderer();
listRoot();
this.setVisible(true);
}
this.racine.add(lecteur);
}
//Nous créons, avec notre hiérarchie, un arbre
arbre = new JTree(this.racine);
arbre.setRootVisible(false);
//On définit le rendu pour cet arbre
arbre.setCellRenderer(this.tCellRenderer[0]);
if(file.isFile())
return new DefaultMutableTreeNode(file.getName());
else{
File[] list = file.listFiles();
if(list == null)
return new DefaultMutableTreeNode(file.getName());
Il existe une autre façon de changer l'affichage (le design) de votre application. Chaque système
d'exploitation possède son propre « design », mais vous avez pu constater que vos applications Java ne
ressemblent pas du tout à ce que votre OS (Operating System, ou système d'exploitation) vous propose
d'habitude ! Les couleurs, mais aussi la façon dont sont dessinés vos composants… Mais il y a un moyen de
pallier ce problème : utiliser le « look and feel » de votre OS.
J'ai rajouté ces lignes de code dans le constructeur de mon objet, avant l'instruction setVisible(true) :
try {
//On force à utiliser le « look and feel » du système
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
//Ici on force tous les composants de notre fenêtre (this) à se redessiner avec le « look
and feel » du système
SwingUtilities.updateComponentTreeUI(this);
}
catch (InstantiationException e) {}
catch (ClassNotFoundException e) {}
catch (UnsupportedLookAndFeelException e) {}
catch (IllegalAccessException e) {}
Cela me donne la figure suivante.
Design de l'OS forcé
Bien sûr, vous pouvez utiliser d'autres « look and feel » que ceux de votre système et de Java. Voici un
code qui permet de lister ces types d'affichage et d'instancier un objet Fenetre en lui spécifiant quel
modèle utiliser :
listRoot();
//On force l'utilisation
try {
UIManager.setLookAndFeel(lookAndFeel);
SwingUtilities.updateComponentTreeUI(this);
}
catch (InstantiationException e) {}
catch (ClassNotFoundException e) {}
catch (UnsupportedLookAndFeelException e) {}
catch (IllegalAccessException e) {}
this.setVisible(true);
}
//…
C'est maintenant que les choses se compliquent ! Il va falloir faire la lumière sur certaines choses… Vous
commencez à connaître les arbres : cependant, je vous ai caché quelques éléments afin de ne pas surcharger
le début de ce chapitre.
Votre JTree est en fait composé de plusieurs objets. Voici une liste des objets que vous serez susceptibles
d'utiliser avec ce composant (il y a cinq interfaces et une classe concrète…) :
TreePath : objet qui vous permet de connaître le chemin d'un nœud dans l'arbre. La voilà,
notre classe concrète ;
/**
* Méthode appelée lorsqu'un noeud est inséré
*/
public void treeNodesInserted(TreeModelEvent event) {
System.out.println("Un noeud a été inséré !");
}
/**
* Méthode appelée lorsqu'un noeud est supprimé
*/
public void treeNodesRemoved(TreeModelEvent event) {
System.out.println("Un noeud a été retiré !");
}
/**
* Méthode appelée lorsque la structure d'un noeud a été modifiée
*/
public void treeStructureChanged(TreeModelEvent event) {
System.out.println("La structure d'un noeud a changé !");
}
});
if(file.isFile())
return new DefaultMutableTreeNode(file.getName());
else{
File[] list = file.listFiles();
if(list == null)
return new DefaultMutableTreeNode(file.getName());
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
catch (InstantiationException e) {}
catch (ClassNotFoundException e) {}
catch (UnsupportedLookAndFeelException e) {}
catch (IllegalAccessException e) {}
Fenetre fen = new Fenetre();
}
}
Afin de pouvoir changer le nom d'un nœud, vous devez double-cliquer dessus avec un intervalle d'environ
une demi-seconde entre chaque clic… Si vous double-cliquez trop vite, vous déplierez le nœud !
Ce code a donné chez moi la figure suivante.
Changement de la valeur
d'un nœud
Le dossier « toto » s'appelait « CNAM/ » : vous pouvez voir que lorsque nous changeons le nom d'un
nœud, la méthode treeNodesChanged(TreeModelEvent evt) est invoquée !
Vous voyez que, mis à part le fait que plusieurs objets sont mis en jeu, ce n'est pas si compliqué que ça…
Maintenant, je vous propose d’examiner la manière d'ajouter des nœuds à notre arbre. Pour ce faire, nous
allons utiliser un bouton qui va nous demander de spécifier le nom du nouveau nœud, via
unJOptionPane.
public Fenetre(){
this.setSize(200, 300);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("JTree");
//On invoque la méthode de construction de l'arbre
listRoot();
bouton.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent event) {
if(arbre.getLastSelectedPathComponent() != null){
JOptionPane jop = new JOptionPane();
String nodeName = jop.showInputDialog("Saisir un nom de noeud");
int count = 0;
for(File file : File.listRoots())
{
DefaultMutableTreeNode lecteur = new DefaultMutableTreeNode(file.getAbsolutePath());
try {
for(File nom : file.listFiles()){
DefaultMutableTreeNode node = new DefaultMutableTreeNode(nom.getName()+"\\");
lecteur.add(this.listFile(nom, node));
}
} catch (NullPointerException e) {}
this.racine.add(lecteur);
}
//Nous créons, avec notre hiérarchie, un arbre
arbre = new JTree();
this.model = new DefaultTreeModel(this.racine);
arbre.setModel(model);
arbre.setRootVisible(false);
arbre.setEditable(true);
arbre.getModel().addTreeModelListener(new TreeModelListener() {
public void treeNodesChanged(TreeModelEvent evt) {
System.out.println("Changement dans l'arbre");
Object[] listNoeuds = evt.getChildren();
int[] listIndices = evt.getChildIndices();
for (int i = 0; i < listNoeuds.length; i++) {
System.out.println("Index " + listIndices[i] + ", noeud déclencheur : " +
listNoeuds[i]);
}
}
public void treeNodesInserted(TreeModelEvent event) {
System.out.println("Un noeud a été inséré !");
}
public void treeNodesRemoved(TreeModelEvent event) {
System.out.println("Un noeud a été retiré !");
}
public void treeStructureChanged(TreeModelEvent event) {
System.out.println("La structure d'un noeud a changé !");
}
});
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
catch (InstantiationException e) {}
catch (ClassNotFoundException e) {}
catch (UnsupportedLookAndFeelException e) {}
catch (IllegalAccessException e) {}
Fenetre fen = new Fenetre();
}
}
Vous remarquerez que nous avons ajouté des variables d'instances afin d'y avoir accès dans toute notre
classe !
La figure suivante nous montre différentes étapes de création de nœuds.
Là non plus, rien d'extraordinairement compliqué, mis à part cette portion de code :
parentNode = (DefaultMutableTreeNode)arbre.getLastSelectedPathComponent();
DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(nodeName);
DefaultMutableTreeNode parentNode.add(childNode);
model.insertNodeInto(childNode, parentNode, parentNode.getChildCount()-1);
model.nodeChanged(parentNode);
Tout d'abord, nous récupérons le dernier nœud sélectionné avec la ligne 1. Ensuite, nous créons un nouveau
nœud avec la ligne 2 et l'ajoutons dans le nœud parent avec la ligne 3. Cependant, nous devons spécifier à
notre modèle qu'il contient un nouveau nœud et donc qu'il a changé, au moyen des instructions des lignes 4
et 5.
Voilà : je pense que vous en savez assez pour utiliser les arbres dans vos futures applications !
Afin d'intercepter les événements sur tel ou tel composant, vous devez implémenter
l'interfaceTreeSelectionListener.
Cette interface n'a qu'une méthode à redéfinir : public void
valueChanged(TreeSelectionEvent event).
L'affichage des différents éléments constituant un arbre peut être modifié à l'aide
d'unDefaultTreeCellRenderer. Définissez et affectez cet objet à votre arbre pour en
personnaliser l'affichage.
Vous pouvez aussi changer le « look and feel » et utiliser celui de votre OS.
Nous continuons notre progression avec un autre composant assez complexe : le tableau. Celui-ci
fonctionne un peu comme le JTree vu précédemment.
Les choses se compliquent dès que l'on doit manipuler les données à l'intérieur du tableau, car Java
impose de séparer strictement l'affichage et les données dans le code.
Premiers pas
Les tableaux sont des composants qui permettent d'afficher des données de façon structurée.
Pour ceux qui ne savent pas ce que c'est, en voici un à la figure suivante.
Exemple de tableau
public Fenetre(){
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("JTable");
this.setSize(300, 120);
Les titres des colonnes de votre tableau peuvent être de type String ou de type Object, tandis que les
données sont obligatoirement de type Object.
Vous verrez un peu plus loin qu'il est possible de mettre plusieurs types d'éléments dans un tableau. Mais
nous n'en sommes pas là : il nous faut d'abord comprendre comment fonctionne cet objet.
Les plus observateurs auront remarqué que j'ai mis le tableau dans un scroll… En fait, si vous avez essayé
d'ajouter le tableau dans le contentPane sans scroll, vous avez dû constater que les titres des colonnes
n'apparaissent pas.
En effet, le scroll indique automatiquement au tableau l'endroit où il doit afficher ses titres ! Sans lui, vous
seriez obligés de préciser où afficher l'en-tête du tableau, comme ceci :
Gestion de l'affichage
Les cellules
Vos tableaux sont composés de cellules. Vous pouvez les voir facilement, elles sont encadrées de
bordures noires et contiennent les données que vous avez mises dans le tableau d'Object et
de String. Celles-ci peuvent être retrouvées par leurs coordonnées (x, y) où x correspond au numéro de
la ligne et y au numéro de la colonne ! Une cellule est donc l'intersection d'une ligne et d'une colonne.
Afin de modifier une cellule, il faut récupérer la ligne et la colonne auxquelles elle appartient. Ne vous
inquiétez pas, nous allons prendre tout cela point par point. Tout d'abord, commençons par changer la
taille d'une colonne et d'une ligne. Le résultat final ressemble à ce qu'on voit sur la figure suivante.
Changement de taille
Vous allez voir que le code utilisé est simple comme tout, encore fallait-il que vous sachiez quelles
méthodes et quels objets utiliser… Voici le code permettant d'obtenir ce résultat :
public Fenetre(){
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("JTable");
this.setSize(300, 240);
Object[][] data = {
{"Cysboy", "28 ans", "1.80 m"},
{"BZHHydde", "28 ans", "1.80 m"},
{"IamBow", "24 ans", "1.90 m"},
{"FunMan", "32 ans", "1.85 m"}
};
change.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent arg0) {
changeSize(200, 80);
change.setEnabled(false);
retablir.setEnabled(true);
}
});
retablir.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent arg0) {
changeSize(75, 16);
change.setEnabled(true);
retablir.setEnabled(false);
}
});
retablir.setEnabled(false);
pan.add(change);
pan.add(retablir);
/**
* Change la taille d'une ligne et d'une colonne
* J'ai mis deux boucles afin que vous puissiez voir
* comment parcourir les colonnes et les lignes
*/
public void changeSize(int width, int height){
//Nous créons un objet TableColumn afin de travailler sur notre colonne
TableColumn col;
for(int i = 0; i < tableau.getColumnCount(); i++){
if(i == 1){
//On récupère le modèle de la colonne
col = tableau.getColumnModel().getColumn(i);
//On lui affecte la nouvelle valeur
col.setPreferredWidth(width);
}
}
for(int i = 0; i < tableau.getRowCount(); i++){
//On affecte la taille de la ligne à l'indice spécifié !
if(i == 1)
tableau.setRowHeight(i, height);
}
}
Vous constatez que la ligne et la colonne concernées changent bien de taille lors du clic sur les boutons.
Vous venez donc de voir comment changer la taille des cellules de façon dynamique. Je dis ça parce que,
au cas où vous ne l'auriez pas remarqué, vous pouvez changer la taille des colonnes manuellement. Il vous
suffit de cliquer sur un séparateur de colonne, de maintenir le clic et de déplacer le séparateur, comme
indiqué à la figure suivante.
Séparateurs
Par contre, cette instruction a dû vous sembler étrange
: tableau.getColumnModel().getColumn(i);. En fait, vous devez savoir que c'est un objet qui
fait le lien entre votre tableau et vos données. Celui-ci est ce qu'on appelle un modèle de tableau (ça vous
rappelle les modèles d'arbres, non ?). L'objet en question s'appelle JTableModel et vous allez voir qu'il
permet de faire des choses très intéressantes ! C'est lui qui stocke vos données… Toutes vos données !
public Fenetre(){
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("JTable");
this.setSize(600, 140);
Object[][] data = {
{"Cysboy", new JButton("6boy"), new Double(1.80), new Boolean(true)},
{"BZHHydde", new JButton("BZH"), new Double(1.78), new Boolean(false)},
{"IamBow", new JButton("BoW"), new Double(1.90), new Boolean(false)},
{"FunMan", new JButton("Year"), new Double(1.85), new Boolean(true)}
};
Pour être le plus flexible possible, on doit créer son propre modèle qui va stocker les données du tableau. Il
vous suffit de créer une classe héritant de AbstractTableModel qui — vous l'avez sûrement deviné —
est une classe abstraite…
public Fenetre(){
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("JTable");
this.setSize(600, 140);
Object[][] data = {
{"Cysboy", new JButton("6boy"), new Double(1.80), new Boolean(true)},
{"BZHHydde", new JButton("BZH"), new Double(1.78), new Boolean(false)},
{"IamBow", new JButton("BoW"), new Double(1.90), new Boolean(false)},
{"FunMan", new JButton("Year"), new Double(1.85), new Boolean(true)}
};
//Constructeur
public ZModel(Object[][] data, String[] title){
this.data = data;
this.title = title;
}
/**
* Retourne le titre de la colonne à l'indice spécifié
*/
public String getColumnName(int col) {
return this.title[col];
}
Exécutez à nouveau votre code, après avoir rajouté cette méthode dans votre objet ZModel : vous devriez
avoir le même rendu que la figure suivante.
Regardez la figure suivante pour comprendre l'intérêt de gérer sa propre classe de modèle.
Affichage de checkbox
Vous avez vu ? Les booléens se sont transformés en cases à cocher ! Les booléens valant « vrai » sont
devenus des cases cochées et les booléens valant « faux » sont maintenant des cases non cochées ! Pour
obtenir ça, j'ai redéfini une méthode dans mon modèle et le reste est automatique. Cette méthode permet de
retourner la classe du type de valeur d'un modèle et de transformer vos booléens en cases à cocher… Au
moment où notre objet crée le rendu des cellules, il invoque cette méthode et s'en sert pour créer certaines
choses, comme ce que vous venez de voir.
Pour obtenir ce rendu, il vous suffit de redéfinir la méthode getColumnClass(int col). Cette
méthode retourne un objet Class. Je vous laisse réfléchir un peu…
Pour savoir comment faire, c'est juste en dessous :
Ajoutez donc ce morceau de code dans votre modèle pour renvoyer true :
Les modèles font un pont entre ce qu'affiche JTable et les actions de l'utilisateur. Pour modifier
l'affichage des cellules, nous devrons utiliser DefaultCellRenderer.
Contrôlez l'affichage
Le but du jeu est de définir une nouvelle façon de dessiner les composants dans les cellules. En définitive,
nous n'allons pas vraiment faire cela, mais dire à notre tableau que la valeur contenue dans une cellule
donnée est un composant (bouton ou autre). Il suffit de créer une classe héritant
deDefaultTableCellRenderer et de redéfinir la méthode public Component
getTableCellRendererComponent(JTable table, Object value, boolean
isSelected, boolean hasFocus, int row, int column).
Il y en a, des paramètres ! Mais, dans le cas qui nous intéresse, nous n'avons besoin que d'un seul d'entre
eux :value. Remarquez que cette méthode retourne un objet Component. Nous allons seulement spécifier
le type d'objet dont il s'agit suivant le cas.
Voilà notre bouton en chair et en os ! Je me doute bien que les plus taquins d'entre vous ont dû essayer de
mettre plus d'un type de composant dans le tableau… Et ils se retrouvent le bec dans l'eau car il ne prend en
compte que les boutons pour le moment.
En fait, une fois que vous avez défini une classe héritée afin de gérer le rendu de vos cellules, vous avez fait
le plus gros du travail… Tenez, si, par exemple, nous voulons mettre ce genre de données dans notre
tableau :
Object[][] data = {
{"Cysboy", new JButton("6boy"), new JComboBox(new String[]{"toto", "titi", "tata"}), new
Boolean(true)},
{"BZHHydde", new JButton("BZH"), new JComboBox(new String[]{"toto", "titi", "tata"}), new
Boolean(false)},
{"IamBow", new JButton("BoW"), new JComboBox(new String[]{"toto", "titi", "tata"}), new
Boolean(false)},
{"FunMan", new JButton("Year"), new JComboBox(new String[]{"toto", "titi", "tata"}), new
Boolean(true)}
};
… et si nous conservons l'objet de rendu de cellules tel qu'il est actuellement, nous obtiendrons la figure
suivante.
Les boutons s'affichent toujours, mais pas les combos ! Je sais que certains d'entre vous ont presque trouvé
la solution. Vous n'auriez pas ajouté ce qui suit dans votre objet de rendu de cellule ?
public Fenetre(){
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("JTable");
this.setSize(600, 180);
Object[][] data = {
{"Cysboy", "6boy", "Combo", new Boolean(true)},
{"BZHHydde", "BZH", "Combo", new Boolean(false)},
{"IamBow", "BoW", "Combo", new Boolean(false)},
{"FunMan", "Year", "Combo", new Boolean(true)}
};
}
public static void main(String[] args){
Fenetre fen = new Fenetre();
fen.setVisible(true);
}
}
De là, nous allons créer une classe qui affichera un bouton dans les cellules de la seconde colonne et une
combo dans les cellules de la troisième colonne… Le principe est simple : créer une classe qui héritera de
la classe JButton et qui implémentera l'interface TableCellRenderer. Nous allons ensuite dire à notre
tableau qu'il doit utiliser utiliser ce type de rendu pour la seconde colonne.
Votre bouton est de nouveau éditable, mais ce problème sera réglé par la suite… Pour le rendu de la cellule
numéro 3, je vous laisse un peu chercher, ce n'est pas très difficile maintenant que vous avez appris cette
méthode.
Dernière ligne droite avant la fin du chapitre… Nous commencerons par le plus difficile et terminerons par
le plus simple ! Je vous le donne en mille : le composant le plus difficile à utiliser dans un tableau, entre un
bouton et une combo c'est… le bouton !
Eh oui, vous verrez que la combo est gérée presque automatiquement, alors qu'il vous faudra dire aux
boutons ce qu'ils devront faire… Pour arriver à cela, nous allons créer une classe qui permettra à notre
tableau d'effectuer des actions spécifiques grâce aux boutons.
Vous pouvez voir que lorsque vous cliquez sur un bouton, la valeur dans la cellule située juste à gauche est
modifiée. L'utilisation est donc très simple. Imaginez par conséquent que la gestion des combos est encore
plus aisée !
Un peu plus tôt, je vous ai fait développer une classe permettant d'afficher la combo normalement.
Cependant, il y a beaucoup plus facile… Vous avez pu voir que la classe DefaultCellEditor pouvait
prendre un objet en paramètre : dans l'exemple du JButton, il utilisait un JCheckBox. Vous devez savoir
que cet objet accepte d'autres types de paramètres :
un JComboBox ;
un JTextField.
Nous pouvons donc utiliser l'objet DefaultCellEditor directement en lui passant une combo en
paramètre… Nous allons aussi enlever l'objet permettant d'afficher correctement la combo afin que vous
puissiez juger de l'efficacité de cette méthode.
public Fenetre(){
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("JTable");
this.setSize(600, 180);
//Données de notre tableau
Object[][] data = {
{"Cysboy", "6boy", comboData[0], new Boolean(true)},
{"BZHHydde", "BZH", comboData[0], new Boolean(false)},
{"IamBow", "BoW", comboData[0], new Boolean(false)},
{"FunMan", "Year", comboData[0], new Boolean(true)}
};
//Titre du tableau
String title[] = {"Pseudo", "Age", "Taille", "OK ?"};
//Combo à utiliser
JComboBox combo = new JComboBox(comboData);
DefaultCellEditor et combo
Votre cellule se « transforme » en combo lorsque vous cliquez dessus ! En fait, lorsque le tableau sent une
action sur cette cellule, il utilise l'éditeur que vous avez spécifié pour celle-ci.
Si vous préférez que la combo soit affichée directement même sans clic de souris, il vous suffit de laisser
l'objet gérant l'affichage et le tour est joué. De même, pour le bouton, si vous enlevez l'objet de rendu du
tableau, celui-ci s'affiche comme un bouton lors du clic sur la cellule !
Il ne nous reste plus qu'à voir comment rajouter des informations dans notre tableau, et le tour est joué.
Certains d'entre vous l'auront remarqué, les boutons ont un drôle de comportement.
Cela est dû au fait que vous avez affecté des comportements spéciaux à votre tableau… Il faut donc définir
un modèle à utiliser afin de bien définir tous les points comme l'affichage, la mise à jour, etc.
Nous allons donc utiliser un modèle de tableau personnalisé où les actions seront définies par nos soins.
Voici la classe Fenetre modifiée en conséquence :
public Fenetre(){
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("JTable");
this.setSize(600, 180);
//Données de notre tableau
Object[][] data = {
{"Cysboy", "6boy", comboData[0], new Boolean(true)},
{"BZHHydde", "BZH", comboData[0], new Boolean(false)},
{"IamBow", "BoW", comboData[0], new Boolean(false)},
{"FunMan", "Year", comboData[0], new Boolean(true)}
};
String title[] = {"Pseudo", "Age", "Taille", "OK ?"};
JComboBox combo = new JComboBox(comboData);
//Nous devons utiliser un modèle d'affichage spécifique pour pallier les bugs
d'affichage !
ZModel zModel = new ZModel(data, title);
//Constructeur
public ZModel(Object[][] data, String[] title){
this.data = data;
this.title = title;
}
Voici la classe ButtonEditor utilisant le modèle de tableau pour gérer la modification des valeurs :
Je vais profiter de ce point pour vous montrer une autre façon d'initialiser un tableau :
Dans un premier temps, ajoutons et retirons des lignes à notre tableau. Nous garderons le même code que
précédemment avec deux ou trois ajouts :
Le modèle par défaut défini lors de la création du tableau nous donne accès à deux méthodes fort utiles :
removeRow(int row) : efface une ligne du modèle et met automatiquement à jour le tableau.
Avant de pouvoir utiliser ce modèle, nous allons devoir le récupérer. En fait, c'est notre tableau qui va
nous le fournir en invoquant la méthode getModel() qui retourne un objet TableModel. Attention, un
cast sera nécessaire afin de pouvoir utiliser l'objet récupéré ! Par exemple
:((ZModel)table.getModel()).removeRow().
Essayez de développer ces nouvelles fonctionnalités. Pour télécharger le code complet du chapitre, c'est par
ici que ça se passe.
Celui-ci prend en paramètres un tableau d'objets à deux dimensions (un tableau de données)
correspondant aux données à afficher, et un tableau de chaînes de caractères qui, lui, affichera les
titres des colonnes.
Afin de gérer vous-mêmes le contenu du tableau, vous pouvez utiliser un modèle de données
(TableModel).
Pour ajouter ou retirer des lignes à un tableau, il faut passer par un modèle de données. Ainsi,
l'affichage est mis à jour automatiquement.
La gestion de l'affichage brut (hors édition) des cellules peut se gérer colonne par colonne à l'aide
d'une classe dérivant de TableCellRenderer.
La gestion de l'affichage brut lors de l'édition d'une cellule se gère colonne par colonne avec une
classe dérivant de DefaultCellEditor.
TP : LE PENDU
Ce TP est sûrement le plus difficile que vous aurez à réaliser ! Il fait appel à beaucoup d'éléments vus
précédemment. Nous allons devoir réaliser un jeu de pendu. Le principe est classique : vous devez trouver
un mot secret lettre par lettre en faisant un minimum d'erreurs.
Nous en profiterons pour utiliser des design patterns, ces fameuses bonnes pratiques de programmation.
Vous devez réaliser un jeu du pendu en Java gérant la sauvegarde des dix meilleurs scores. Toutefois, j'ai
des exigences précises :
l'application doit contenir les menus Nouveau, Scores, Règles et À propos ;
les points doivent être cumulés en tenant compte des mots trouvés et des erreurs commises ;
il faut vérifier si le joueur est dans le top dix, auquel cas on lui demande son pseudo, on
enregistre les données et on le redirige vers la page des scores ;
Vous aurez besoin d'un « fichier dictionnaire » contenant de nombreux mots pour votre jeu :
Prérequis
Vous devrez utiliser les flux afin de parcourir le fichier texte qui contient plus de 336 000 lignes : il faudra
donc choisir un nombre aléatoire entre 0 et 336 529, puis récupérer le mot désigné. Pour obtenir un nombre
aléatoire entre 0 et 336 529, j'ai codé ceci :
Il faudra également que vous pensiez à gérer les caractères accentués, lorsque vous cliquerez sur le bouton
« E » par exemple. Vous devrez donc aussi afficher les lettres « E » accentuées.
Je ne vais pas tout vous dévoiler, il serait dommage de gâcher le plaisir. En revanche, j'insiste sur le fait que
c'est un TP difficile, et qu'il vous faudra certainement plusieurs heures avant d'en venir à bout. Prenez donc
le temps de déterminer les problèmes, réfléchissez bien et codez proprement !
Je vous conseille vivement d'aller relire les chapitres traitant des design patterns, car j'en ai utilisé ici ; de
plus, j'ai rangé mes classes en packages.
Allez, en avant !
Correction
Une fois n'est pas coutume, je ne vais pas inscrire ici tous les codes source, mais plutôt vous fournir tout
mon projet Eclipse contenant un .jar exécutable ; et pour cause, il contient beaucoup de classes, comme
vous le pouvez le voir à la figure suivante.
Récupérer le projet
Classes du TP
Voici donc une astuce d'Eclipse permettant de rapatrier un projet.
Une fois Eclipse ouvert, effectuez un clic droit dans la zone où se trouvent vos projets, puis cliquez
surImport et choisissez Existing project dans General (voir figure suivante).
Importer
un projet existant dans Eclipse
Il ne vous reste plus qu'à spécifier l'endroit où vous avez décompressé l'archive .jar que je vous ai
fournie, et le tour est joué.
Une fois l'archive décompressée, vous devriez pouvoir lancer le fichier .jar par un double-clic. Si rien ne
se produit, mettez à jour vos variables d'environnement (voir le premier chapitre).
Prenez bien le temps de lire et comprendre le code. Si vous n'arrivez pas à tout faire maintenant, essayez de
commencer par réaliser les choses les plus simples, vous pourrez toujours améliorer votre travail plus tard
lorsque vous vous sentirez plus à l'aise !
Vous pourrez constater que j'ai rangé mon code d'une façon assez étrange, avec un
packagecom.sdz.model et un com.sdz.vue… Cette façon de procéder correspond à un autre pattern
de conception permettant de séparer le code en couches capables d'interagir entre elles : c'est le sujet du
chapitre suivant.
Ce chapitre va vous présenter un des design patterns les plus connus : MVC. Il va vous apprendre à
découper vos codes en trois parties : modèle, vue et contrôleur. C'est un pattern composé, ce qui signifie
qu'il est constitué d'au moins deux patterns (mais rien n'empêche qu'il y en ait plus).
Nous allons voir cela tout de suite, inutile de tergiverser plus longtemps !
Premiers pas
découverte du pattern.
Ici, nous procéderons autrement : puisque le pattern MVC est plus complexe à aborder, nous allons entrer
directement dans le vif du sujet. Le schéma présenté à la figure suivante en décrit le principe ; il ne devrait
pas être étranger à ceux d'entre vous qui auraient déjà fait quelques recherches concernant ce pattern.
La vue
Ce que l'on nomme « la vue » est en fait une IHM. Elle représente ce que l'utilisateur a sous les yeux. La
vue peut donc être :
une application graphique Swing, AWT, SWT pour Java (Form pour C#…) ;
une page web ;
etc.
Le modèle
Le modèle peut être divers et varié. C'est là que se trouvent les données. Il s'agit en général d'un ou
plusieurs objets Java. Ces objets s'apparentent généralement à ce qu'on appelle souvent « la couche métier »
de l'application et effectuent des traitements absolument transparents pour l'utilisateur. Par exemple, on
peut citer des objets dont le rôle est de gérer une ou plusieurs tables d'une base de données. En trois mots, il
s'agit du cœur du programme !
Dans le chapitre précédent, nous avons confectionné un jeu du pendu. Dans cette application, notre
fenêtreSwing correspond à la vue et l'objet Model correspond au modèle.
Le contrôleur
Cet objet - car il s'agit aussi d'un objet - permet de faire le lien entre la vue et le modèle lorsqu'une action
utilisateur est intervenue sur la vue. C'est cet objet qui aura pour rôle de contrôler les données.
Maintenant que toute la lumière est faite sur les trois composants de ce pattern, je vais expliquer plus
précisément la façon dont il travaille.
Afin de travailler sur un exemple concret, nous allons reprendre notre calculatrice issue d'un TP précédent.
Dans une application structurée en MVC, voici ce qu'il peut se passer :
l'utilisateur effectue une action sur votre calculatrice (un clic sur un bouton) ;
l'action est captée par le contrôleur, qui va vérifier la cohérence des données et éventuellement
les transformer afin que le modèle les comprenne. Le contrôleur peut aussi demander à la vue
de changer ;
le modèle reçoit les données et change d'état (une variable qui change, par exemple) ;
le modèle notifie la vue (ou les vues) qu'il faut se mettre à jour ;
l'affichage dans la vue (ou les vues) est modifié en conséquence en allant chercher l'état du
modèle.
Je vous disais plus haut que le pattern MVC était un pattern composé : à ce stade de votre apprentissage,
vous pouvez isoler deux patterns dans cette architecture. Le pattern observer se trouve au niveau du
modèle. Ainsi, lorsque celui-ci va changer d'état, tous les objets qui l'observeront seront mis au courant
automatiquement, et ce, avec un couplage faible !
Le deuxième est plus difficile à voir mais il s'agit du pattern strategy ! Ce pattern est situé au niveau du
contrôleur. On dit aussi que le contrôleur est la stratégie (en référence au pattern du même nom) de la vue.
En fait, le contrôleur va transférer les données de l'utilisateur au modèle et il a tout à fait le droit de
modifier le contenu.
Ceux qui se demandent pourquoi utiliser le pattern strategy pourront se souvenir de la raison d'être de ce
pattern : encapsuler les morceaux de code qui changent !
En utilisant ce pattern, vous prévenez les risques potentiels de changement dans votre logique de contrôle.
Il vous suffira d'utiliser une autre implémentation de votre contrôleur afin d'avoir des contrôles différents.
Ceci dit, vous devez tout de même savoir que le modèle et le contrôleur sont intimement liés : un objet
contrôleur pour notre calculatrice ne servira que pour notre calculatrice ! Nous pouvons donc autoriser un
couplage fort entre ces deux objets.
Le modèle
Le modèle est l'objet qui sera chargé de stocker les données nécessaires à un calcul (nombre et opérateur) et
d'avoir le résultat. Afin de prévoir un changement éventuel de modèle, nous créerons le notre à partir d'un
supertype de modèle : de cette manière, si un changement s'opère, nous pourrons utiliser les différentes
classes filles de façon polymorphe.
Avant de foncer tête baissée, réfléchissons à ce que notre modèle doit être capable d'effectuer. Pour réaliser
des calculs simples, il devra :
calculer le résultat ;
renvoyer le résultat ;
Très bien : voila donc la liste des méthodes que nous trouverons dans notre classe abstraite.
Comme vous le savez, nous allons utiliser le pattern observer afin de faire communiquer notre modèle avec
d'autres objets. Il nous faudra donc une implémentation de ce pattern ; la voici, dans un
packagecom.sdz.observer.
Observable.java
package com.sdz.observer;
Observer.java
package com.sdz.observer;
Voici donc le code de notre classe abstraite que nous placerons dans le package
com.sdz.model.
AbstractModel.java
package com.sdz.model;
import java.util.ArrayList;
import com.sdz.observer.Observable;
import com.sdz.observer.Observer;
//Effectue le calcul
public abstract void calcul();
Calculator.java
package com.sdz.model;
import com.sdz.observer.Observable;
public class Calculator extends AbstractModel{
//Définit l'opérateur
public void setOperateur(String ope){
//On lance le calcul
calcul();
//Définit le nombre
public void setNombre(String result){
//On concatène le nombre
this.operande += result;
//On met à jour
notifyObserver(this.operande);
}
//Force le calcul
public void getResultat() {
calcul();
}
//Réinitialise tout
public void reset(){
this.result = 0;
this.operande = "0";
this.operateur = "";
//Mise à jour !
notifyObserver(String.valueOf(this.result));
}
//Calcul
public void calcul(){
//S'il n'y a pas d'opérateur, le résultat est le nombre saisi
if(this.operateur.equals("")){
this.result = Double.parseDouble(this.operande);
}
else{
//Si l'opérande n'est pas vide, on calcule avec l'opérateur de calcul
if(!this.operande.equals("")){
if(this.operateur.equals("+"))
this.result += Double.parseDouble(this.operande);
if(this.operateur.equals("-"))
this.result -= Double.parseDouble(this.operande);
if(this.operateur.equals("*"))
this.result *= Double.parseDouble(this.operande);
if(this.operateur.equals("/")){
try{
this.result /= Double.parseDouble(this.operande);
}catch(ArithmeticException e){
this.result = 0;
}
}
}
}
this.operande = "";
//On lance aussi la mise à jour !
notifyObserver(String.valueOf(this.result));
}
}
Voilà, notre modèle est prêt à l'emploi ! Nous allons donc continuer à créer les composants de ce pattern.
Le contrôleur
Celui-ci sera chargé de faire le lien entre notre vue et notre modèle. Nous créerons aussi une classe abstraite
afin de définir un supertype de variable pour utiliser, le cas échéant, des contrôleurs de façon polymorphe.
Que doit faire notre contrôleur? C'est lui qui va intercepter les actions de l'utilisateur, qui va modeler les
données et les envoyer au modèle. Il devra donc :
avertir le modèle pour qu'il se réinitialise dans le cas d'un clic sur le bouton reset ;
Voilà donc notre liste de méthodes pour cet objet. Cependant, puisque notre contrôleur doit interagir avec
le modèle, il faudra qu'il possède une instance de notre modèle.
AbstractControler.java
package com.sdz.controler;
import java.util.ArrayList;
import com.sdz.model.AbstractModel;
public abstract class AbstractControler {
//Définit l'opérateur
public void setOperateur(String ope){
this.operateur = ope;
control();
}
//Définit le nombre
public void setNombre(String nombre){
this.nbre = nombre;
control();
}
//Efface
public void reset(){
this.calc.reset();
}
//Méthode de contrôle
abstract void control();
}
Nous avons défini les actions globales de notre objet de contrôle et vous constatez aussi qu'à chaque action
dans notre contrôleur, celui-ci invoque la méthode control(). Celle-ci va vérifier les données et informer
le modèle en conséquence.
Nous allons voir maintenant ce que doit effectuer notre instance concrète. Voici donc, sans plus tarder,
notre classe.
CalculetteControler.java
package com.sdz.controler;
import com.sdz.model.AbstractModel;
this.operateur = "";
this.nbre = "";
}
}
Vous pouvez voir que cette classe redéfinit la méthode control() et qu'elle permet d'indiquer les
informations à envoyer à notre modèle. Celui-ci mis à jour, les données à afficher dans la vue seront
envoyées via l'implémentation du pattern observer entre notre modèle et notre vue. D'ailleurs, il ne nous
manque plus qu'elle, alors allons-y !
La vue
Voici le plus facile à développer et ce que vous devriez maîtriser le mieux… La vue sera créée avec le
packagejavax.swing. Je vous donne donc le code source de notre classe que j'ai mis dans le
packagecom.sdz.vue.
Calculette.java
package com.sdz.vue;
//CTRL + SHIFT + O pour générer les imports
public class Calculette extends JFrame implements Observer{
String[] tab_string = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", ".", "=", "C",
"+", "-", "*", "/"};
JButton[] tab_button = new JButton[tab_string.length];
switch(i){
case 11 :
tab_button[i].addActionListener(opeListener);
chiffre.add(tab_button[i]);
break;
case 12 :
tab_button[i].setForeground(Color.red);
tab_button[i].addActionListener(new ResetListener());
tab_button[i].setPreferredSize(dim2);
operateur.add(tab_button[i]);
break;
case 13 :
case 14 :
case 15 :
case 16 :
tab_button[i].setForeground(Color.red);
tab_button[i].addActionListener(opeListener);
tab_button[i].setPreferredSize(dim2);
operateur.add(tab_button[i]);
break;
default :
chiffre.add(tab_button[i]);
tab_button[i].addActionListener(new ChiffreListener());
break;
}
}
panEcran.add(ecran);
panEcran.setBorder(BorderFactory.createLineBorder(Color.black));
container.add(panEcran, BorderLayout.NORTH);
container.add(chiffre, BorderLayout.CENTER);
container.add(operateur, BorderLayout.EAST);
}
controler.setNombre(((JButton)e.getSource()).getText());
}
}
Toutes nos classes sont à présent opérationnelles. Il ne nous manque plus qu'une classe de test afin
d'observer le résultat. Elle crée les trois composants qui vont dialoguer entre eux : le modèle (données), la
vue (fenêtre) et le contrôleur qui lie les deux.
La voici :
import com.sdz.controler.*;
import com.sdz.model.*;
import com.sdz.vue.Calculette;
Il informe le modèle.
J'émets toutefois quelques réserves concernant ce pattern. Bien qu'il soit très utile grâce à ses avantages à
long terme, celui-ci complique grandement votre code et peut le rendre très difficile à comprendre pour une
personne extérieure à l'équipe de développement. Même si le design pattern permet de résoudre beaucoup
de problèmes, attention à la « patternite aigüe » : son usage trop fréquent peut rendre le code
incompréhensible et son entretien impossible à réaliser.
Avec ce pattern, le code est découpé en trois parties logiques qui communiquent entre elles :
o Le modèle (données)
o La vue (fenêtre)
Utiliser ce pattern permet de découpler trois acteurs d'une application, ce qui permet plus de
souplesse et une maintenance plus aisée du code.
LE DRAG'N DROP
Cette notion est somme toute assez importante à l'heure actuelle : beaucoup de gens l'utilisent, ne serait-ce
que pour déplacer des fichiers dans leur système d'exploitation ou encore faire des copies sur une clé USB.
Pour rappel, le Drag'n Drop - traduit par « Glisser-Déposer » - revient à sélectionner un élément graphique
d'un clic gauche, à le déplacer grâce à la souris tout en maintenant le bouton enfoncé et à le déposer à
l'endroit voulu en relâchant le bouton. En Java, cette notion est arrivée avec JDK 1.2, dans le système
graphique awt. Nous verrons comment ceci était géré car, même si ce système est fondu et simplifié
avecswing, vous devrez utiliser l'ancienne gestion de ce comportement, version awt.
Je vous propose de commencer par un exemple simple, en utilisant swing, puis ensuite de découvrir un cas
plus complet en utilisant tous les rouages de ces événements, car il s'agit encore et toujours d'événements.
Présentation
La première chose à faire en swing pour activer le drag'n drop, c'est d'activer cette fonctionnalité dans les
composants concernés.
Voici un code de test :
public Test1(){
super("Test de Drag'n Drop");
setSize(300, 200);
pan2.add(text2, BorderLayout.SOUTH);
pan2.add(text, BorderLayout.NORTH);
pan.add(pan2, BorderLayout.SOUTH);
add(pan, BorderLayout.CENTER);
setVisible(true);
}
Récapitulons. Nous avons une fenêtre contenant trois composants : un JTextArea avec le drag'n drop
activé et deux JTextField dont seul celui du dessus a l'option activée.
La figure suivante vous montre ce que donne ce code.
Lancement du programme
La figure suivante donne le résultat après avoir sélectionné une portion de texte et l'avoir glissée dans
leJTextField n° 1.
Texte cliqué-glissé
Enfin, vous trouverez à la figure suivante le résultat d'un déplacement du contenu du JTextField n° 1
vers le JTextField n° 2.
Changement de JTextField
Étant donné que ce dernier JTextField est dépourvu de l'option désirée, vous ne pouvez plus déplacer le
texte.
J'ai essayé de faire la même chose avec un JLabel et ça n'a pas fonctionné !
C'est tout à fait normal. Par défaut, le drag'n drop n'est disponible que pour certains composants. D'abord, il
ne faut pas confondre l'action « drag » et l'option « drop ». Certains composants autorisent les deux alors
que d'autres n'autorisent que le drag. Voici un tableau récapitulatif des actions autorisées par composant :
JEditorPane X X
JColorChooser X X
JFileChooser X .
JTextPane X X
JTextField X X
JTextArea X X
JFormattedTextField X X
JPasswordTextField . X
JLabel . .
JTable X .
JTree X .
JList X .
Certains composants de ce tableau autorisent soit l'export de données, soit l'import de données, soit les
deux, soit aucun des deux. Certains composants n'ont aucun comportement lorsque nous y déposons des
données… Ceci est dû à leur complexité et à leurs modes de fonctionnement. Par exemple, donner un
comportement par défaut à un JTree n'est pas une mince affaire. Lors d'un drop, doit-il :
ajouter l'élément ?
De ce fait, le comportement est laissé aux bons soins du développeur, en l'occurrence, vous.
Par contre, il faut que vous gardiez en mémoire que lorsqu'on parle de « drag », il y a deux notions
implicites à prendre en compte : le « drag déplacement » et le « drag copie ».
la copie ;
le déplacement.
Par exemple, sous Windows, lorsque vous déplacez un fichier avec un drag'n drop dans un dossier sans
changer de disque dur, ce fichier est entièrement déplacé : cela revient à faire un couper/coller. En
revanche, si vous effectuez la même opération en maintenant la touche Ctrl , l'action du drag'n drop devient
l'équivalent d'un copier/coller.
L'action « drag déplacement » indique donc les composants autorisant, par défaut, l'action de type
couper/coller, l'action « drag copie » indique que les composants autorisent les actions de type
copier/coller. La finalité, bien sûr, étant de déposer des données à l'endroit souhaité.
Gardez bien en tête que ce sont les fonctionnalités activées par défaut sur ces composants.
Tu veux dire que nous pourrions ajouter cette fonctionnalité à notre JLabel ?
Pour répondre à cette question, nous allons devoir mettre le nez dans le fonctionnement caché de cette
fonctionnalité.
Fonctionnement
Comme beaucoup d'entre vous ont dû le deviner, le transfert des informations entre deux composants se fait
grâce à trois composantes essentielles :
un composant d'origine ;
un composant cible.
Cette vision, bien qu'exacte dans la théorie, se simplifie dans la pratique, pas de panique. Pour schématiser
ce que je viens de vous dire, voici un petit diagramme en figure suivante.
Fonctionnement du drag'n drop
Ce dernier est assez simple à comprendre : pendant l'opération de drag'n drop, les données transitent d'un
composant à l'autre via un objet. Dans l'API Swing, le mécanisme de drag'n drop est encapsulé dans
l'objetJComponent dont tous les objets graphiques héritent, ce qui signifie que tous les objets graphiques
peuvent implémenter cette fonctionnalité.
Afin d'activer le drag'n drop sur un composant graphique qui ne le permet pas par défaut, nous devons
utiliser la méthode setTransferHandler(TransferHandler newHandler) de
l'objet JComponent. Cette méthode prend un objet TransferHandler en paramètre : c'est celui-ci qui
lance le mécanisme de drag'n drop.
Les composants du tableau récapitulatif (hormis le JLabel) ont tous un objet TransferHandler par
défaut. Le drag'n drop s'active par la méthode setDragEnabled(true) sur la plupart des composants,
mais comme vous avez pu le constater, pas sur le JLabel… Afin de contourner cela, nous devons lui
spécifier un objet TransferHandler réalisé par nos soins.
Attention, toutefois ! Vous pouvez définir un TransferHandler pour un objet possédant déjà un
comportement par défaut, mais cette action supplantera le mécanisme par défaut du composant :
redéfinissez donc les comportements avec prudence !
Retournons à notre JLabel. Afin de lui ajouter les fonctionnalités voulues, nous devons lui affecter un
nouveau TransferHandler. Une fois que ce nouvel objet lui sera assigné, nous lui ajouterons un
événement souris afin de lancer l'action de drag'n drop : je vous rappelle que
l'objet TransferHandler ne permet que le transit des données, il ne gère pas les événements ! Dans
notre événement, nous avons juste à récupérer le composant initiateur du drag, récupérer son
objet TransferHandler et invoquer sa méthodeexportAsDrag(JComponent comp,
InputEvent event, int action).
//-------------------------------------------------------------------
//On crée le nouvel objet pour activer le drag'n drop
src.setTransferHandler(new TransferHandler("text"));
//On spécifie au composant qu'il doit envoyer ses données via son objet TransferHandler
src.addMouseListener(new MouseAdapter(){
//On utilise cet événement pour que les actions soient visibles dès le clic de
souris…
//Nous aurions pu utiliser mouseReleased, mais, niveau IHM, nous n'aurions rien vu
public void mousePressed(MouseEvent e){
//On récupère le JComponent
JComponent lab = (JComponent)e.getSource();
//Du composant, on récupère l'objet de transfert : le nôtre
TransferHandler handle = lab.getTransferHandler();
//On lui ordonne d'amorcer la procédure de drag'n drop
handle.exportAsDrag(lab, e, TransferHandler.COPY);
}
});
//-------------------------------------------------------------------
pan.add(srcLib);
pan.add(src);
pan.add(destLib);
pan.add(dest);
setContentPane(pan);
setVisible(true);
}
Avant le drag
Sur la figure suivante, on voit que le contenu est déplacé.
Texte déplacé
Enfin, sur la figure suivante, on déplace un fragment du contenu de notre champ texte vers notre JLabel.
la classe doit être Serializable pour pouvoir sauvegarder et restaurer l'état des instances de
cette classe ;
les propriétés privées de la classe (variables d'instance) doivent être accessibles publiquement
via des méthodes accesseurs (get ou set) suivies du nom de la propriété avec la première
lettre transformée en majuscule ;
En fait, notre objet de transfert va utiliser la propriété « text » de notre objet JLabel, ceci afin de récupérer
son contenu et de le faire transiter. Nous verrons plus tard comment faire pour les cas où nous ne
connaissons pas le nom de la propriété…
Ensuite, nous avons récupéré l'objet TransferHandler depuis notre composant : nous le lui avions
affecté avec un setter, nous pouvons le récupérer avec un getter.
le second paramètre indique à notre objet l'événement sur lequel il doit déclencher le transfert ;
le dernier indique l'action qui doit être effectuée : copie, déplacement, rien…
Comme je vous l'avais dit, il existe plusieurs types d'actions qui peuvent être effectuées lors du drop, celles-
ci sont paramétrables via l'objet TransferHandle :
TransferHandler.LINK : n'autorise que l'action lien sur les données du composant cible ;
cela revient à créer un raccourci ;
Attention, l'objet TransferHandler n'accepte que les actions COPY lorsqu'il est instancié avec le
paramètre « text » : si vous modifiez la valeur ici, votre drag'n drop ne fonctionnera plus.
Alors, même si nous avons réussi à faire un JLabel avec l'option drag'n drop, celui-ci sera restreint ?
Non, mais si nous sommes parvenus à créer un nouveau TranferHandler, pour arriver à débrider notre
composant, nous allons devoir encore approfondir…
Afin de personnaliser le drag'n drop pour notre composant, nous allons devoir mettre les mains dans le
cambouis. La classe TransferHandler fait pas mal de choses dans votre dos et, tout comme les modèles
de composants (cf. JTree, JTable), dès lors que vous y mettez les mains, tout sera à votre charge !
Le but est maintenant de déplacer les données du JLabel vers notre zone de texte façon « couper/coller ».
Vous vous en doutez, nous allons devoir redéfinir le comportement de certaines des méthodes de notre
objet de transfert. Ne vous inquiétez pas, nous allons y aller en douceur. Voici la liste des méthodes que
nous allons utiliser pour arriver à faire ce que nous cherchons :
import javax.swing.TransferHandler;
/**
* Méthode permettant à l'objet de savoir si les données reçues
* via un drop sont autorisées à être importées
* @param info
* @return boolean
*/
public boolean canImport(TransferHandler.TransferSupport info) {}
/**
* C'est ici que l'insertion des données dans notre composant est réalisée
* @param support
* @return boolean
*/
public boolean importData(TransferHandler.TransferSupport support){}
/**
* Cette méthode est invoquée à la fin de l'action DROP
* Si des actions sont à faire ensuite, c'est ici qu'il faudra coder le comportement
désiré
* @param c
* @param t
* @param action
*/
protected void exportDone(JComponent c, Transferable t, int action){}
/**
* Dans cette méthode, nous allons créer l'objet utilisé par le système de drag'n drop
* afin de faire circuler les données entre les composants
* Vous pouvez voir qu'il s'agit d'un objet de type Transferable
* @param c
* @return
*/
protected Transferable createTransferable(JComponent c) {}
/**
* Cette méthode est utilisée afin de déterminer le comportement
* du composant vis-à-vis du drag'n drop : nous retrouverons
* nos variables statiques COPY, MOVE, COPY_OR_MOVE, LINK ou NONE
* @param c
* @return int
*/
public int getSourceActions(JComponent c) {}
}
Commençons par définir le comportement souhaité pour notre composant : le déplacement. Cela se fait via
la méthode public int getSourceActions(JComponent c). Nous allons utiliser les variables
statiques de la classe mère pour définir l'action autorisée :
La seconde étape de notre démarche consiste à autoriser l'import de données vers notre composant grâce à
la méthode public boolean importData(TransferHandler.TransferSupport support) :
//On récupère notre objet Transferable, celui qui contient les données en transit
Transferable data = support.getTransferable();
String str = "";
try {
//Nous récupérons nos données en spécifiant ce que nous attendons
str = (String)data.getTransferData(DataFlavor.stringFlavor);
} catch (UnsupportedFlavorException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
Voilà : à ce stade, nous avons redéfini la copie du champ de texte vers notre JLabel. Voici notre objet en
l'état :
public LabelContentDemo(){
setTitle("Drag'n Drop avec un JLabel !");
setSize(300, 100);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel pan = new JPanel();
pan.setLayout(new GridLayout(2,2));
pan.setBackground(Color.white);
//--------------------------------------------------------
//On utilise notre nouvel objet MyTransferHandle
src.setTransferHandler(new MyTransferHandler());
src.addMouseListener(new MouseAdapter(){
dest.setDragEnabled(true);
pan.add(srcLib);
pan.add(src);
pan.add(destLib);
pan.add(dest);
setContentPane(pan);
setVisible(true);
}
try {
str = (String)data.getTransferData(DataFlavor.stringFlavor);
} catch (UnsupportedFlavorException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
Vous vous doutez de la marche à suivre : cependant, comme je vous l'avais dit au début de ce chapitre,
vous allez être confrontés au problème du positionnement du drop sur votre composant. Cependant, votre
boîte à outils dispose d'un nouvel objet dont le rôle est d'informer sur la position du drop :
l'objetTransferSupport.
Avant de poursuivre dans cette voie, rappelez-vous qu'il faut définir l'action que doit effectuer notre
composant lors du dépôt de nos données. C'est possible grâce à l'objet DropMode que nous pouvons
utiliser via la méthode setDropMode(DropMode dropMode). Voici la liste des modes disponibles :
USE_SELECTION
ON
INSERT
ON_OR_INSERT
INSERT_COLS
INSERT_ROWS
ON_OR_INSERT_COLS
ON_OR_INSERT_ROWS
Vous l'aurez compris : certains modes sont utilisables par des tableaux et d'autres non… Afin que vous
puissiez vous faire votre propre idée sur le sujet, je vous invite à les essayer dans l'exemple qui va suivre.
C'est grâce à cela que nous allons spécifier le mode de fonctionnement de notre arbre.
Maintenant que nous savons comment spécifier le mode de fonctionnement, il ne nous reste plus qu'à
trouver comment, et surtout où insérer le nouvel élément. C'est là que notre ami
le TransfertSupport entre en jeu. Cet objet permet de récupérer un objet DropLocation contenant
toutes les informations nécessaires au bon positionnement des données dans le composant cible.
En fait, par l'objet TransfertSupport, vous pourrez déduire un objet DropLocation propre à votre
composant, par exemple :
Maintenant que je vous ai présenté la marche à suivre et les objets à utiliser, je vous propose un exemple
qui, je pense, parle de lui-même et est assez commenté pour que vous puissiez vous y retrouver. Voici les
classes utilisées.
MyTransferHandler.java
try {
str = (String)data.getTransferData(DataFlavor.stringFlavor);
} catch (UnsupportedFlavorException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
TreeTransferHandler.java
JTree tree;
public TreeTransferHandler(JTree tree){
this.tree = tree;
}
return true;
}
tree.makeVisible(path.pathByAddingChild(nouveau));
tree.scrollPathToVisible(path);
return true;
}
TreeDragDemo.java
//------------------------------------------------------
//On utilise notre nouvel objet MyTransferHandle
src.setTransferHandler(new MyTransferHandler());
src.addMouseListener(new MouseAdapter(){
dest.setDragEnabled(true);
tree = new JTree(getModel());
tree.setDragEnabled(true);
tree.setTransferHandler(new TreeTransferHandler(tree));
pan.add(src);
pan.add(new JScrollPane(tree));
combo.addItemListener(new ItemListener(){
if(value.equals("USE_SELECTION"))
tree.setDropMode(DropMode.USE_SELECTION);
if(value.equals("ON"))
tree.setDropMode(DropMode.ON);
if(value.equals("INSERT"))
tree.setDropMode(DropMode.INSERT);
if(value.equals("ON_OR_INSERT"))
tree.setDropMode(DropMode.ON_OR_INSERT);
}
});
add(pan, BorderLayout.CENTER);
add(combo, BorderLayout.SOUTH);
setVisible(true);
}
root.add(tuto);
root.add(forum);
À la lecture de tous ces chapitres, vous devriez être à même de comprendre et d'assimiler le fonctionnement
du code qui suit. Son objectif est de simuler le déplacement de vos composants sur votre IHM, un peu
comme sur les trois figures suivantes.
MyGlassPane.java
public MyGlassPane(){
//Afin de ne peindre que ce qui nous intéresse
setOpaque(false);
//On définit la transparence
transparence = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.55f);
}
MouseGlassListener.java
}
}
MouseGlassMotionListener.java
/**
* Méthode fonctionnant sur le même principe que la classe précédente
* mais cette fois sur l'action de déplacement
*/
public void mouseDragged(MouseEvent event) {
//Vous connaissez maintenant…
Component c = event.getComponent();
Fenetre.java
public Fenetre(){
super("Test de GlassPane");
setSize(400, 200);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pan.add(bouton1);
pan.add(label);
add(pan, BorderLayout.NORTH);
pan2.add(text);
pan2.add(bouton2);
add(pan2, BorderLayout.SOUTH);
setGlassPane(glass);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
Le drag'n drop n'est disponible via la méthode setDragEnabled(true); que pour certains
composants.
Le drag'n drop permet de récupérer des données d'un composant source pour les transmettre à
un composant cible, le tout via un objet : l'objet TransferHandler.
Afin d'avoir le contrôle du mécanisme de drag'n drop, vous pouvez réaliser votre
propreTransferHandler.
Ce dernier dispose d'une classe interne permettant de gérer la communication entre les
composants (l'objet TransferHandler.TransferSupport) et permet aussi de s'assurer
que les données reçues sont bien du type attendu.
Afin d'améliorer la performance et la réactivité de vos programmes Java, nous allons parler de
l'EDT, pour «Event Dispatch Thread ». Comme son nom l'indique, il s'agit d'un thread, d'une pile
d'appel. Cependant celui-ci a une particularité, il s'occupe de gérer toutes les modifications
portant sur un composant graphique :
le redimensionnement ;
le changement de couleur ;
le changement de valeur ;
…
Vos applications graphiques seront plus performantes et plus sûres lorsque vous utiliserez ce
thread pour effectuer tous les changements qui pourraient intervenir sur votre IHM.
Vous savez déjà que, lorsque vous lancez un programme Java en mode console, un thread
principal est démarré pour empiler les instructions de votre programme jusqu'à la fin. Ce que
vous ignorez peut-être, c'est qu'un autre thread est lancé : celui qui s'occupe de toutes les tâches
de fond (lancement de nouveaux threads…).
Or depuis un certain temps, nous ne travaillons plus en mode console mais en mode graphique.
Et, je vous le donne en mille, un troisième thread est lancé qui se nomme l'EDT (Event Dispatch
Thread). Comme je vous le disais, c'est dans celui-ci que tous les changements portant sur des
composants sont exécutés. Voici un petit schéma illustrant mes dires (figure suivante).
Threads lancés au démarrage
de tout programme Java
La philosophie de Java est que toute modification apportée à un composant se fait
obligatoirement dans l'EDT : lorsque vous utilisez une méthode actionPerformed, celle-ci, son
contenu compris, est exécutée dans l'EDT (c'est aussi le cas pour les autres intercepteurs
d'événements). La politique de Java est simple : toute action modifiant l'état d'un composant
graphique doit se faire dans un seul et unique thread, l'EDT. Vous vous demandez sûrement
pourquoi. C'est simple, les composants graphiques ne sont pas « thread-safe » : ils ne peuvent
pas être utilisés par plusieurs threads simultanément et assurer un fonctionnement sans erreurs !
Alors, pour s'assurer que les composants sont utilisés au bon endroit, on doit placer toutes les
interactions dans l'EDT.
Par contre, cela signifie que si dans une méthode actionPerformed nous avons un traitement
assez long, c'est toute notre interface graphique qui sera figée !
Vous vous souvenez de la première fois que nous avons tenté de contrôler notre animation ?
Lorsque nous cliquions sur le bouton pour la lancer, notre interface était bloquée étant donné que
la méthode contenant une boucle infinie n'était pas dépilée du thread dans lequel elle était
lancée. D'ailleurs, si vous vous souvenez bien, le bouton s'affichait comme si on n'avait pas
relâché le clic ; c'était dû au fait que l'exécution de notre méthode se faisait dans l'EDT, bloquant
ainsi toutes les actions sur nos composants.
Pourquoi les
IHM Java se figent lors de traitements longs
Imaginez la ligne comme une tête de lecture. Il y a déjà quelques événements à faire dans
l'EDT :
la création de la fenêtre ;
Seulement voilà, nous cliquons sur un bouton engendrant un long, un très long traitement dans
l'EDT (dernier bloc) : du coup, toute notre IHM est figée ! Non pas parce que Java est lent, mais
parce que nous avons exécuté un traitement au mauvais endroit. Il existe toutefois quelques
méthodes thread-safe :
paint() et repaint() ;
À ce stade, une question se pose : comment exécuter une action dans l'EDT ? C'est exactement
ce que nous allons voir.
Utiliser l'EDT
Java vous fournit la classe SwingUtilities qui offre plusieurs méthodes statiques permettant
d'insérer du code dans l'EDT :
Maintenant que vous savez comment exécuter des instructions dans l'EDT, il nous faut un cas
concret :
Au lancement de ce test, vous constatez que le thread principal ne reprend la main qu'après la fin
de la méthode updateBouton(), comme le montre la figure suivante.
Thread principal
bloqué durant un traitement
La solution pour rendre la main au thread principal avant la fin de la méthode, vous la
connaissez : créez un nouveau thread, mais cette fois vous allez également exécuter la mise à
jour du bouton dans l'EDT. Voilà donc ce que nous obtenons :
Lancement d'un
traitement dans l'EDT
Ce code est rudimentaire, mais il a l'avantage de vous montrer comment utiliser les méthodes
présentées. Cependant, pour bien faire, j'aurais aussi dû inclure la création de la fenêtre dans
l'EDT, car tout ce qui touche aux composants graphiques doit être mis dans celui-ci.
Pour finir notre tour du sujet, il manque encore la méthode invokeAndWait(). Celle-ci fait la
même chose que sa cousine, mais comme je vous le disais, elle bloque le thread courant jusqu'à
la fin de son exécution. De plus, elle peut lever deux
exceptions : InterruptedException et InvocationTargetException.
Depuis la version 6 de Java, une classe est mise à disposition pour effectuer des traitements
lourds et interagir avec l'EDT.
Cette dernière est une classe abstraite permettant de réaliser des traitements en tâche de fond
tout en dialoguant avec les composants graphiques via l'EDT, aussi bien en cours de traitement
qu'en fin de traitement. Dès que vous aurez un traitement prenant pas mal de temps et devant
interagir avec votre IHM, pensez aux SwingWorker.
Vu que cette classe est abstraite, vous allez devoir redéfinir une méthode : doInBackground().
Elle permet de redéfinir ce que doit faire l'objet en tâche de fond. Une fois cette tâche effectuée,
la méthodedoInBackground() prend fin. Vous avez la possibilité de redéfinir la
méthode done(), qui a pour rôle d'interagir avec votre IHM tout en s'assurant que ce sera fait
dans l'EDT. Implémenter la méthode done()est optionnel, vous n'êtes nullement tenus de le
faire.
Vous constatez que le traitement se fait bien en tâche de fond, et que votre composant est mis à
jour dans l'EDT. La preuve à la figure suivante.
Utilisation d'un
objet SwingWorker
Je vous disais plus haut que vous pouviez interagir avec l'EDT pendant le traitement. Pour ce
faire, il suffit d'utiliser la méthode setProgress(int progress) combinée avec
l'événementPropertyChangeListener, qui sera informé du changement d'état de la
propriété progress.
Nous pouvons donc utiliser ces méthodes dans notre objet SwingWorker afin de récupérer le
résultat d'un traitement. Pour le moment, nous n'avons pas utilisé la généricité de cette classe.
Or, comme l'indique le titre de cette section, SwingWorker peut prendre deux types génériques.
Le premier correspond au type de renvoi de la méthode doInBackground() et, par extension,
au type de renvoi de la méthode get(). Le deuxième est utilisé comme type de retour
intermédiaire pendant l'exécution de la méthodedoInBackground().
Afin de gérer les résultats intermédiaires, vous pouvez utiliser les méthodes suivantes :
Au lancement d'un programme Java, trois threads se lancent : le thread principal, celui
gérant les tâches de fond et l'EDT.
Java préconise que toute modification des composants graphiques se fasse dans
l'EDT.
Si vos IHM se figent, c'est peut-être parce que vous avez lancé un traitement long
dans l'EDT.
Afin d'améliorer la réactivité de vos applications, vous devez choisir au mieux dans
quel thread vous allez traiter vos données.
Java offre la classe SwingUtilities, qui permet de lancer des actions dans l'EDT
depuis n'importe quel thread.
J'espère que vous avez appris tout plein de choses et que vous commencez à faire des choses
sympa avec ce langage de programmation.
#
Partie 4 - Interactions avec les bases de données
DBC : LA PORTE D'ACCÈS AUX BASES DE
DONNÉES
Dans ce chapitre, nous ferons nos premiers pas avec Java DataBase Connectivity, communément
appeléJDBC. Il s'agit en fait de classes Java permettant de se connecter et d'interagir avec des bases de
données. Mais avant toute chose, il nous faut une base de données ! Nous allons donc nous pencher sur
l'utilité d'une base de données et verrons comment en installer une que nous utiliserons afin d'illustrer la
suite de cette partie.
Pour commencer, je pense qu'un petit rappel sur le fonctionnement des bases de données s'impose.
Lorsque vous réalisez un logiciel, un site web ou quelque chose d'autre, vous êtes confrontés tôt ou tard à
cette question : « Comment vais-je procéder pour sauvegarder mes données ? Pourquoi ne pas tout stocker
dans des fichiers ? »
Les bases de données (BDD) permettent de stocker des données. Mais concrètement, comment cela
fonctionne-t-il ? En quelques mots, il s'agit d'un système de fichiers contenant les données de votre
application. Cependant, ces fichiers sont totalement transparents pour l'utilisateur d'une base de données,
donc totalement transparents pour vous ! La différence avec les fichiers classiques se trouve dans le fait que
ce n'est pas vous qui les gérez : c'est votre BDD qui les organise, les range et, le cas échéant, vous retourne
les informations qui y sont stockées. De plus, plusieurs utilisateurs peuvent accéder simultanément aux
données dont ils ont besoin, sans compter que de nos jours, les applications sont amenées à traiter une
grande quantité de données, le tout en réseau. Imaginez-vous gérer tout cela manuellement alors que les
BDD le font automatiquement…
Les données sont ordonnées par « tables », c'est-à-dire par regroupements de plusieurs valeurs. C'est vous
qui créerez vos propres tables, en spécifiant quelles données vous souhaiterez y intégrer. Une base de
données peut être vue comme une gigantesque armoire à tiroirs dont vous spécifiez les noms et qui
contiennent une multitude de fiches dont vous spécifiez aussi le contenu.
Je sais, un schéma est toujours le bienvenu, je vous invite donc à jeter un œil à la figure suivante.
Une BDD contenant deux tables
Dans cette base de données, nous trouvons deux tables : une dont le rôle est de stocker des informations
relatives à des personnes (noms, prénoms et âges) ainsi qu'une autre qui s'occupe de stocker des pays, avec
leur nom et leur capitale.
Si je reprends ma comparaison ci-dessus, la BDD symbolise l'armoire, chaque table représente un tiroir et
chaque ligne de la table correspond à une fiche de ce tiroir ! De plus, ce qui est formidable avec les BDD,
c'est que vous pouvez les interroger en leur posant des questions via un langage précis. Vous pouvez
interroger votre base de données en lui donnant les instructions suivantes :
etc.
Le langage permettant d'interroger des bases de données est le langage SQL (Structured Query
Language ou, en français, « langage de requête structurée »). Grâce aux BDD, vos données sont stockées,
classées par vos soins et identifiables facilement sans avoir à gérer votre propre système de fichiers.
Pour utiliser une BDD, vous avez besoin de deux éléments : la base de données et ce qu'on appelle le
SGBD (Système de Gestion de Base de Données).
Cette partie ne s'intéresse pas au langage SQL. Vous pouvez cependant trouver un cours sur
OpenClassrooms qui traite de MySQL, un SGBDR (« R » signifie « relationnelles ») qui utilise le langage
SQL. Je vous invite à lire le chapitre sur l'insertion de données ainsi que le suivant sur la sélection de
données.
Quelle base de données utiliser
Il existe plusieurs bases de données et toutes sont utilisées par beaucoup de développeurs. Voici une liste
non exhaustive recensant les principales bases :
PostgreSQL ;
MySQL ;
SQL Server ;
Oracle ;
Access.
Toutes ces bases de données permettent d'effectuer les actions que je vous ai expliquées plus haut. Chacune
possède des spécificités : certaines sont payantes (Oracle), d'autres sont plutôt permissives avec les données
qu'elles contiennent (MySQL), d'autres encore sont dotées d'un système de gestion très simple à utiliser
(MySQL), etc. C'est à vous de faire votre choix en regardant par exemple sur Internet ce qu'en disent les
utilisateurs. Pour cette partie traitant des bases de données, mon choix s'est porté sur PostgreSQL qui est
gratuit et complet.
Installation de PostgreSQL
Téléchargez une version de PostgreSQL pour Windows, Linux ou Mac OS X. Je vous invite à
décompresser l'archive téléchargée et à exécuter le fichier.
À partir de maintenant, si je ne mentionne pas une fenêtre de l'assistant d'installation particulière, vous
pouvez laisser les réglages par défaut.
L'installation commence et il vous est demandé votre langue : choisissez et validez. Vous serez invités, par
la suite, à saisir un mot de passe pour l'utilisateur, comme à la figure suivante.
Choix du mot de
passe
Un mot de passe vous sera également demandé pour le « superadministrateur » (voir figure suivante).
Choix du mot de
passe pour le superutilisateur
À la fin de la préinstallation, vous aurez le choix d'exécuter ou non le « Stack Builder » ; ce n'est pas
nécessaire, il permet juste d'installer d'autres logiciels en rapport avec PostgreSQL.
Le serveur est à présent installé : il doit en être de même pour le SGBD ! Pour vérifier que l'installation
s'est bien déroulée, ouvrez le menu « Démarrer » et rendez-vous dans Tous les programmes (sous
Windows) : l'encart PostgreSQL 8.3 (le numéro de version peut être plus récent) doit ressembler à la
figure suivante.
Dans ce dossier, deux exécutables permettent respectivement de lancer et d'arrêter le serveur. Le dernier
exécutable, « pgAdmin III », correspond à notre SGBD : lancez-le, nous allons configurer notre serveur.
Dans le menu Fichier, choisissez Ajouter un serveur…, comme indiqué à la figure.
Hôte : correspond à l'adresse du serveur sur le réseau ; ici, le serveur est situé sur votre ordinateur,
écrivez donc « localhost ».
Vous n'avez normalement pas besoin de modifier le port ; dans le cas contraire, insérez la valeur
qui figure sur l'image, à savoir 5432.
Nous reviendrons sur tout cela, mais vous pouvez observer que votre serveur, nommé « SDZ », possède
une base de données appelée « postgres » ne contenant aucune table. Nous allons maintenant apprendre à
créer une base, des tables et surtout faire un bref rappel sur ce fameux langage SQL.
Premièrement, les bases de données servent à stocker des informations ; ça, vous le savez. Mais ce que
vous ignorez peut-être, c'est que pour ranger correctement nos informations, nous devrons les analyser…
Ce chapitre n'a pas pour objectif de traiter de l'analyse combinée avec des diagrammes entités - associations
(dans le jargon, cela désigne ce dont on se sert pour créer des BDD, c'est-à-dire pour organiser les
informations des tables et de leur contenu)… Nous nous contenterons de poser un thème et d'agir comme si
nous connaissions tout cela !
Pour notre base de données, nous allons donc gérer une école dont voici les caractéristiques :
à chaque classe est attribué un professeur pour chacune des matières dispensées ;
un professeur peut enseigner plusieurs matières et exercer ses fonctions dans plusieurs classes.
Vous vous rendez compte qu'il y a beaucoup d'informations à gérer. En théorie, nous devrions établir un
dictionnaire des données, vérifier à qui appartient quelle donnée, poursuivre avec une modélisation à la
façon MCD (Modèle Conceptuel de Données) et simplifier le tout selon certaines règles, pour terminer
avec un MPD (Modèle Physique de Données). Nous raccourcirons le processus : je vous fournis à la figure
suivante un modèle tout prêt que je vous expliquerai tout de même.
Tous ces éléments correspondent à nos futures tables ; les attributs qui s'y trouvent se nomment des «
champs ». Tous les acteurs mentionnés figurent dans ce schéma (classe, professeur, élève…). Vous
constatez que chaque acteur possède un attribut nommé « id » correspondant à son identifiant : c'est un
champ de type entier qui s'incrémentera à chaque nouvelle entrée ; c'est également grâce à ce champ que
nous pouvons créer des liens entre les acteurs.
Vous devez savoir que les flèches du schéma signifient « a un » ; de ce fait, un élève « a une » classe.
Certaines tables contiennent un champ se terminant par « _k ». Quelques-unes de ces tables possèdent
deux champs de cette nature, pour une raison très simple : parce que nous avons décidé qu'un professeur
pouvait enseigner plusieurs matières, nous avons alors besoin de ce qu'on appelle une « table de
jointures ». Ainsi, nous pouvons spécifier que tel professeur enseigne telle ou telle matière et qu'une
association professeur/matière est assignée à une classe. Ces liens se feront par les identifiants (id).
De plus - il est difficile de ne pas avoir remarqué cela - chaque champ possède un type
(int, double,date, boolean…). Nous savons maintenant tout ce qui est nécessaire pour construire
notre BDD !
Pour cette opération, rien de plus simple : pgAdmin met à notre disposition un outil qui facilite la création
de bases de données et de tables (exécuter tout cela à la main avec SQL, c'est un peu fastidieux). Pour créer
une nouvelle base de données, effectuez un clic droit sur Bases de données, comme àa
la figure suivante.
Renseignez le nom de la base de données (« Ecole » dans notre cas) et choisissez l'encodage UTF-8. Cet
encodage correspond à un jeu de caractères étendu qui autorise les caractères spéciaux. Une fois cela fait,
vous devriez obtenir quelque chose de similaire à la figure suivante.
Première BDD
Vous pouvez désormais voir la nouvelle base de données ainsi que le script SQL permettant de la créer. Il
ne nous reste plus qu'à créer les tables à l'aide du bon type de données…
Nous allons maintenant nous attaquer à la création de nos tables afin de pouvoir travailler correctement. Je
vous expliquerai comment créer une table simple pour vous donner une idée du principe ; je fournirai aux
plus fainéants le script SQL qui finira la création des tables.
Commençons par la table classe, étant donné que c'est l'une des tables qui n'a aucun lien avec une autre.
La procédure est la même que précédemment : il vous suffit d'effectuer un clic droit sur Tables cette fois,
comme le montre la figure suivante.
son nom ;
Ajoutez ensuite les champs, comme le montrent les deux figures suivantes (j'ai ajouté des préfixes aux
champs pour qu'il n'y ait pas d'ambiguïté dans les requêtes SQL).
Ajout d'une colonne à la
table Ajout de la colonne cls_id
Le champ cls_id est de type serial afin qu'il utilise une séquence (le champ s'incrémente ainsi
automatiquement). Nous allons aussi lui ajouter une contrainte de clé primaire.
Placez donc maintenant la contrainte de clé primaire sur votre identifiant, comme représenté à la
figure suivante.
Ajout d'une contrainte de
clé primaire
Cliquez sur « Ajouter ». Choisissez la colonne cls_id et cliquez sur « Ajouter ». Validez ensuite le
tout (figure suivante).
Ajout d'une contrainte
Vous avez vu comment créer une table avec PostgreSQL, mais je ne vais pas vous demander de le faire
pour chacune d'entre elles, je ne suis pas méchant à ce point. Vous n'allez donc pas créer toutes les tables et
tous les champs de cette manière, puisque cet ouvrage a pour but de vous apprendre à utiliser les BDD avec
Java, pas avec le SGBD… Je vous invite donc à télécharger une archive .zip contenant le script SQL de
création des tables restantes ainsi que de leur contenu.
Une fois le dossier décompressé, il ne vous reste plus qu'à ouvrir le fichier avec PostgreSQL en vous
rendant dans l'éditeur de requêtes SQL, comme indiqué à la figure suivante.
Icône d'ouverture de
l'éditeur de requêtes
Vous pouvez à présent ouvrir le fichier que je vous ai fourni en cliquant sur Fichier > Ouvrir puis
choisir le fichier .sql. Exécutez la requête en appuyant sur F5 ou dirigez-vous vers le menu Requête et
choisissez l'action Exécuter. Fermez l'éditeur de requêtes.
Votre base est maintenant entièrement créée, et en plus elle contient des données !
Ceux d'entre vous qui ont déjà installé une imprimante savent que leur machine a besoin d'un driver (appelé
aussi pilote, c'est une sorte de mode d'emploi utilisé par l'ordinateur) pour que la communication puisse
s'effectuer entre les deux acteurs. Ici, c'est la même chose : pgAdmin utilise un driver pour se connecter à
la base de données. Étant donné que les personnes qui ont développé les deux logiciels travaillent main
dans la main, il n'y aura pas de problème de communication ; mais qu'en sera-t-il pour Java ?
En fait, avec Java, vous aurez besoin de drivers, mais pas sous n'importe quelle forme : pour vous connecter
à une base de données, il vous faut un fichier .jar qui correspond au fameux pilote et qui contient tout ce
dont vous aurez besoin pour vous connecter à une base PostgreSQL.
MySQL ;
SQL Server ;
Oracle ;
d'autres bases.
Un bémol toutefois : vous pouvez aussi vous connecter à une BDD en utilisant les pilotes ODBC (Open
DataBase Connectivity) présents dans Windows. Cela nécessite cependant d'installer les pilotes dans
Windows et de les paramétrer dans les sources de données ODBC pour, par la suite, utiliser ces pilotes
ODBC afin de se connecter à la BDD dans un programme Java. Je ne parlerai donc pas de cette méthode
puisqu'elle ne fonctionne que pour Windows.
Pour trouver le driver JDBC qu'il vous faut, une rapide recherche à l'aide de votre moteur de recherche
répondra à vos attentes, comme indiqué à la figure suivante.
Recherche des pilotes JDBC pour PostgreSQL
Sur la page de téléchargement des pilotes pour PostgreSQL, choisissez la dernière version disponible ; pour
ma part, j'ai opté pour la version JDBC4. La version JDBC4 offre des nouveautés et une souplesse
d'utilisation accrue de JDBC, mais vous devez savoir qu'il existe trois autres types de drivers JDBC ; au
total, il en existe donc quatre :
des drivers JDBC de type 1 : JDBC-ODBC, ce type utilise l'interface ODBC pour se connecter à
une base de données (on en a déjà parlé) ; au niveau de la portabilité, on trouve mieux ;
des drivers JDBC de type 2 : ils intègrent les pilotes natifs et les pilotes Java ; en fait, la partie Java
traduit les instructions en natif afin d'être comprises et interprétées par les pilotes natifs ;
des drivers JDBC de type 3 : écrit entièrement en Java, ce type convertit les appels en un langage
totalement indépendant du SGBD ; un serveur intégré traduit ensuite les instructions dans le
langage souhaité par le SGBD ;
des drivers JDBC de type 4 : des pilotes convertissant directement les appels JDBC en instructions
compréhensibles par le SGBD ; ce type de drivers est codé et proposé par les éditeurs de BDD.
Le tout est de savoir si votre application est vouée à être exportée sur différents postes ; dans ce cas,
l'approche CLASSPATH est la plus judicieuse (sinon, il faudra ajouter l'archive dans tous les JRE…). En ce
qui nous concerne, nous utiliserons la deuxième méthode afin de ne pas surcharger nos projets. Je vous
laisse donc placer l'archive téléchargée dans le dossier susmentionné.
Connexion
La base de données est prête, les tables sont créées, remplies et nous possédons le driver nécessaire ! Il ne
nous reste plus qu'à nous connecter. Créons un nouveau projet dans Eclipse avec une classe contenant une
méthode public static void main(String[] args). Voici le code source permettant la
connexion :
} catch (Exception e) {
e.printStackTrace();
}
}
}
Dans un premier temps, nous avons créé une instance de l'objet Driver présent dans le fichier .jar que
nous avons téléchargé. Il est inutile de créer une véritable instance de ce type d'objet ; j'entends par là que
l'instruction org.postgres.Driver driver = new org.postgres.Driver() n'est pas
nécessaire. Nous utilisons alors la réflexivité afin d'instancier cet objet.
À ce stade, il existe comme un pont entre votre programme Java et votre BDD, mais le trafic routier n'y est
pas encore autorisé : il faut qu'une connexion soit effective afin que le programme et la base de données
puissent communiquer. Cela se réalise grâce à cette ligne de code :
l'URL de connexion ;
le nom de l'utilisateur ;
Le premier bloc correspond au début de l'URL de connexion, qui commence toujours par jdbc:. Dans
notre cas, nous utilisons PostgreSQL, la dénomination postgresql: suit donc le début de l'URL. Si vous
utilisez une source de données ODBC, il faut écrire jdbc:odbc:. En fait, cela dépend du pilote JDBC et
permet à Java de savoir quel pilote utiliser.
Dans le deuxième bloc se trouve la localisation de la machine physique sur le réseau ; ici, nous travaillons
en local, nous utilisons donc //localhost:5432. En effet, le nom de la machine physique est suivi du
numéro de port utilisé.
Enfin, dans le dernier bloc, pour ceux qui ne l'auraient pas deviné, il s'agit du nom de notre base de
données.
Les informations des deux derniers blocs dépendent du pilote JDBC utilisé. Pour en savoir plus, consultez
sa documentation.
En exécutant ce code, vous obtiendrez le résultat affiché à la figure suivante.
Connexion effective
Cette procédure lève une exception en cas de problème (mot de passe invalide…).
L'avantage d'utiliser les fichiers .jar comme drivers de connexion est que vous n'êtes pas tenus
d'initialiser le driver par une méthode telle que la réflexivité, tout se passe dans Java. Puisqu'un rappel du
protocole à utiliser est présent dans l'URL de connexion, tout est optimal et Java s'en sort tout seul ! Ne
vous étonnez donc pas si vous ne voyez plus
l'instruction Class.forName("org.postgresql.Driver") par la suite.
JDBC permet à des programmes Java de communiquer avec des bases de données.
Une base de données est un système de fichiers stockant des informations regroupées dans des
tables.
Il existe plusieurs types de drivers JDBC à utiliser selon la façon dont vous souhaitez vous
connecter à la BDD.
Pour vous connecter à votre BDD, vous devez utiliser l'objet Connection fourni par
l'objetDriverManager.
Celui-ci prend en paramètre une URL de connexion permettant d'identifier le type de base de
données, l'adresse du serveur et le nom de la base à interroger, en plus du nom d'utilisateur et du
mot de passe de connexion.
Nous continuons notre voyage initiatique au pays de JDBC en abordant la manière d'interroger notre BDD.
Eh oui, une base de données n'est utile que si nous pouvons consulter, ajouter, modifier et supprimer les
données qu'elle contient. Pour y parvenir, il était impératif de se connecter au préalable. Maintenant que
c'est chose faite, nous allons voir comment fouiner dans notre BDD.
Voici deux objets que vous utiliserez sûrement beaucoup ! En fait, ce sont ces deux objets qui permettent
de récupérer des données de la BDD et de travailler avec celles-ci. Afin de vous faire comprendre tout cela
de façon simple, voici un exemple assez complet (mais tout de même pas exhaustif) affichant le contenu
de la table classe :
System.out.println("\n**********************************");
//On affiche le nom des colonnes
for(int i = 1; i <= resultMeta.getColumnCount(); i++)
System.out.print("\t" + resultMeta.getColumnName(i).toUpperCase() + "\t *");
System.out.println("\n**********************************");
while(result.next()){
for(int i = 1; i <= resultMeta.getColumnCount(); i++)
System.out.print("\t" + result.getObject(i).toString() + "\t |");
System.out.println("\n---------------------------------");
result.close();
state.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
La figure suivante nous montre le résultat de ce code.
Recherche dans la table classe
Les metadatas (ou, plus communément, les métadonnées) constituent en réalité un ensemble de données
servant à décrire une structure. Dans notre cas, elles permettent de connaître le nom des tables, des champs,
leur type…
J'ai simplement exécuté une requête SQL et récupéré les lignes retournées. Mais détaillons un peu plus ce
qu'il s'est passé. Déjà, vous avez pu remarquer que j'ai spécifié l'URL complète pour la connexion : sinon,
comment savoir à quelle BDD se connecter ?
Ce dernier point mis à part, les choses se sont déroulées en quatre étapes distinctes :
fermeture des objets utilisés (bien que non obligatoire, c'est recommandé).
L'objet Statement permet d'exécuter des instructions SQL, il interroge la base de données et retourne
les résultats. Ensuite, ces résultats sont stockés dans l'objet ResultSet, grâce auquel on peut parcourir
les lignes de résultats et les afficher.
Comme je vous l'ai mentionné, l'objet Statement permet d'exécuter des requêtes SQL. Ces dernières
peuvent être de différents types :
CREATE ;
INSERT ;
UPDATE ;
SELECT ;
DELETE.
Puisque cette requête retourne un résultat contenant beaucoup de lignes, contenant elles-mêmes plusieurs
colonnes, j'ai stocké ce résultat dans un objet ResultSet, qui permet d'effectuer diverses actions sur des
résultats de requêtes SQL.
Ici, j'ai utilisé un objet de type ResultSetMetaData afin de récupérer les métadonnées de ma requête,
c'est-à-dire ses informations globales. J'ai ensuite utilisé cet objet afin de récupérer le nombre de colonnes
renvoyé par la requête SQL ainsi que leur nom. Cet objet de métadonnées permet de récupérer des
informations très utiles, comme :
le nom de la table à laquelle appartient la colonne (dans le cas d'une jointure de tables) ;
etc.
Il existe aussi un objet DataBaseMetaData qui fournit des informations sur la base de données.
Vous comprenez mieux à présent ce que signifie cette portion de code :
System.out.println("\n**********************************");
//On affiche le nom des colonnes
for(int i = 1; i <= resultMeta.getColumnCount(); i++)
System.out.print("\t" + resultMeta.getColumnName(i).toUpperCase() + "\t *");
System.out.println("\n**********************************");
Je me suis servi de la méthode retournant le nombre de colonnes dans le résultat afin de récupérer le nom
de la colonne grâce à son index.
Attention, contrairement aux indices de tableaux, les indices de colonnes SQL commencent à 1 !
Ensuite, je récupère les données de la requête en me servant de l'indice des colonnes :
while(result.next()){
for(int i = 1; i <= resultMeta.getColumnCount(); i++)
System.out.print("\t" + result.getObject(i).toString() + "\t |");
System.out.println("\n---------------------------------");
}
J'utilise une première boucle me permettant alors de parcourir chaque ligne via la boucle for tant que
l'objet ResultSet retourne des lignes de résultats. La méthode next() permet de positionner l'objet
sur la ligne suivante de la liste de résultats. Au premier tour de boucle, cette méthode place l'objet sur la
première ligne. Si vous n'avez pas positionné l'objet ResultSet et que vous tentez de lire des données,
une exception est levée !
Je suis parti du principe que le type de données de mes colonnes était inconnu, mais étant donné que je les
connais, le code suivant aurait tout aussi bien fonctionné :
while(result.next()){
System.out.print("\t" + result.getInt("cls_id") + "\t |");
System.out.print("\t" + result.getString("cls_nom") + "\t |");
System.out.println("\n---------------------------------");
}
Je connais désormais le nom des colonnes retournées par la requête SQL. Je connais également leur type, il
me suffit donc d'invoquer la méthode adéquate de l'objet ResultSet en utilisant le nom de la colonne à
récupérer. En revanche, si vous essayez de récupérer le contenu de la colonne cls_nom avec la
méthodegetInt("cls_nom"), vous aurez une exception !
Il existe une méthode getXXX() par type primitif ainsi que quelques autres correspondant aux types SQL :
getArray(int colummnIndex) ;
getAscii(int colummnIndex) ;
getBigDecimal(int colummnIndex) ;
getBinary(int colummnIndex) ;
getBlob(int colummnIndex) ;
getBoolean(int colummnIndex) ;
getBytes(int colummnIndex) ;
getCharacter(int colummnIndex) ;
getDate(int colummnIndex) ;
getDouble(int colummnIndex) ;
getFloat(int colummnIndex) ;
getInt(int colummnIndex) ;
getLong(int colummnIndex) ;
getObject(int colummnIndex) ;
getString(int colummnIndex).
Pour finir, je n'ai plus qu'à fermer mes objets à l'aide des
instructions result.close() etstate.close().
Avant de voir plus en détail les possibilités qu'offrent ces objets, nous allons créer deux ou trois requêtes
SQL afin de nous habituer à la façon dont tout cela fonctionne.
Entraînons-nous
Le but du jeu est de coder les résultats que j'ai obtenus. Voici, en figure suivante, ce que vous devez
récupérer en premier. Je vous laisse chercher dans quelle table nous allons travailler.
Entraînement à la recherche
Cherchez bien… Bon, vous avez sûrement trouvé, il n'y avait rien de compliqué. Voici une des corrections
possibles :
while(result.next()){
System.out.print("\t" + result.getString("prof_nom") + "\t |");
System.out.print("\t" + result.getString("prof_prenom") + "\t |");
System.out.println("\n---------------------------------");
}
result.close();
state.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Allez : on complique la tâche, maintenant ; regardez la figure suivante.
Autre recherche
Ne vous faites pas exploser la cervelle tout de suite, on ne fait que commencer ! Voici un code possible
afin d'obtenir ce résultat :
while(result.next()){
if(!nom.equals(result.getString("prof_nom"))){
nom = result.getString("prof_nom");
System.out.println(nom + " " + result.getString("prof_prenom") + " enseigne : ");
}
System.out.println("\t\t\t - " + result.getString("mat_nom"));
}
result.close();
state.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Allez, un dernier exemple en figure suivante.
if(!nom.equals(result.getString("prof_nom"))){
nom = result.getString("prof_nom");
System.out.println("\t * " + nom + " " + result.getString("prof_prenom") + "
enseigne : ");
}
System.out.println("\t\t\t - " + result.getString("mat_nom"));
}
result.close();
state.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Statement
Vous avez vu comment obtenir un objet Statement. Mais je ne vous ai pas tout dit… Vous savez déjà
que pour récupérer un objet Statement, vous devez le demander gentiment à un objet Connection en
invoquant la méthode createStatement(). Ce que vous ne savez pas, c'est que vous pouvez spécifier
des paramètres pour la création de l'objet Statement. Ces paramètres permettent différentes actions lors
du parcours des résultats via l'objet ResultSet.
TYPE_FORWARD_ONLY : le résultat n'est consultable qu'en avançant dans les données renvoyées, il
est donc impossible de revenir en arrière lors de la lecture ;
CONCUR_READONLY : les données sont consultables en lecture seule, c'est-à-dire que l'on ne peut
modifier des valeurs pour mettre la base à jour ;
CONCUR_UPDATABLE : les données sont modifiables ; lors d'une modification, la base est mise à
jour.
Par défaut, les ResultSet issus d'un Statement sont de type TYPE_FORWARD_ONLY pour le parcours
et CONCUR_READONLY pour les actions réalisables.
Ces paramètres sont des variables statiques de la classe ResultSet, vous savez donc comment les utiliser.
Voici comment créer un Statement permettant à l'objet ResultSet de pouvoir être lu d'avant en arrière
avec possibilité de modification :
Il va falloir vous accrocher un tout petit peu… De tels objets sont créés exactement de la même façon que
desStatement classiques, sauf qu'au lieu de cette instruction :
En fait, vous pouvez insérer un caractère spécial dans vos requêtes et remplacer ce caractère grâce à des
méthodes de l'objet PreparedStatement en spécifiant sa place et sa valeur (son type étant défini par la
méthode utilisée).
Voici un exemple :
prepare.close();
state.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Cela nous donne la figure suivante.
Requête préparée
Pour en terminer avec les méthodes de l'objet PreparedStatement que je présente ici (il en existe
d'autres), prepare.clearParameters() permet de réinitialiser la requête préparée afin de retirer
toutes les valeurs renseignées. Si vous ajoutez cette méthode à la fin du code que je vous ai présenté et que
vous affichez à nouveau le contenu de l'objet, vous obtenez la figure suivante.
ResultSet : le retour
Maintenant que nous avons vu comment procéder, nous allons apprendre à nous promener dans nos
objetsResultSet. En fait, l'objet ResultSet offre beaucoup de méthodes permettant d'explorer les
résultats, à condition que vous ayez bien préparé l'objet Statement.
vous positionner sur une ligne par rapport à votre emplacement actuel : res.relative(-3).
Je vous ai concocté un morceau de code que j'ai commenté et qui met tout cela en oeuvre.
//CTRL + SHIFT + O pour générer les imports
public class Resultset {
public static void main(String[] args) {
try {
Class.forName("org.postgresql.Driver");
String url = "jdbc:postgresql://localhost:5432/Ecole";
String user = "postgres";
String passwd = "postgres";
System.out.println("\n\t---------------------------------------");
System.out.println("\tLECTURE STANDARD.");
System.out.println("\t---------------------------------------");
while(res.next()){
System.out.println("\tNom : "+res.getString("prof_nom") +" \t prénom :
"+res.getString("prof_prenom"));
//On regarde si on se trouve sur la dernière ligne du résultat
if(res.isLast())
System.out.println("\t\t* DERNIER RESULTAT !\n");
i++;
}
System.out.println("\t---------------------------------------");
System.out.println("\tLecture en sens contraire.");
System.out.println("\t---------------------------------------");
System.out.println("\t---------------------------------------");
System.out.println("\tAprès positionnement absolu du curseur à la place N° "+ i/2 +
".");
System.out.println("\t---------------------------------------");
//On positionne le curseur sur la ligne i/2
//Peu importe où on se trouve
res.absolute(i/2);
while(res.next())
System.out.println("\tNom : "+res.getString("prof_nom") +" \t prénom : "+
res.getString("prof_prenom"));
System.out.println("\t---------------------------------------");
System.out.println("\tAprès positionnement relatif du curseur à la place N° "+(i-(i-
2)) + ".");
System.out.println("\t---------------------------------------");
//On place le curseur à la ligne actuelle moins i-2
//Si on n'avait pas mis de signe moins, on aurait avancé de i-2 lignes
res.relative(-(i-2));
while(res.next())
System.out.println("\tNom : "+res.getString("prof_nom") +" \t prénom :
"+res.getString("prof_prenom"));
res.close();
state.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
La figure suivante montre le résultat obtenu.
Utilisation d'un
ResultSet
Il est très important de noter l'endroit où vous vous situez dans le parcours de la requête !
Il existe des emplacements particuliers. Par exemple, si vous n'êtes pas encore positionnés sur le premier
élément et que vous procédez à un rs.relative(1), vous vous retrouvez sur le premier élément. De
même, un rs.absolute(0) correspond à un rs.beforeFirst().
Ce qui signifie que lorsque vous souhaitez placer le curseur sur la première ligne, vous devez
utiliserabsolute(1) quel que soit l'endroit où vous vous trouvez ! En revanche, cela nécessite que
leResultSet soit de type TYPE_SCROLL_SENSITIVE ou TYPE_SCROLL_INSENSITIVE, sans quoi
vous aurez une exception.
Pendant la lecture, vous pouvez utiliser des méthodes qui ressemblent à celles que je vous ai déjà
présentées lors du parcours d'un résultat. Souvenez-vous des méthodes de ce type :
res.getAscii() ;
res.getBytes() ;
res.getInt() ;
res.getString() ;
etc.
Ici, vous devez remplacer getXXX() par updateXXX(). Ces méthodes de mise à jour des données
prennent deux paramètres :
et ainsi de suite.
Changer la valeur d'un champ est donc très facile. Cependant, il faut, en plus de changer les valeurs,
valider ces changements pour qu'ils soient effectifs : cela se fait par la méthode updateRow(). De la
même manière, vous pouvez annuler des changements grâce à la méthode cancelRowUpdates().
Sachez que si vous devez annuler des modifications, vous devez le faire avant la méthode de validation,
sinon l'annulation sera ignorée.
Je vous propose d'étudier un exemple de mise à jour :
//Et voilà !
System.out.println("*********************************");
System.out.println("APRES REMODIFICATION : ");
System.out.println("\tNOM : " + res.getString("prof_nom") + " - PRENOM : " +
res.getString("prof_prenom") + "\n");
res.close();
state.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
La figure suivante représente ce que nous obtenons.
Mise à jour d'une ligne pendant la lecture
En quelques instants, les données ont été modifiées dans la base de données, nous avons donc réussi à
relever le défi !
Nous allons maintenant voir comment exécuter les autres types de requêtes avec Java.
Vous savez depuis quelque temps déjà que ce sont les objets Statement qui sont chargés d'exécuter les
instructions SQL. Par conséquent, vous devez avoir deviné que les requêtes de
type INSERT, UPDATE,DELETE et CREATE sont également exécutées par ces objets. Voici un code
d'exemple :
res = state.executeQuery(query);
res.first();
//On affiche une nouvelle fois
System.out.println("\n\t\t REMISE A ZERO : ");
System.out.println("\t\t * NOM : " + res.getString("prof_nom") + " - PRENOM :" +
res.getString("prof_prenom"));
prepare.close();
res.close();
state.close();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
Cela correspond à la figure suivante.
Ici, nous avons utilisé un PreparedStatement pour compliquer immédiatement, mais nous aurions
tout aussi bien pu utiliser un simple Statement et invoquer la méthode executeUpdate(String
query).
Vous savez quoi ? Pour les autres types de requêtes, il suffit d'invoquer la même méthode que pour la mise
à jour. En fait, celle-ci retourne un booléen indiquant si le traitement a réussi ou échoué. Voici quelques
exemples :
Par où commencer ? Lorsque vous insérez, modifiez ou supprimez des données dans PostgreSQL, il se
produit un événement automatique : la validation des modifications par le moteur SQL. C'est aussi
simple que ça… Voici un petit schéma en figure suivante pour que vous visualisiez cela.
Lorsque vous exécutez une requête de type INSERT, CREATE, UPDATE ou DELETE, le type de cette
requête modifie les données présentes dans la base. Une fois qu'elle est exécutée, le moteur SQL valide
directement ces modifications !
Comme cela, c'est vous qui avez le contrôle sur vos données afin de maîtriser l'intégrité de vos données.
Imaginez que vous deviez exécuter deux requêtes, une modification et une insertion, et que vous partiez du
principe que l'insertion dépend de la mise à jour… Comment feriez-vous si de mauvaises données étaient
mises à jour ? L'insertion qui en découle serait mauvaise. Cela, bien sûr, si le moteur SQL valide
automatiquement les requêtes exécutées.
Pour gérer manuellement les transactions, on spécifie au moteur SQL de ne pas valider automatiquement
les requêtes SQL grâce à une méthode (qui ne concernera toutefois pas l'objet Statement, mais
l'objetConnection) prenant un booléen en paramètre :
} catch (Exception e) {
e.printStackTrace();
}
}
}
Lorsque vous souhaitez que vos requêtes soient prises en compte, il vous faut les valider en utilisant la
méthode conn.commit().
En mode setAutoCommit(false), si vous ne validez pas vos requêtes, elles ne seront pas prises en
compte.
Vous pouvez revenir à tout moment au mode de validation automatique grâce à setAutoCommit(true).
Voici un exemple :
state.executeUpdate(query);
result.close();
state.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Vous pouvez exécuter ce code autant de fois que vous voulez, vous obtiendrez toujours la même chose que
sur la figure suivante.
Transaction manuelle
Vous voyez que malgré sa présence, la requête de mise à jour est inopérante. Vous pouvez voir les
modifications lors de l'exécution du script, mais étant donné que vous ne les avez pas validées, elles sont
annulées à la fin du code. Pour que la mise à jour soit effective, il faudrait effectuer
un conn.commit()avant la fin du script.
Il existe un autre objet qui fonctionne de la même manière que l'objet ResultSet, mais qui
précompile la requête et permet d'utiliser un système de requête à trous :
l'objetPreparedStatement.
En utilisant les transactions manuelles, toute instruction non validée par la méthode commit() de
l'objet Connection est annulée.
Vous savez désormais comment vous connecter à une BDD depuis Java. Je vous ai montré comment lire et
modifier des données. Après vous avoir fait découvrir tout cela, je me suis dit que montrer une approche un
peu plus objet ne serait pas du luxe. C'est vrai, établir sans arrêt la connexion à notre base de données
commence à être fastidieux. Je vous propose donc d'y remédier avec ce chapitre en découvrant le pattern
singleton.
Pourquoi veux-tu absolument qu'on ait une seule instance de notre objet Connection ?
Parce que cela ne sert pas à grand-chose de réinitialiser la connexion à votre BDD. Rappelez-vous que la
connexion sert à établir le pont entre votre base et votre application. Pourquoi voulez-vous que votre
application se connecte à chaque fois à la BDD ? Une fois la connexion effective, pourquoi vouloir l'établir
de nouveau ? Votre application et votre BDD peuvent discuter !
Bon, c'est vrai qu'avec du recul, cela paraît superflu… Du coup, comment fais-tu pour garantir qu'une
seule instance de Connection existe dans l'application ?
C'est ici que le pattern singleton intervient ! Ce pattern est peut-être l'un des plus simples à comprendre
même, s'il contient un point qui va vous faire bondir : le principe de base est d'interdire l'instanciation d'une
classe, grâce à un constructeur déclaré private.
Le pattern singleton
Nous voulons qu'il soit impossible de créer plus d'un objet de connexion. Voici une classe qui permet de
s'assurer que c'est le cas :
//Constructeur privé
private SdzConnection(){
try {
connect = DriverManager.getConnection(url, user, passwd);
} catch (SQLException e) {
e.printStackTrace();
}
}
//Méthode qui va nous retourner notre instance et la créer si elle n'existe pas
public static Connection getInstance(){
if(connect == null){
new SdzConnection();
}
return connect;
}
}
Nous avons ici une classe avec un constructeur privé : du coup, impossible d'instancier cet objet et
d'accéder à ses attributs, puisqu'ils sont déclarés private ! Notre objet Connection est instancié dans le
constructeur privé et la seule méthode accessible depuis l'extérieur de la classe est getInstance(). C'est
donc cette méthode qui a pour rôle de créer la connexion si elle n'existe pas, et seulement dans ce cas.
Pour en être bien sûrs, nous allons faire un petit test… Voici le code un peu modifié de la
méthodegetInstance() :
SdzConnection.getInstance().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
La méthode en question est appelée quatre fois. Que croyez-vous que ce code va afficher ? Quelque chose
comme la figure suivante !
Vous pouvez relancer le code de test, vous verrez qu'il fonctionne toujours ! J'avais commencé par insérer
un constructeur privé car vous deviez savoir que cela existait, mais remarquez que c'était superflu dans
notre cas…
Par contre, dans une application multithreads, pour être sûrs d'éviter les conflits, il vous suffit de
synchroniser la méthode getInstance() et le tour est joué. Mais - parce qu'il y a un mais - cette
méthode ne règle le problème qu'avant l'instanciation de la connexion. Autrement dit, une fois la connexion
instanciée, la synchronisation ne sert plus à rien.
Le problème du multithreading ne se pose pas vraiment pour une connexion à une BDD puisque
ce singletonsert surtout de passerelle entre votre BDD et votre application. Cependant, il peut exister
d'autres objets que des connexions SQL qui ne doivent être instanciés qu'une fois ; tous ne sont pas aussi
laxistes concernant le multithreading.
Voyons donc comment parfaire ce pattern avec un exemple autre qu'une connexion SQL.
Nous allons travailler avec un autre exemple et vu que j'étais très inspiré, revoici notre superbe singleton :
//Constructeur privé
private SdzSingleton(){
this.name = "Mon singleton";
System.out.println("\t\tCRÉATION DE L'INSTANCE ! ! !");
}
return single;
}
//Accesseur
public String getName(){
return this.name;
}
}
Ce n'est pas que je manquais d'inspiration, c'est juste qu'avec une classe toute simple, on comprend mieux
les choses… Et voici notre classe de test :
Un petit singleton
La politique du singleton est toujours bonne. Mais je vais vous poser une question : quand croyez-vous que
la création d'une instance soit la plus judicieuse ? Ici, nous avons exécuté notre code et l'instance a été créée
lorsqu'on l'a demandée pour la première fois ! C'est le principal problème que posent le singleton et le
multithreading : la première instance… Une fois celle-ci créée, les problèmes se font plus rares.
Pour limiter les ennuis, nous allons donc laisser cette lourde tâche à la JVM, dès le chargement de la classe,
en instanciant notre singleton lors de sa déclaration :
//Constructeur privé
private SdzSingleton(){
this.name = "Mon singleton";
System.out.println("\t\tCRÉATION DE L'INSTANCE ! ! !");
}
return single;
}
//Accesseur
public String getName(){
return this.name;
}
}
Avec ce code, c'est la machine virtuelle qui s'occupe de charger l'instance du singleton, bien avant que
n'importe quel thread vienne taquiner la méthode getInstance()…
Il existe une autre méthode permettant de faire cela, mais elle ne fonctionne parfaitement que depuis le
JDK 1.5. On appelle cette méthode « le verrouillage à double vérification ». Elle consiste à utiliser le mot
clévolatile combiné au mot clé synchronized.
Pour les lecteurs qui l'ignorent, déclarer une variable volatile permet d'assurer un accès ordonné des
threads à une variable (plusieurs threads peuvent accéder à cette variable), marquant ainsi le premier point
de verrouillage. Ensuite, la double vérification s'effectue dans la méthode getInstance() : on effectue
la synchronisation uniquement lorsque le singleton n'est pas créé.
private SdzSingleton(){
this.name = "Mon singleton";
System.out.println("\n\t\tCRÉATION DE L'INSTANCE ! ! !");
}
Pour économiser les ressources, vous ne devriez créer qu'un seul objet de connexion.
Ce pattern repose sur un constructeur privé associé à une méthode retournant l'instance créée
dans la classe elle-même.
TP : UN TESTEUR DE REQUÊTES
Vous avez appris un tas de choses sur JDBC et il est grand temps que vous les mettiez en pratique !
Dans ce TP, je vais vous demander de réaliser un testeur de requêtes SQL. Vous ne voyez pas où je veux en
venir ? Lisez donc la suite…
créer une IHM permettant la saisie d'une requête SQL dans un champ ;
lancer l'exécution de la requête grâce à un bouton se trouvant dans une barre d'outils ;
un petit message en bas de fenêtre affichera le temps d'exécution de la requête ainsi que le
nombre de lignes retournées.
Si vous ne savez pas comment procéder pour le temps d'exécution de la requête, voici un indice
:System.currentTimeMillis() retourne un long…
Les figures suivantes vous montrent ce que j'ai obtenu avec mon code. Inspirez-vous-en pour réaliser votre
programme.
Au lancement
Exécution d'une requête Erreur
dans une requête
Je n'ai plus qu'à vous souhaiter bonne chance et bon courage ! Let's go !
Correction
Classe SdzConnection.java
package com.sdz.connection;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import javax.swing.JOptionPane;
/**
* URL de connection
*/
private static String url = "jdbc:postgresql://localhost:5432/Ecole";
/**
* Nom du user
*/
private static String user = "postgres";
/**
* Mot de passe du user
*/
private static String passwd = "postgres";
/**
* Objet Connection
*/
private static Connection connect;
/**
* Méthode qui va retourner notre instance
* et la créer si elle n'existe pas...
* @return
*/
public static Connection getInstance(){
if(connect == null){
try {
connect = DriverManager.getConnection(url, user, passwd);
} catch (SQLException e) {
JOptionPane.showMessageDialog(null, e.getMessage(), "ERREUR DE CONNEXION ! ",
JOptionPane.ERROR_MESSAGE);
}
}
return connect;
}
}
Classe Fenetre.java
package com.sdz.tp;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JToolBar;
import com.sdz.connection.SdzConnection;
/**
* ToolBar pour le lancement des requêtes
*/
private JToolBar tool = new JToolBar();
/**
* Le bouton
*/
private JButton load = new JButton(new ImageIcon("img/load.png"));
/**
* Le délimiteur
*/
private JSplitPane split;
/**
* Le conteneur de résultat
*/
private JPanel result = new JPanel();
/**
* Requête par défaut pour le démarrage
*/
private String requete = "SELECT * FROM classe";
/**
* Le composant dans lequel taper la requête
*/
private JTextArea text = new JTextArea(requete);
/**
* Constructeur
*/
public Fenetre(){
setSize(900, 600);
setTitle("TP JDBC");
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
initToolbar();
initContent();
initTable(requete);
}
/**
* Initialise la toolbar
*/
private void initToolbar(){
load.setPreferredSize(new Dimension(30, 35));
load.setBorder(null);
load.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent event){
initTable(text.getText());
}
});
tool.add(load);
getContentPane().add(tool, BorderLayout.NORTH);
}
/**
* Initialise le contenu de la fenêtre
*/
public void initContent(){
//Vous connaissez ça...
result.setLayout(new BorderLayout());
split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, new JScrollPane(text), result);
split.setDividerLocation(100);
getContentPane().add(split, BorderLayout.CENTER);
}
/**
* Initialise le visuel avec la requête saisie dans l'éditeur
* @param query
*/
public void initTable(String query){
try {
//On crée un statement
long start = System.currentTimeMillis();
Statement state =
SdzConnection.getInstance().createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
j++;
}
} catch (SQLException e) {
//Dans le cas d'une exception, on affiche une pop-up et on efface le contenu
result.removeAll();
result.add(new JScrollPane(new JTable()), BorderLayout.CENTER);
result.revalidate();
JOptionPane.showMessageDialog(null, e.getMessage(), "ERREUR ! ",
JOptionPane.ERROR_MESSAGE);
}
}
/**
* Point de départ du programme
* @param args
*/
public static void main(String[] args){
Fenetre fen = new Fenetre();
fen.setVisible(true);
}
}
Bien sûr, ce code n'est pas parfait, vous pouvez l'améliorer ! Voilà d'ailleurs quelques pistes :
vous pouvez utiliser un autre composant que moi pour la saisie de la requête, par exemple
unJTextPane pour la coloration syntaxique ;
vous pouvez également créer un menu qui vous permettra de sauvegarder vos requêtes ;
vous pouvez également créer un tableau interactif autorisant la modification des données.
Vous voulez utiliser vos données dans des objets, et c'est normal ! Vous avez sans doute essayé de faire en
sorte que les données de votre base collent à vos objets, à l'aide des méthodes de récupération, de création,
de mise à jour et (ou) de suppression, sans obtenir le résultat escompté.
Avec le pattern DAO (Data Access Object), vous allez voir comment procéder et surtout, comment rendre
le tout stable !
Vous voulez que les données de la base puissent être utilisées via des objets Java ? En tout premier lieu, il
faut créer une classe par entité (les tables, exceptées celles de jointure), ce qui nous donnerait les classes
suivantes :
Eleve ;
Matiere ;
Professeur ;
Classe.
Et, si nous suivons la logique des relations entre nos tables, nos classes sont liées suivant le diagramme de
classes correspondant à la figure suivante.
Diagramme de classe de notre
BDD
Grâce à ce diagramme, nous voyons les liens entre les objets : une classe est composée de plusieurs élèves
et de plusieurs professeurs, et un professeur peut exercer plusieurs matières. Les tables de jointures de la
base sont symbolisées par la composition dans nos objets.
Une fois que cela est fait, nous devons coder ces objets avec les accesseurs et les mutateurs adéquats :
On appelle ce genre d'objet des « POJO », pour Plain Old Java Object ! Ce qui nous donne ces codes
source :
Classe Eleve.java
package com.sdz.bean;
Classe Matiere.java
package com.sdz.bean;
public Matiere(){}
Classe Professeur.java
package com.sdz.bean;
import java.util.HashSet;
import java.util.Set;
public Professeur(){}
Classe Classe.java
package com.sdz.bean;
Le pattern DAO
Contexte
Vous disposez de données sérialisées dans une base de données et vous souhaitez les manipuler avec des
objets Java. Cependant, votre entreprise est en pleine restructuration et vous ne savez pas si vos données
vont :
…
Comment faire en sorte de ne pas avoir à modifier toutes les utilisations de nos objets ? Comment réaliser
un système qui pourrait s'adapter aux futures modifications de supports de données ? Comment procéder
afin que les objets que nous allons utiliser restent tels qu'ils sont ?
Le pattern DAO
Ce pattern permet de faire le lien entre la couche d'accès aux données et la couche métier d'une application
(vos classes). Il permet de mieux maîtriser les changements susceptibles d'être opérés sur le système de
stockage des données ; donc, par extension, de préparer une migration d'un système à un autre (BDD vers
fichiers XML, par exemple…). Ceci se fait en séparant accès aux données (BDD) et objets métiers (POJO).
Je me doute que tout ceci doit vous sembler très flou. C'est normal, mais ne vous en faites pas, je vais tout
vous expliquer… Déjà, il y a cette histoire de séparation des couches métier et des couches d'accès aux
données. Il s'agit ni plus ni moins de faire en sorte qu'un type d'objet se charge de récupérer les données
dans la base et qu'un autre type d'objet (souvent des POJO) soit utilisé pour manipuler ces données.
Schématiquement, ça nous donne la figure suivante.
Fonctionnement du pattern DAO
Les objets que nous avons créés plus haut sont nos POJO, les objets utilisés par le programme pour
manipuler les données de la base. Les objets qui iront chercher les données en base devront être capables
d'effectuer des recherches, des insertions, des mises à jour et des suppressions ! Par conséquent, nous
pouvons définir un super type d'objet afin d'utiliser au mieux le polymorphisme… Nous allons devoir créer
une classe abstraite (ou une interface) mettant en oeuvre toutes les méthodes sus-mentionnées.
Comment faire pour demander à nos objets DAO de récupérer tel type d'objet ou de sérialiser tel autre ?
Avec des cast ?
Soit avec des cast, soit en créant une classe générique (figure suivante) !
Classe DAO
Afin de ne pas surcharger les codes d'exemples, j'ai volontairement utilisé des objets Statementmais il
aurait mieux valu utiliser des requêtes préparées (PreparedStatement) pour des questions de
performances et de sécurité.
Classe DAO.java
package com.sdz.dao;
import java.sql.Connection;
import com.sdz.connection.SdzConnection;
/**
* Méthode de création
* @param obj
* @return boolean
*/
public abstract boolean create(T obj);
/**
* Méthode pour effacer
* @param obj
* @return boolean
*/
public abstract boolean delete(T obj);
/**
* Méthode de mise à jour
* @param obj
* @return boolean
*/
public abstract boolean update(T obj);
/**
* Méthode de recherche des informations
* @param id
* @return T
*/
public abstract T find(int id);
}
Classe EleveDAO.java
package com.sdz.dao.implement;
//CTRL + SHIFT + O pour générer les imports
public class EleveDAO extends DAO<Eleve> {
public EleveDAO(Connection conn) {
super(conn);
}
try {
ResultSet result = this.connect.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY).executeQuery("SELECT * FROM eleve WHERE elv_id = " +
id);
if(result.first())
eleve = new Eleve(
id,
result.getString("elv_nom"),
result.getString("elv_prenom"
));
} catch (SQLException e) {
e.printStackTrace();
}
return eleve;
}
}
Classe MatiereDAO.java
package com.sdz.dao.implement;
//CTRL + SHIFT + O pour générer les imports
public class MatiereDAO extends DAO<Matiere> {
public MatiereDAO(Connection conn) {
super(conn);
}
try {
ResultSet result = this.connect.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY
).executeQuery("SELECT * FROM matiere WHERE mat_id = " + id);
if(result.first())
matiere = new Matiere(id, result.getString("mat_nom"));
} catch (SQLException e) {
e.printStackTrace();
}
return matiere;
}
}
Classe ProfesseurDAO.java
package com.sdz.dao.implement;
//CTRL + SHIFT + O pour générer les imports
public class ProfesseurDAO extends DAO<Professeur> {
public ProfesseurDAO(Connection conn) {
super(conn);
}
while(result.next())
professeur.addMatiere(matDao.find(result.getInt("mat_id")));
}
} catch (SQLException e) {
e.printStackTrace();
}
return professeur;
}
public boolean update(Professeur obj) {
return false;
}
}
Classe ClasseDAO.java
package com.sdz.dao.implement;
//CTRL + SHIFT + O pour générer les imports
public class ClasseDAO extends DAO<Classe> {
public ClasseDAO(Connection conn) {
super(conn);
}
if(result.first()){
classe = new Classe(id, result.getString("cls_nom"));
result = this.connect.createStatement().executeQuery(
"SELECT prof_id, prof_nom, prof_prenom from professeur " +
"INNER JOIN j_mat_prof ON prof_id = jmp_prof_k " +
"INNER JOIN j_cls_jmp ON jmp_id = jcm_jmp_k AND jcm_cls_k = " + id
);
while(result.next())
classe.addProfesseur(profDao.find(result.getInt("prof_id")));
while(result.next())
classe.addEleve(eleveDao.find(result.getInt("elv_id")));
}
} catch (SQLException e) {
e.printStackTrace();
}
return classe;
}
}
Pour ne pas compliquer la tâche, je n'ai détaillé que la méthode de recherche des données, les autres sont
des coquilles vides. Mais vous devriez être capables de faire ça tout seuls.
Premier test
Nous avons réalisé une bonne partie de ce pattern, nous allons pouvoir faire notre premier test.
import com.sdz.bean.Classe;
//CTRL + SHIFT + O pour générer les imports
public class FirstTest {
public static void main(String[] args) {
//Testons des élèves
DAO<Eleve> eleveDao = new EleveDAO(SdzConnection.getInstance());
for(int i = 1; i < 5; i++){
Eleve eleve = eleveDao.find(i);
System.out.println("Elève N°" + eleve.getId() + " - " + eleve.getNom() + " " +
eleve.getPrenom());
}
System.out.println("\n********************************\n");
System.out.println("\n********************************\n");
Le pattern factory
Nous allons aborder ici une notion importante : la fabrication d'objets ! En effet, le pattern DAO
implémente aussi ce qu'on appelle le pattern factory. Celui-ci consiste à déléguer l'instanciation d'objets à
une classe.
En fait, une fabrique ne fait que ça. En général, lorsque vous voyez ce genre de code dans une classe :
class A{
public Object getData(int type){
Object obj;
//----------------------
if(type == 0)
obj = new B();
else if(type == 1)
obj = new C();
else
obj = new D();
//----------------------
obj.doSomething();
obj.doSomethingElse();
}
}
… vous constatez que la création d'objets est conditionnée par une variable et que, selon cette dernière,
l'objet instancié n'est pas le même. Nous allons donc extraire ce code pour le mettre dans une classe à part :
package com.sdz.transact;
B b = Factory.getData(0);
C c = Factory.getData(1);
//…
Pourquoi faire tout ça ? En temps normal, nous travaillons avec des objets concrets, non soumis au
changement. Cependant, dans le cas qui nous intéresse, nos objets peuvent être amenés à changer. Et j'irai
même plus loin : le type d'objet utilisé peut changer !
L'avantage d'utiliser une fabrique, c'est que les instances concrètes (utilisation du mot clé new) se font à un
seul endroit ! Donc, si nous devons faire des changements, il ne se feront qu'à un seul endroit. Si nous
ajoutons un paramètre dans le constructeur, par exemple…
Je vous propose maintenant de voir comment ce pattern est implémenté dans le pattern DAO.
En fait, la factory dans le pattern DAO sert à construire nos instances d'objets d'accès aux données. Du
coup, vu que nous disposons d'un super type d'objet, nous savons ce que va retourner notre fabrique
(figure suivante).
Diagramme de classe de notre factory
Voici le code de notre fabrique :
package com.sdz.dao;
//CTRL + SHIFT + O pour générer les imports
public class DAOFactory {
protected static final Connection conn = SdzConnection.getInstance();
/**
* Retourne un objet Classe interagissant avec la BDD
* @return DAO
*/
public static DAO getClasseDAO(){
return new ClasseDAO(conn);
}
/**
* Retourne un objet Professeur interagissant avec la BDD
* @return DAO
*/
public static DAO getProfesseurDAO(){
return new ProfesseurDAO(conn);
}
/**
* Retourne un objet Eleve interagissant avec la BDD
* @return DAO
*/
public static DAO getEleveDAO(){
return new EleveDAO(conn);
}
/**
* Retourne un objet Matiere interagissant avec la BDD
* @return DAO
*/
public static DAO getMatiereDAO(){
return new MatiereDAO(conn);
}
}
Et voici un code qui devrait vous plaire :
System.out.println("\n\t****************************************");
System.out.println("\n\t****************************************");
On a bien compris le principe du pattern DAO, ainsi que la combinaison DAO - factory. Cependant, on ne
voit pas comment gérer plusieurs systèmes de sauvegarde de données. Faut-il modifier les DAO à chaque
fois ?
Non, bien sûr… Chaque type de gestion de données (PostgreSQL, XML, MySQL…) peut disposer de son
propre type de DAO. Le vrai problème, c'est de savoir comment récupérer les DAO, puisque nous avons
délégué leurs instanciations à une fabrique. Vous allez voir : les choses les plus compliquées peuvent être
aussi les plus simples.
De l'usine à la multinationale
Le fait est que notre structure actuelle fonctionne pour notre système actuel. Ah ! Mais ! Qu'entends-je,
qu'ouïs-je ? Votre patron vient de trancher ! Vous allez utiliser PostgreSQL et du XML !
C'est bien ce qu'on disait plus haut… Comment gérer ça ? On ne va pas mettre
des if(){…}else{…} dans la fabrique, tout de même ?
Vous voulez insérer des conditions afin de savoir quel type d'instance retourner : ça ressemble grandement
à une portion de code pouvant être déclinée en fabrique !
Oui ! Notre fabrique actuelle nous permet de construire des objets accédant à des données se trouvant dans
une base de données PostgreSQL. Mais maintenant, le défi consiste à utiliser aussi des données provenant
de fichiers XML.
Nous allons donc créer une classe abstraite pour nos futures fabriques. Elle devra avoir les méthodes
permettant de récupérer les différents DAO et une méthode permettant d'instancier la bonne fabrique ! Je
vous ai préparé un diagramme de classe à la figure suivante, vous comprendrez mieux.
Diagramme de
classe de nos factory
Je vous ai même préparé les codes source :
Classe AbstractDAOFactory.java
package com.sdz.dao;
Classe DAOFactory.java
package com.sdz.dao;
//CTRL + SHIFT + O pour générer les imports
public class DAOFactory extends AbstractDAOFactory{
protected static final Connection conn = SdzConnection.getInstance();
Classe XMLDAOFactory.java
package com.sdz.dao;
Je reprends le dernier exemple que nous avions réalisé, avec quelques modifications…
System.out.println("\n\t************************************");
System.out.println("\n\t***********************************");
Le pattern DAO vous permet de lier vos tables avec des objets Java.
Interagir avec des bases de données en encapsulant l'accès à celles-ci permet de faciliter la
migration vers une autre base en cas de besoin.
Afin d'être vraiment le plus souple possible, on peut laisser la création de nos DAO à une
factory codée par nos soins.
Pour gérer différents types de DAO (BDD, XML, fichiers…), on peut utiliser une factory qui
se chargera de créer nos factory de DAO.