L'essentiel de Java en une heure
Date de publication : 25/06/2007 , Date de mise à jour : 25/06/2007
Par
Romain Guy (Gfx) (home)
Depuis 10 ans maintenant, le phénomène Java ne cesse de prendre de l'ampleur.
Simple à appréhender, cette technologie recèle de richesses incroyables que
vous pourrez découvrir après avoir suivi cette initiation.
Cet article a été initialement écrit et publié dans le magazine Login avant JavaOne2005.
Les références temporelles sont donc à relativiser, et en ce mois de juin 2007 nous en sommes bel et bien à la version 6 de Java.
I. Introduction
II. Java 2 Standard Edition
III. Notre premier programme
IV. Les paquets de classes
V. WebHunter
VI. Une interface graphique pour WebHunter
VII. Distribuer l'application
VIII. Screenshots
IX. Téléchargements
I. Introduction
Java se différencie des autres technologies informatiques en cela que tout
le monde en a entendu parler. Les développeurs Web et les internautes ont
découvert Java par l'entremise des applets. Les sociétés connaissent toutes
l'existence de Java pour l'entreprise, appelé J2EE. De nombreux développeurs
travaillent tous les jours sur des applications bureautiques Java. Les adolescents
réclament enfin des téléphones mobiles compatibles Java pour télécharger des
jeux vidéo. Les exemples sont nombreux et prouvent l'omniprésence de cette
technologie, des robots de la NASA aux serveurs de commerce électronique en
passant par les cartes à puce et les jeux vidéo.
Le terme Java désigne une plate-forme de développement née en 1994 dans les
locaux de Sun Microsystems. Cette technologie était à l'époque destinée à la
production d'applications pour des terminaux multimédia. Java représente
aujourd'hui une nébuleuse au coeur de laquelle il est parfois difficile de se
retrouver. Ce nom est en effet utilisé pour désigner des systèmes très différents.
Sa signification dépend donc énormément du contexte. Le mot Java n'aura pas la
même signification pour une SSII que pour un opérateur téléphonique.
Ces différentes variantes reposent heureusement sur un socle commun, le
bytecode Java. Il s'agit d'un langage informatique comparable à celui des
assembleurs pour processeurs x86 ou PowerPC. Contrairement à ces derniers, le
bytecode Java n'a pas été créé pour être exécuté par un processeur physique
mais par une machine virtuelle, appelée Java Virtual Machine (JVM). Cette
propriété très particulière permet donc à un même programme Java, c'est-à-dire
à un bytecode, de s'exécuter sur tous systèmes d'exploitation, sur toutes
architectures matériels, à condition de disposer d'une JVM répondant aux
spécifications de Sun Microsystems. Les JVM peuvent d'une certaine manière se
voirent apparentées à des émulateurs, comparables à ceux utilisés pour faire
fonctionner les logiciels Amiga ou Apple II sur les PC modernes.
La nécessité d'employer une machine virtuelle pour exécuter le bytecode Java
explique pourquoi cette technologie est souvent dite interprétée. Le processeur
physique ne peut pas comprendre un programme Java sans une traduction préalable
par une JVM. Il est néanmoins difficile, en pratique, d'utiliser ce terme. Le
bytecode Java doit en effet être généré à l'aide d'un compilateur depuis un
langage source. Développer pour une plate-forme Java ressemble donc au
développement croisé. Les JVM modernes introduisent en outre des techniques
très avancées de compilation à la volée, à l'aide des compilateurs Just In Time
(JIT), dont les résultats sont comparables à ceux des compilateurs dits natifs,
produisant du code directement exécutable par le processeur physique.
Produire un programme Java nécessite donc de générer du bytecode, mais depuis
quel langage source ? Historiquement, les développeurs Java utilisent pour la
plupart un langage appelé... Java. A ce stade, il est important de ne pas confondre
le bytecode Java, le dénominateur commun des plates-formes Java, la machine
virtuelle Java, qui exécute le bytecode, et le langage Java, utilisé pour générer
le bytecode. Java est un langage orienté objet, fortement inspiré du C et du
Smalltalk. Il offre une syntaxe classique, très proche du C et du C++, et simple.
Vous pouvez néanmoins choisir parmi plusieurs centaines d'autres langages pour
générer du bytecode Java : LISP, Python, COBOL, BASIC, etc.
Un langage source n'est malheureusement pas suffisant pour développer des
applications. Les développeurs Java ont donc toujours recours à une plate-forme
destinée à un domaine particulier. Une plate-forme fournit, en sus d'une JVM, un
ensemble de bibliothèques et d'outils proposant des fonctionnalités nécessaires
pour réaliser des logiciels. L'univers Java contient aujourd'hui trois
plates-formes majeures : Java 2 Micro Edition (J2ME), Java 2 Standard Edition
(J2SE) et Java 2 Enterprise Edition (J2EE). La première se destine au
marché de l'informatique embarquée et se divise elle-même en plusieurs sous plates-formes
qui ciblent des machines de tailles différentes. J2ME est la plate-forme utilisée
pour développer des applications pour PDA et téléphones mobiles.
La seconde désigne la plate-forme de développement historique destinée aux
postes de travail. J2SE permet de créer des applications bureautiques, des jeux,
des applets, etc. Enfin, J2EE cible le marché des entreprises et des serveurs.
Cette plate-forme sert par exemple à mettre en place des portails d'entreprise
ou des boutiques de commerce électronique. Nous allons pour notre part nous
intéresser à J2SE, bien plus simple que J2EE mais aux fonctionnalités plus
étoffées que J2ME.

