L'essentiel de Java en une heure

Romain Guy (Gfx)

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. ♪

- Titre : L'essentiel de Java en une heure

- Auteur : Romain Guy (Gfx)

- Parution : 25 juin 2007

- Mise à jour : 25 juin 2007

- Licence : Cette création est mise à disposition sous un contrat Creative Commons (Paternité - Partage des Conditions Initiales à l'Identique).

Sommaire

- 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 plateforme 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 cœur 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érielles, à condition de disposer d'une JVM répondant aux spécifications de Sun Microsystems. Les JVM peuvent d'une certaine manière se voir 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 plateforme 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. À ce stade, il est important de ne pas confondre le bytecode Java, le dénominateur commun des plateformes 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 plateforme destinée à un domaine particulier. Une plateforme 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 plateformes 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 plateformes qui ciblent des machines de tailles différentes. J2ME est la plateforme utilisée pour développer des applications pour PDA et téléphones mobiles. La seconde désigne la plateforme 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 plateforme 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 plateforme 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 plateforme 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 plateforme 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 plateformes 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

1. public class Application {

2. public static void main(String[] args) {

3. System.out.println("Ceci n'est pas un film sur le cyclisme : " + args[0]);

4. }

5. }

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. public class Application {

2. private String movie;

4. public Application(String movie) {

5. this.movie = movie;

6. }

8. public void printMovie() {

9. System.out.println("Ceci n'est pas un film sur le cyclisme : " + movie);

10. }

12. public static void main(String[] args) {

13. Application a = new Application(args[0]);

14. a.printMovie();

15. }

16. }

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é 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 » contenue 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

1. import java.util.Date;

3. public class Clock {

4. public static void main(String[] args) {

5. System.out.println(new Date());

6. }

7. }

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 :

package com.madhatter;

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 trouvent 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. import java.io.*;

3. public class Writer {

4. public static void main(String[] args) {

5. try {

6. PrintWriter in = new PrintWriter(new File("test.txt"));

7. in.println("Input/Output");

8. in.close();

9. } catch (FileNotFoundException e) {

10. e.printStackTrace();

11. }

12. }

13. }

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. package com.loginmag.webhunter;

3. import java.io.*;

4. import java.net.*;

6. public class WebHunter {

8. public WebHunter() {

9. }

11. public boolean download(String source) {

12. try {

13. URL url = new URL(source);

15. BufferedInputStream  in  = new BufferedInputStream(url.openStream(), 1024);

16. String name = new File(url.getFile()).getName();

17. BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(name));

19. int read = -1;

20. byte[] data = new byte[1024];

22. read = in.read(data, 0, 1024);

23. while (read != -1) {

24. System.out.print("#");

25. out.write(data, 0, read);

26. read = in.read(data, 0, 1024);

27. }

28. System.out.println();

30. in.close();

31. out.close();

32. } catch (MalformedURLException murle) {

33. return false;

34. } catch (IOException ioe) {

35. return false;

36. }

38. return true;

39. }

41. public static void main(String[] args) {

42. if (args.length < 1) {

43. System.out.println("java WebHunter <url to download>");

44. System.exit(1);

45. }

47. WebHunter hunter = new WebHunter();

48. boolean downloaded = hunter.download(args[0]);

50. if (downloaded) {

51. System.out.println("Download is a success!");

52. } else {

53. System.out.println("Download failed.");

54. }

55. }

56. }

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 bytes, 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

1. public void download(URL source, File target) throws IOException {

2. BufferedInputStream  in  = new BufferedInputStream(source.openStream(), 1024);

3. BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(target));

4. // le reste du code sans les return

5. }

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 à outils 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. import java.util.*;

2. // ...

3. private List listeners;

5. public WebHunter() {

6. this.listeners = new ArrayList();

7. }

9. public void addDownloadProgressListener(DownloadProgressListener listener) {

10. this.listeners.add(listener);

11. }

13. public void removeDownloadProgressListener(DownloadProgressListener listener) {

14. this.listeners.remove(listener);

15. }

17. private void fireDownloadProgressEvent(int bytesRead) {

18. DownloadProgressEvent event = new DownloadProgressEvent(bytesRead);

19. Iterator it = listeners.iterator();

20. while (it.hasNext()) {

21. ((DownloadProgressListener) it.next()).downloadProgressed(event);

22. }

23. }

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. package com.loginmag.webhunter;

3. import java.io.*;

4. import java.net.*;

6. import java.awt.*;

7. import java.awt.event.*;

8. import javax.swing.*;

9. import javax.swing.border.*;