Le site http://java.sun.com/j2se propose de télécharger le JDK, le JRE et la documentation complète.
II. Java 2 Standard Edition
La plate-forme J2SE existe aujourd'hui en version 5.0, également numérotée
1.5. J2SE existe sous deux formes, une pour les développeurs, le J2SE
Development Kit (JDK), et une pour les utilisateurs, le Java Runtime Environment
(JRE). Le JDK comprend des outils de développement, comme le compilateur,
l'API J2SE, un ensemble de bibliothèques pour créer des applications, et un JRE.
Ce dernier regroupe quant à lui une JVM et les bibliothèques nécessaires pour
exécuter des applications. Toute personne souhaitant essayer vos programmes
Java devra donc installer un JRE. Le problème majeur provient des différentes
versions des JRE et JDK qui n'offrent pas tous les mêmes bibliothèques. Vous
devrez donc consulter attentivement la documentation pour savoir de quelle
version de J2SE dépend votre application.
La plate-forme J2SE peut être obtenue gratuitement auprès de Sun Microsystems sur
le site
http://java.sun.com. Vous y trouverez les dernières versions pour Solaris,
Linux et Windows ainsi que la documentation complète des outils et des
API.
N'oubliez pas de télécharger cette documentation qui n'est pas comprise dans
le JDK mais disponible dans une archive séparée. Vous ne pourrez pas développer
sans son aide. Les utilisateurs de MacOS X devront se rendre sur l'Apple
Developer Connection (
http://developer.apple.com). Contrairement à la plupart des distributions
Linux et à Windows, MacOS X installe un J2SE par défaut (la version 1.4.2
avec Panther).
Un JDK suffit pour développer et exécuter des applications Java puisqu'il
comprend un compilateur pour le langage Java et une machine virtuelle.
Son utilisation reste néanmoins cantonnée à la commande en ligne et aucun
environnement de développement (IDE) n'est fourni. De nombreux IDE et éditeurs
de code source pour Java sont disponibles gratuitement sur Internet. Lorsque
vous téléchargez un JDK depuis le site de Sun, vous pouvez choisir un paquetage
comprenant
NetBeans, un IDE Open Source. IBM propose également son environnement
de développement gratuit,
Eclipse, disponible sur le site
http://www.eclipse.org.
Ces deux outils écrits en Java fonctionnent sur la plate-forme J2SE et pourront
donc être installés sur la même machine que votre JDK. Ces IDE favorisent la
productivité mais masquent de nombreux concepts au programmeur. C'est pourquoi
nous allons aborder notre découverte de J2SE avec les outils standards du JDK,
en commande en ligne. Nous pourrons ainsi maîtriser les concepts de base du
développement Java. Vous pourrez également les appliquer aux plates-formes J2ME
et J2EE.
Une fois le JDK installé sur votre système, vous devez rendre ses outils
accessibles depuis n'importe quel dossier. Il suffit pour cela d'ajouter le
répertoire "bin/" de votre JDK à la variable d'environnement "PATH" puis de
tester l'installation en tapant la commande java, comme ici sous Linux :
$ export PATH=$PATH:/usr/java/jdk1.5.0/bin/
$ java -version
java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0-b64)
Java HotSpot(TM) Client VM (build 1.5.0-b64, mixed mode, sharing)
|
Les deux commandes que vous devez connaître pour créer votre premier programme
sont "java", pour invoquer la machine virtuelle, et "javac", pour compiler des
fichiers source Java en bytecode. Le répertoire "bin/" du JDK contient de nombreux
autres outils destinés à des opérations très spécifiques et rarement employés pour
le développement d'applications classiques.

Le site http://www.java.com permet de diffuser facilement le JRE auprès de vos utilisateurs.

La structure du JDK de J2SE 5.0.

Eclipse est l'IDE proposé gratuitement par IBM.

Sun Microstems vous propose de télécharger l'IDE Netbeans avec le JDK.

Les utilisateurs de MacOS X devront se rendre sur le site de l'Apple Developer Connection pour obtenir un SDK.

L'IDE Xcode de MacOS X est parfaitement adapté à la création d'applications Java.

Les fichiers Java sont compilés en bytecode qui est exécuté par la machine virtuelle.