11. public class WebHunterUI extends JFrame implements ActionListener {

12. private Box centerPane;

13. private JTextField sourceFile;

15. public WebHunterUI() {

16. super("WebHunter");

18. build();

20. pack();

21. setResizable(false);

22. setLocationRelativeTo(null);

23. setDefaultCloseOperation(EXIT_ON_CLOSE);

24. setVisible(true);

25. }

27. public void actionPerformed(ActionEvent evt) {

28. // ...

29. }

31. private void build() {

32. getContentPane().setLayout(new BorderLayout());

33. getContentPane().add(BorderLayout.NORTH,  buildDownloadPane());

34. getContentPane().add(BorderLayout.CENTER, centerPane = Box.createVerticalBox());

35. }

37. private Container buildDownloadPane() {

38. JPanel panel = new JPanel();

39. JButton button;

41. panel.add(new JLabel("File to download: "));

42. panel.add(sourceFile = new JTextField(15));

43. panel.add(button = new JButton("Download..."));

45. sourceFile.setText("http://jext.free.fr/macos7.jpg");

46. button.addActionListener(this);

48. return panel;

49. }

51. public static void main(String[] args) {

52. new WebHunterUI();

53. }

54. }

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. public class DownloaderThread extends Thread implements DownloadProgressListener {

2. // ...

4. public void run() {

5. try {

6. URLConnection connection = source.openConnection();

7. int maxSize = connection.getContentLength();

8. bar.setMaximum(maxSize);

9. } catch (IOException ioe) {

10. bar.setIndeterminate(true);

11. }

13. WebHunter hunter = new WebHunter();

14. hunter.addDownloadProgressListener(this);

16. try {

17. hunter.download(source, target);

18. } catch (IOException ioe) {

19. JOptionPane.showMessageDialog(bar, "Une erreur inconnue est survenue.", "Erreur de téléchargement", JOptionPane.ERROR_MESSAGE);

20. }

21. }

23. public void downloadProgressed(DownloadProgressEvent event) {

24. bar.setValue(bar.getValue() + event.getBytesRead());

25. }

26. }

L'interface graphique Swing de WebHunter apparaît comme une application MacOS X normale.

La version graphique de WebHunter permet de choisir le chemin et le nom du fichier de destination.

Suivant le modèle des dialogues de copie de fichier de MacOS X, WebHunter empile les téléchargements.

La version Windows est moins jolie par défaut, car elle utilise le look and feel Metal de Swing.

Sous Linux, WebHunter propose exactement la même interface graphique sans changer une ligne de code.

Les applications Swing peuvent prendre l'apparence des applications natives comme ici sous Windows XP.

Les dernières versions de J2SE proposent un look and feel GTK+.

VII - Distribuer l'application

La compilation de « WebHunter » produit plusieurs fichiers « .class ». Il est donc difficile de distribuer l'application telle quelle. Le problème est d'autant plus important que le nombre de classes est grand. Le JDK offre pour cela un outil appelé jar, pour « java archive », dont le rôle est de compresser toutes les classes et ressources d'une application dans un seul fichier « .jar ». Ces fichiers sont au format ZIP et peuvent donc être ouverts par la plupart des applications d'archivage. Voici comment créer le JAR de WebHunter :

$ jar cf WebHunter.jar com/loginmag/webhunter/*.class

Le paramètre « c » indique la création d'une nouvelle archive tandis que « f » sert à préciser le nom du fichier archive. Après exécution de cette commande, vous obtenez le fichier « WebHunter.jar » dans le répertoire courant. Le problème qui se pose à présent est l'exécution de l'application depuis un JAR. Les commandes « java » et « javac » proposent toutes les deux une option « -classpath » qui permet d'indiquer où se trouvent les classes utilisées par l'application. Vous pouvez pointer vers un répertoire ou un fichier JAR, comme ici :

$ java -classpath WebHunter.jar com.loginmag.webhunter.WebHunter

La notion de classpath est indispensable si vous désirez exploiter des bibliothèques qui ne sont pas fournies en standard dans J2SE. Par exemple, pour compiler une application utilisant l'interpréteur Jython, voici la ligne de commande à saisir :

$ javac -classpath jython.jar:. *.java

Les entrées du classpath sont séparées par deux points sous les systèmes Unix (Linux, Solaris, MacOS X) et par un point-virgule sous Windows. Cela vous permet de scinder votre application en modules fonctionnels. Nous aurions par exemple pu créer le fichier « webhunter-engine.jar » avec la classe « WebHunter » et l'archive « webhunter-ui.jar » avec toutes les autres classes. En spécifiant le classpath correctement le résultat aurait être le même.

Cette introduction à la plateforme et au langage Java nous a permis de nous familiariser avec les concepts les plus importants. Le JDK de J2SE est très riche et propose des milliers de classes qui couvrent une grande partie des besoins des développeurs. Outre la documentation officielle, très bien structurée, il est recommandé d'utiliser un environnement de développement intégré comme Eclipse. Ces outils sont en effet capables de proposer automatiquement la complétion des classes et des méthodes. Puisqu'il est impossible de retenir par cœur le nom de toutes les méthodes dont vous aurez besoin, un IDE vous fera gagner beaucoup de temps. L'affichage de la documentation au sein même de l'éditeur de code source est également un plus indéniable. Java est une plateforme facile d'accès, disponible sur de nombreux OS et disposant de nombreux outils puissants et gratuits, il serait donc dommage de passer à côté.

VIII - Screenshots

La gestion des entrées/sorties en Java est un domaine très vaste.

Les threads peuvent entraîner de nombreuses complications, il est donc conseillé de se documenter abondamment à leur sujet.

Eclipse est un IDE puissant, mais parfois difficile à apprendre.

Pour découvrir ce que Java a vous offrir n'hésitez pas à consulter des sites spécialisés comme JavaLobby.

L'édition 2005 de JavaOne marquera l'annonce de Mustang, la version 6.0 de J2SE.

IX - Téléchargements

Téléchargez les sources de l'application