Apple ne propose toujours pas de J2SE 5.0 pour son OS. Il faudra attendre l'arrivée de Tiger (c'est chose faite désormais).
III. Notre premier programme
Nous savons que Java est un langage orienté objet. Cela signifie que les
programmes sont implémentés dans des classes. Une classe peut être vue
comme un patron utilisé pour fabriquer des objets. Une classe définit donc
des attributs, qui sont des variables appartenant aux objets et
des méthodes, qui sont des fonctions appartenant aux objets. En tant que
telle, une classe ne sert à rien, vous devez l'instancier pour l'utiliser.
Un objet, ou instance, est alors créé et vous pourrez invoquer ses méthodes.
En pratique, le langage Java impose à chaque classe d'être décrite dans son
propre fichier portant l'extension ".java". Le nom du fichier doit correspondre
exactement au nom de la classe. Ainsi, la classe "Application" devra être définie
dans le fichier "Application.java". Lors de la compilation, chaque fichier ".java"
traité engendre la création d'un fichier ".class" contenant le bytecode de
la classe concernée. Dans notre exemple, nous obtiendrions donc
"Application.class". Le listing 1 propose le contenu de la classe
"Application" que vous pouvez compiler puis exécuter à l'aide des commandes
suivantes :
$ javac Application.java
$ java Application "La Classe Américaine"
|
En tapant la commande "ls" vous pourrez voir le fichier "Application.class".
Le listing 1 présente plusieurs particularités intéressantes. Tout
d'abord, il définit une méthode "main()" dite statique.
En programmation objet, une méthode statique est une méthode qui est commune à
toutes les instances d'une classe et à la classe elle-même. Une méthode statique
peut donc être invoquée sans instancier la classe. Puisque la machine virtuelle
ne peut pas savoir comment instancier la classe de votre application, il est
nécessaire de définir une méthode statique comme point d'entrée. Cette
caractéristique est très intéressante si l'on s'attarde sur la commande
"java Application" utilisée pour exécuter le programme. Vous noterez que
l'exécution d'un programme correspond en réalité à désigner la classe contenant
la méthode "main()". De fait, vous pouvez créer une méthode "main()"
par classe. Contrairement au C qui n'autorise qu'un seul point d'entrée pour
tout un programme, Java vous permet d'en avoir autant que vous le désirez. Cela
est particulièrement utile pour réaliser des tests de vos classes sans exécuter
la véritable application.
| Listing 1 |
|
| public class Application {
| public static void main(String[] args) {
| System.out.println("Ceci n'est pas un flim sur le cyclimse : " + args[0]);
| }
| }
|
|
|
|
Le second point intéressant de cet exemple réside dans l'utilisation de deux
autres classes, appelées "String" et "System" et appartenant à la
bibliothèque standard de Java. Ces deux classes, à l'instar de la nôtre, suivent
la convention des noms Java : toutes les classes commencent par une majuscule et
toutes les méthodes par une minuscule. La classe "String" est utilisée pour
définir un tableau de chaînes de caractères contenant les paramètres passés
depuis la ligne de commande. "System" donne quant à elle accès à de nombreuses
méthodes statiques relatives à l'environnement d'exécution. Nous accédons
ici à un attribut appelé "out", qui est une instance de la classe
"PrintStream", sur lequel nous invoquons la méthode "println()". Le
résultat de l'exécution est l'affichage du message, suivi du premier paramètre
de la commande en ligne, sur la sortie standard de votre système.
Le listing 2 est une variation de la même application mais respectant
l'approche objet de Java. Dans cet exemple, nous décrivons une véritable
classe, disposant d'un constructeur et d'une méthode. Les constructeurs
sont des méthodes un peu particulières puisqu'elles ne disposent
d'aucun type de retour et portent le même nom que la classe. Les constructeurs
sont appelés quand une instance de la classe est créée, comme dans le
"main()" de ce nouvel exemple. Le mot-clé "new" de Java sert à invoquer
un constructeur puis à récupérer un objet. La création d'objets en Java est
très simple puisque vous n'avez pas à vous soucier de leur destruction pour
libérer la mémoire. Les JVM doivent en effet implémenter un garbage
collector (GC ou ramasse-miettes) dont le rôle est de rechercher les
objets inutilisés dans l'application pour les détruire et récupérer de la
mémoire. Sachez en outre que les objets Java sont en fait des références vers
des objets. Ainsi, ces deux lignes ne copient pas a dans b mais créent une
référence b pointant vers le même objet que la référence a :
Application a = new Application();
Application b = a;
|
| Listing 2 |
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. |
|
| public class Application {
| private String movie;
|
| public Application(String movie) {
| this.movie = movie;
| }
|
| public void printMovie() {
| System.out.println("Ceci n'est pas un flim sur le cyclimse : " + movie);
| }
|
| public static void main(String[] args) {
| Application a = new Application(args[0]);
| a.printMovie();
| }
| }
|
|
|
|
|
L'exemple du listing 2 introduit en outre une variable spéciale appelée
"this". Dans un constructeur ou une méthode, cette variable agit comme une
référence à l'objet lui-même. Cela permet de contourner des conflits de noms
ainsi que le prouve notre constructeur : "this.movie" désigne l'attribut movie de
"Application" tandis que movie désigne le paramètre. Enfin, nous avons utilisé
les mots-clé "private" et "public" devant les définitions de la classe, de
l'attribut et des méthodes. Ces modificateurs déterminent la visibilité
des entités auxquelles ils sont appliqués. Par exemple, un attribut privé
ne peut pas être vu depuis une autre classe, au contraire d'une méthode
publique. Ces modificateurs sont employés pour masquer les données et
l'implémentation d'une classe aux autres classes. Il existe deux autres
modificateurs de visibilité, "protected" et le mot-clé vide. Un attribut
protected est privé, hormis pour les classes héritées. Le mot-clé vide rend
les attributs publics au sein d'un paquet. Une classe héritée
est définie en ajoutant "extends" ClasseParente à la suite de la déclaration :
public class Application2 extends Application {
public Application2() {
super("La Classe Américaine");
}
}
|
Lorsqu'une classe hérite d'une autre, son ancêtre ou parent est
appelée la super classe. Le mot-clé "super" est donc une référence à
cet ancêtre. Dans l'exemple ci-dessus, "super()" permet d'invoquer un
constructeur parent tandis que "super.movie" permet d'accéder à l'attribut
parent "movie". De manière générale, une classe héritée peut utiliser tous
les attributs, constructeurs et méthodes de la super classe à condition que
leur visibilité ne soit pas précisée, "public" ou "protected". L'héritage
permet donc d'ajouter des fonctionnalités à une classe. Une classe héritée
peut être utilisée avec le type de sa super classe. Ainsi nous pouvons écrire
"Application a = new Application2()". En effet, une "Application2" hérite
d'"Application" et peut donc être considérée comme une "Application".
L'inverse est possible en effectuant une opération de transtypage ou
de conversion explicite :
Application a = new Application2();
Application2 b = (Application2) a;
|
Nous indiquons ici que nous souhaitons transformer l'"Application" en
"Application2". Ce procédé est très utile car, en Java, toutes les classes
héritent automatiquement de la classe "java.lang.Object". Dans de nombreux
cas, comme dans les listes et piles, vos objets vous seront
renvoyés sous forme d'instances d'"Object" et vous devrez les convertir
explicitement dans le type désiré. Java propose en outre des classes
particulières appelées interfaces. Une interface ne peut pas posséder
d'attribut et ses méthodes ne peuvent pas contenir de code :
public interface Supprimable {
public void supprimer();
}
|
Une interface peut hériter d'une autre interface tandis qu'une classe doit
l'implémenter à l'aide du mot-clé "implements", qui apparaît nécessairement
après "extends" (si vous l'avez utilisé). Les interfaces ont ceci de particulier
que toutes leurs méthodes doivent apparaître dans la classe qui les implémente.
Ainsi, si nous créons une classe "Fichier" qui implémente "Supprimable", nous
devrons ajouter une méthode "supprimer()" dans "Fichier". L'instance d'une
classe implémentant une interface peut enfin être vue comme une instance
de l'interface :
Supprimable f = new Fichier();
|
Le mécanisme des interfaces permet donc de forcer des classes à implémenter
une API sans les restreindre à une implémentation particulière. Les
interfaces sont énormément utilisées dans Java, notamment pour la
réalisation d'interfaces graphiques.

Notre première application Java est un succès !
IV. Les paquets de classes
J2SE offre une quantité impressionnante de classes pour vous aider à réaliser
vos applications. Elles sont classées par thèmes dans des paquets, ou packages
en anglais. Un paquet est un répertoire regroupant des sous paquets et des
classes. Dans notre première application, nous avons utilisé les classes
"String" et "System" appartenant au paquet "java.lang", importé par défaut dans
toute classe Java. Le paquet "java.lang" désigne le paquet "lang", contenu dans
le paquet "java". Sur le disque dur, les classes de ce paquet se trouvent dans
un répertoire "java/lang". La notation "." est employée par Java pour opérer
une distinction entre les paquets et leur représentation physique. Ainsi,
une application "Clock" contenu dans le paquet "com.madhatter" se trouvera dans
le fichier "com/madhatter/Clock.class" et devra être exécutée ainsi :
$ java com.madhatter.Clock
|
Par défaut, une classe ne connaît que les classes de son paquet et de
"java.lang". Pour en utiliser d'autres, il est nécessaire de les importer à
l'aide du mot-clé "import". Les directives d'import se placent toujours au
début du fichier Java, avant la déclaration de la classe. J2SE propose par
exemple une classe "Date" pour représenter une date et une heure. Elle se
trouve dans le paquet "java.util" et peut-être utilisée en suivant l'exemple
du listing 3. La documentation des API de J2SE propose une liste exhaustive
des paquets, de leurs classes et des méthodes de ces dernières. Cette
documentation est non seulement complète mais également très détaillée.
N'hésitez pas à vous y référer chaque fois que vous devez utiliser une classe
standard.
| Listing 3 |
|
| import java.util.Date;
|
| public class Clock {
| public static void main(String[] args) {
| System.out.println(new Date());
| }
| }
|
|
|
|
Lorsque vous devez importer de nombreuses classes depuis un même paquet,
vous pouvez utiliser l'étoile pour désigner tout son contenu. Ainsi, import
"java.util.*" importera toutes les classes du paquet "java.util". Sachez enfin
qu'il est possible de désigner une classe par son nom complet sans importer
son paquet. L'opération "new Date()" du listing 3 est donc équivalente à
new "java.util.Date()" sans directive import.
Pour ranger une classe dans un paquet vous devez déplacer le fichier Java
dans le répertoire adéquat. Notre classe "com.madhatter.Clock" ira donc dans
le dossier "com/madhatter". Vous devez également ajouter une ligne au fichier
source. Cette ligne doit impérativement être la première du fichier :
La prochaine application que nous allons à réaliser permettra de télécharger
des fichiers sur des serveurs web. Nous devons pour cela apprendre à nous
connecter sur un serveur distant et à écrire des fichiers sur le disque dur.
Toutes les primitives d'entrées/sorties, la gestion des fichiers notamment,
se trouve dans le paquet "java.io" (pour Input/Output). Le listing 4
présente un petit programme qui écrit une chaîne de caractères dans un fichier.
En Java toutes les entrées/sorties sont représentées par des flux. Les
flux d'entrées sont des "InputStream" ou des "Reader" et les flux de sortie sont des
"OutputStream" ou des "Writer". Les classes de type "Stream" sont orientées octets
tandis que les autres sont prévues pour manipuler des caractères. Notre
exemple utilise donc un "Writer" pour enregistrer une chaîne de caractères
dans un fichier.
| Listing 4 |
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. |
|
| import java.io.*;
|
| public class Writer {
| public static void main(String[] args) {
| try {
| PrintWriter in = new PrintWriter(new File("test.txt"));
| in.println("Input/Output");
| in.close();
| } catch (FileNotFoundException e) {
| e.printStackTrace();
| }
| }
| }
|
|
|
|
Cet exemple introduit une construction très importante du langage Java, la
clause "try/catch". Pour forcer les développeurs à traiter les erreurs
pouvant survenir au cours de l'exécution d'un programme, les auteurs de Java
ont mis en place un système de levée et de traitement d'exception.
Lorsqu'une méthode rencontre une erreur, elle peut lancer une exception,
représentée par l'instance d'une classe comme "FileNotFoundException". L'appelant
est alors obligé de l'intercepter avec un bloc "try/catch". Le code susceptible de
lancer une exception se trouve dans la section "try". Si une erreur survient,
l'exécution est interrompue et la JVM saute directement à la section "catch".
Dans le listing 4, nous invoquons la méthode "printStackTrace()" de
l'exception qui affiche dans la console le message d'erreur ainsi que le numéro
de la ligne à laquelle elle est survenue. Toutes les exceptions que peuvent lancer
les méthodes sont décrites dans la documentation de la bibliothèque standard
et sont signalées par le compilateur si vous ne les interceptez pas. Certaines
méthodes peuvent lancer plusieurs exceptions différentes selon l'erreur
rencontrée. Ce système contraignant vous offre l'opportunité d'écrire des
programmes propres capables de se rétablir en cas de problème.
V. WebHunter
L'application "WebHunter" décrite dans le listing 5 est un programme
exécutable depuis la commande en ligne qui permet de télécharger un fichier
depuis un serveur web. L'adresse du fichier doit être fournie comme premier
paramètre de la ligne de commande et le résultat est enregistré dans le répertoire
courant. Voici un exemple d'exécution :
$ javac com/loginmag/webhunter/WebHunter.java
$ java com.loginmag.webhunter.WebHunter http://jext.free.fr/macos7.jpg
|
| Listing 5 |
 1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. | 36. | 37. | 38. | 39. | 40. | 41. | 42. | 43. | 44. | 45. | 46. | 47. | 48. | 49. | 50. | 51. | 52. | 53. | 54. | 55. | 56. | 57. | 58. |
|
| package com.loginmag.webhunter;
|
| import java.io.*;
| import java.net.*;
|
| public class WebHunter {
|
| public WebHunter() {
| }
|
| public boolean download(String source) {
| try {
| URL url = new URL(source);
|
| BufferedInputStream in = new BufferedInputStream(url.openStream(), 1024);
| String name = new File(url.getFile()).getName();
| BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(name));
|
| int read = -1;
| byte[] data = new byte[1024];
|
| read = in.read(data, 0, 1024);
| while (read != -1) {
| System.out.print("#");
| out.write(data, 0, read);
| read = in.read(data, 0, 1024);
| }
| System.out.println();
|
| in.close();
| out.close();
| } catch (MalformedURLException murle) {
| return false;
| } catch (IOException ioe) {
| return false;
| }
|
| return true;
| }
|
| public static void main(String[] args) {
| if (args.length < 1) {
| System.out.println("java WebHunter <url to download>");
| System.exit(1);
| }
|
| WebHunter hunter = new WebHunter();
| boolean downloaded = hunter.download(args[0]);
|
| if (downloaded) {
| System.out.println("Download is a success!");
| } else {
| System.out.println("Download failed.");
| }
| }
| }
|
|
|
|
Cet exemple utilise deux paquets, "java.io" que nous connaissons déjà, et
"java.net", destiné au réseau. Ce dernier contient les classes nécessaires
pour manipuler des sockets, des adresses IPv4 et IPv6, des URL, etc.
Notre programme se trouve dans la classe "WebHunter" qui dispose d'un
constructeur vide et d'une unique méthode publique. Cette dernière
s'appelle "download()" et renvoie un booléen pour indiquer le succès ou
l'échec du téléchargement.
Le point d'entrée de l'application commence par vérifier le nombre
d'arguments passés en ligne de commande. S'il n'y en a aucun, un message
d'information est affiché et nous forçons le programme à s'interrompre.
Une instance de "WebHunter" est ensuite créée pour invoquer la méthode
"download()" à laquelle nous passons en paramètre l'adresse du fichier à
télécharger. Un message de succès ou d'échec est affiché selon le résultat.
Tout le travail de téléchargement et d'écriture du fichier est donc réalisé
dans "download()".
Le fonctionnement global de cette méthode est très simple puisqu'il consiste
à créer une URL, à ouvrir un flux de lecture vers cette URL, à ouvrir un
flux d'écriture sur le disque dur, puis à copier les octets du flux de
lecture dans celui d'écriture. Ces opérations sont sujettes à des levées
d'exceptions, c'est pourquoi nous protégeons le code à l'aide d'une
instruction "try/catch". En cas d'erreur, nous renvoyons la valeur "false"
pour indiquer une erreur à l'appelant. Par exemple, la création de l'objet
URL fichier peut générer une "MalformedURLException" si l'adresse du fichier
fournie ne respecte pas les normes des URL. Un objet URL Java permet
d'accéder à certaines méthodes intéressantes, en particulier "openStream()"
qui crée un flux de lecture vers la ressource pointée par l'URL.
Les deux flux utilisés dans "download()" exploitent des
tampons pour
améliorer les performances. Ainsi, notre BufferedInputStream déclare un tampon
de 1024 octets. Cela signifie que, lors d'une demande de lecture, le flux
chargera 1024 octets à la fois. Outre la taille du tampon, le constructeur
du flux attend en paramètre un flux de lecture source. La plupart des flux
Java fonctionnent sur le principe des
décorateurs : une classe en encapsule
une autre pour lui ajouter des fonctionnalités. Dans notre exemple, le
"BufferedInputStream" encapsule le flux de lecture de l'URL pour lui ajouter un
tampon. Le flux d'écriture dans un fichier local est créé de la même manière, en
encapsulant un "FileOutputStream" dans un "BufferedOutputStream". Le nom du fichier
utilisé par le "FileOutputStream" est obtenu en créant un objet "File". Une URL
permet certes de récupérer le nom du fichier, mais seulement le
nom absolu. Par
exemple, pour l'URL
http://www.progx.org/users/Gfx/crossmax.jpg, le nom du fichier obtenu par
l'appel de la méthode "getFile()" est "/users/Gfx/crossmax.jpg". Pour ne récupérer
que la partie qui nous intéresse, "crossmax.jpg", il suffit de créer un objet
"File", de lui passer le nom absolu et d'invoquer sa méthode "getName()".
Pour copier le fichier, nous devons à présent lire les données dans le flux
d'entrée "in" puis les écrire dans le flux "out". Pour ce faire, nous déclarons
un tableau de 1024 octets, c'est-à-dire une variable de type "byte[]" en Java.
La méthode "read()" des "InputStream" attend en entrée un tableau de byte, un
indice de départ dans ce tableau et le nombre d'octets à lire. Puisque nous ne
connaissons pas la taille totale du fichier à télécharger, nous allons essayer
de remplir tout le tableau jusqu'à atteindre la fin du flux. Le problème se pose
lorsqu'il ne reste plus assez d'octets dans le flux pour le remplir. La méthode
"read()" résout ce problème en renvoyant le nombre d'octets réellement lus
depuis le flux. Nous savons alors combien de cases du tableau ont été modifiées
par la nouvelle lecture. Lorsque la fin du flux a été atteinte, "read()" renvoie
la valeur -1. La méthode d'écriture dans le flux de sortie, "write()", fonctionne
exactement de la même manière que "read()". Toutefois, nous ne demandons pas
d'utiliser les 1024 octets du tableau, comme nous l'avons fait dans la lecture,
mais seulement les "read" premiers. Après avoir atteint la fin du fichier,
nous pouvons fermer les deux flux en invoquant les méthodes "close()".
Toutes les opérations relatives aux entrées/sorties, notamment les appels à
"read()" et "write()", sont susceptibles d'émettre des exceptions de type
"IOException" que nous traitons comme les "MalformedURLException". Notre
solution, bien qu'utile, manque de précision. L'appelant sait simplement si
une erreur est survenue ou non. Il n'a aucun moyen de savoir ce qui a provoqué
l'erreur : une URL mal formée, un serveur injoignable, etc. Nous pouvons y
remédier en apportant quelques changements mineurs à notre méthode, présentés
dans le listing 6.
| Listing 6 |
|
| public void download(URL source, File target) throws IOException {
| BufferedInputStream in = new BufferedInputStream(source.openStream(), 1024);
| BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(target));
|
| }
|
|
|
|
La nouvelle version de "download()" ne renvoie plus de valeur mais précise qu'une
exception de type "IOException" peut être émise à l'aide du mot-clé "throws". Vous
pouvez préciser plusieurs exceptions en les séparant par une virgule. Une exception
peut être lancée soit implicitement en omettant le "try/catch" autour d'une méthode
pouvant en générer une, soit explicitement à l'aide du mot-clé "throw". Ainsi, pour
lancer une exception de base, vous pourrez écrire "throw new Exception("Raison de
l'erreur")". Les développeurs Java ont l'habitude de transmettre les exceptions,
comme nous le faisons ici avec la déclaration "throws", plutôt que de renvoyer des
codes d'erreur. L'appelant est alors libre de l'intercepter ou de la transmettre
à son tour. La nouvelle méthode "download()" attend également en paramètre l'URL
source et le fichier cible dans lequel copier le téléchargement. L'appelant doit
alors construire lui-même l'URL peut donc traiter les erreurs de manière plus fine
qu'auparavant. La présence du second paramètre permet à l'appelant de choisir le
chemin du fichier. Il n'est donc plus restreint au répertoire courant, et le nom
peut différer de celui du fichier source. Nous avons donc transformé la classe
"WebHunter" pour la rendre aisément réutilisable.

L'application WebHunter peut télécharger des fichiers distants depuis la commande en ligne.

La documentation des API de J2SE est complète et très détaillée.
VI. Une interface graphique pour WebHunter
"WebHunter" fonctionne parfaitement en ligne de commande mais n'est pas très
pratique pour lancer plusieurs téléchargements à la fois. Pour l'améliorer, nous
allons lui ajouter une interface graphique qui sera capable de lancer
plusieurs téléchargements concurrents. Contrairement à d'autres langages
et plateformes, le choix d'une boîte à outils graphique est très restreint
en Java puisque seules 3 sont disponibles. La première, Abstract Windowing
Toolkit (AWT) est la plus ancienne et est apparue avec la première version
du JDK de Sun Microsystems. AWT est une couche d'émulation pour les
composants natifs du gestionnaire de fenêtre du système d'exploitation. Son
rôle est de recréer le plus fidèlement possible l'apparence des applications
standards de l'OS hôte. AWT n'a malheureusement jamais réussi à atteindre
ce but. Elle a donc rapidement été abandonnée au profit de Swing, une
seconde boîte à outils présente dans le J2SE. Swing n'utilise absolument pas
le système hôte pour s'occuper du rendu des composants graphiques, tout est
intégralement programmé et exécuté en Java. Fonctionnellement très riche, très
souple à utiliser, parfois un peu difficile à comprendre, Swing se veut le choix
de la plupart des développeurs depuis son introduction officielle dans le JDK 1.2.
Le défaut majeur de Swing réside dans son apparence, très différente des
applications standard de l'OS. Elle gère néanmoins un système de "look and feel",
qui sont des thèmes graphiques et fonctionnels pour modifier l'apparence et le
comportement des applications. Ainsi, en changeant une seule ligne de code vous
pourrez donner à votre logiciel l'apparence d'une application GTK+, Aqua, Windows
XP ou autre. Voici les lignes à ajouter dans la méthode "main()" pour prendre
l'apparence du système :
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) { }
|
Sachez que cette boîte à outil repose en partie sur AWT pour les polices de
caractères, les politiques de positionnement, etc. Swing n'est malheureusement
pas appréciée de tous les développeurs du fait de sa complexité et des faiblesses
des looks and feels par défaut. C'est pourquoi IBM a créé le Standard Windowing
Toolkit (SWT) pour développer Eclipse. SWT reprend le concept d'AWT en reposant
en partie sur des composants graphiques natifs. D'une plateforme à l'autre, SWT
émulera les composants graphiques qui ne sont pas fournis par l'hôte. En pratique,
les interfaces SWT sont un peu plus faciles à développer que celles en Swing et
leur apparence par défaut correspond parfaitement à celle de l'environnement
d'exécution.
Puisque SWT n'est pas fourni avec J2SE, nous allons développer notre interface
en Swing. La nouvelle version de "WebHunter" se trouve dans le paquet
"com.loginmag.webhunter" et comprend 5 classes : "DownloaderThread", qui représente
l'exécution d'un thread chargé d'un téléchargement, "DownloadProgressEvent"
et "DownloadProgressListener", deux classes destinées à la gestion des événements
de téléchargement, "WebHunter", que vous connaissez, et "WebHunterUI", contenant
l'interface graphique et le point d'entrée du programme.
La classe "WebHunter" a été un peu modifiée pour émettre des événements. En Java,
un événement est représenté par une classe dont le nom se finit par "Event"
et est reçu par un écouteur, une classe implémentant une interface dont le
nom termine par "Listener". Ainsi, après chaque lecture sur le flux d'entrée, la
méthode "download()" invoque la méthode "downloadProgressed()" de ses écouteurs en
leur passant en paramètre une instance de "DownloadProgressEvent". Cette dernière
contient une seule méthode qui permet de savoir combien d'octets ont effectivement
été lus. En implémentant "DownloadProgressListener" dans notre interface graphique,
nous pourrons donc connaître l'état d'avancement du téléchargement sans insérer
de code dans "download()". Nous nous assurons ainsi que la classe "WebHunter" sera
réutilisable facilement par d'autres applications. Le code de gestion des
événements se trouve dans le listing 7. Implémenter un système d'écoute
s'avère donc particulièrement simple si vous employez les collections
d'objets fournies par le paquet "java.util". Les collections permettent de
stocker plusieurs objets selon différentes méthodes : avec des files, des piles,
des listes chaînes, des vecteurs, des dictionnaires, etc. Chaque collection
implémente l'interface "List" ou "Map". Dans notre exemple, nous utilisons un
simple vecteur par l'entremise de la classe "ArrayList".
| Listing 7 |
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. |
|
| import java.util.*;
|
| private List listeners;
|
| public WebHunter() {
| this.listeners = new ArrayList();
| }
|
| public void addDownloadProgressListener(DownloadProgressListener listener) {
| this.listeners.add(listener);
| }
|
| public void removeDownloadProgressListener(DownloadProgressListener listener) {
| this.listeners.remove(listener);
| }
|
| private void fireDownloadProgressEvent(int bytesRead) {
| DownloadProgressEvent event = new DownloadProgressEvent(bytesRead);
| Iterator it = listeners.iterator();
| while (it.hasNext()) {
| ((DownloadProgressListener) it.next()).downloadProgressed(event);
| }
| }
|
|
|
|
L'interface graphique se trouve dans la classe "WebHunterUI" qui hérite de la
classe "javax.swing.JFrame". En Swing, tous les composants graphiques ont un nom
préfixé par la lettre majuscule "J". Une "JFrame" désigne une fenêtre dans
laquelle nous pouvons ajouter d'autres composants. Son code un peu simplifié se
trouve dans le listing 8. En Swing tous les composants sont installés
dans des conteneurs, dont celui par défaut est "getContentPane()" pour
une "JFrame". Chaque conteneur est associé à une politique de placement des
composants. Par exemple, dans la méthode "build()", une politique "BorderLayout"
est associée au conteneur par défaut. Elle permet de placer les composants
sur les bords ou au centre d'un conteneur, au nord ou au sud par exemple. Ces
politiques, dont le nom finit par "Layout", se trouvent à la fois dans le
paquet "java.awt" et dans "javax.swing". Vous pourrez en utiliser plusieurs
suivant vos besoins. Nous pouvons ainsi citer le "GridLayout" pour placer
les composants en grille. Lorsqu'une politique a été créée, il vous suffit
d'ajouter les composants au conteneur par l'intermédiaire de la méthode "add()".
Selon la politique, vous devrez fournir une contrainte en paramètre. Ainsi, un
"GridLayout" ne demande pas de contrainte tandis que le "BorderLayout" demande
la position, "BorderLayout.NORTH" par exemple. Notre fenêtre se compose donc
du conteneur par défaut, contenant lui-même deux conteneurs. Au nord se trouve
celui construit par la méthode "buildDownloadPane()" pour afficher un champ
texte et un bouton. Au centre se trouve un conteneur de type "Box", qui permet
d'orienter des composants verticalement, comme ici, ou horizontalement.
Chaque fois que l'utilisateur lancera un téléchargement, nous ajouterons des
informations dans la "Box". La méthode "buildDownloadPane()" utilise trois
composants Swing. Le premier est un "JLabel" qui sert à afficher du texte. Le
deuxième, "JTextField", représente un champ de saisie tandis que le troisième
est un simple bouton. Pour lancer un téléchargement quand l'utilisateur clique
sur le bouton, nous devons lui ajouter un écouteur, suivant le même principe
que pour le "DownloadProgressListener". C'est pourquoi "WebHunterUI" implémente
l'interface "ActionListener" et définit la méthode "actionPerformed()". Swing
vous permet ainsi d'écouter des dizaines d'événements pour le clavier, la
souris, le changement des propriétés d'un composant, etc. Le code d'
"actionPerformed()" n'a pas été représenté ici pour des raisons de place mais
vous le trouverez dans le fichier "WebHunterUI.java". Son rôle consiste à
créer une URL à partir du texte saisi dans le "JTextField" puis à ouvrir
une boîte de sauvegarde de fichier pour demander à l'utilisateur de choisir
l'emplacement d'enregistrement du téléchargement. Le programme crée ensuite
un nouveau conteneur dans lequel sont disposés l'adresse du fichier source,
le chemin du fichier cible et une barre de progression. L'ensemble est ajouté
dans la "Box" puis la méthode "pack()" de "JFrame" est appelée. Cette méthode
est très importante puisqu'elle permet de dimensionner tous les composants et
la fenêtre pour leur donner à tous la taille la plus adaptée à leur contenu.
Le téléchargement est initié avec les deux instructions suivantes :
DownloaderThread thread = new DownloaderThread(source, target, bar);
thread.start();
|
| Listing 8 |
 1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. | 36. | 37. | 38. | 39. | 40. | 41. | 42. | 43. | 44. | 45. | 46. | 47. | 48. | 49. | 50. | 51. | 52. | 53. | 54. | 55. | 56. |
|
| package com.loginmag.webhunter;
|
| import java.io.*;
| import java.net.*;
|
| import java.awt.*;
| import java.awt.event.*;
| import javax.swing.*;
| import javax.swing.border.*;
|
| public class WebHunterUI extends JFrame implements ActionListener {
| private Box centerPane;
| private JTextField sourceFile;
|
| public WebHunterUI() {
| super("WebHunter");
|
| build();
|
| pack();
| setResizable(false);
| setLocationRelativeTo(null);
| setDefaultCloseOperation(EXIT_ON_CLOSE);
| setVisible(true);
| }
|
| public void actionPerformed(ActionEvent evt) {
|
| }
|
| private void build() {
| getContentPane().setLayout(new BorderLayout());
| getContentPane().add(BorderLayout.NORTH, buildDownloadPane());
| getContentPane().add(BorderLayout.CENTER, centerPane = Box.createVerticalBox());
| }
|
| private Container buildDownloadPane() {
| JPanel panel = new JPanel();
| JButton button;
|
| panel.add(new JLabel("File to download: "));
| panel.add(sourceFile = new JTextField(15));
| panel.add(button = new JButton("Download..."));
|
| sourceFile.setText("http://jext.free.fr/macos7.jpg");
| button.addActionListener(this);
|
| return panel;
| }
|
| public static void main(String[] args) {
| new WebHunterUI();
| }
| }
|
|
|
|
La première ligne crée une instance de "DownloaderThread" en lui donnant en
paramètre l'URL source, le fichier cible et la barre de progression. Cette
instance représente un thread pour permettre d'exécuter les téléchargements
parallèlement. Le téléchargement commence après l'appel à "start()". Le code
de la classe se trouve dans le listing 9, dans lequel ne figure pas la
déclaration des paquets, des attributs et du constructeur. La classe hérite
de "java.lang.Thread" pour hériter des capacités des Thread Java. Lorsque la
méthode "start()" d'un thread est appelée, sa méthode "run()" est exécutée.
Cette dernière effectue trois opérations principales. La première consiste
à déterminer la taille totale du fichier à télécharger. Quand l'opération
est réussie, la valeur maximale de la barre de progression est modifiée pour
être égale à cette taille. En cas d'exception, la barre de progression est
passée en mode indéterminé. L'étape suivante consiste à créer une instance
de "WebHunter" et de lui ajouter un écouteur d'événement pour mettre à jour
la barre de progression. Enfin, le téléchargement est lancé à l'aide de la
méthode "download()". Les événements de téléchargements sont reçus dans
"downloadProgressed()" qui ajoute le nombre d'octets lus à la valeur courante
de la barre de progression. De cette manière l'utilisateur peut connaître
précisément l'état d'avancement de ses téléchargements. Le code source de
cette interface graphique fait appel à de nombreuses méthodes de Swing que
nous n'avons pas décrites. Il est recommandé de se référer à la documentation
des API pour en comprendre le fonctionnement et les paramètres.
| Listing 9 |
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. |
|
| public class DownloaderThread extends Thread implements DownloadProgressListener {
|
|
| public void run() {
| try {
| URLConnection connection = source.openConnection();
| int maxSize = connection.
|
|
|