Cette leçon explique comment prendre de l'information
en entrée à partir de n'importe qu'elle source de données
capable d'émettre une suite d'octets et symétriquement, comment
envoyer en sortie de l'information vers une destination acceptant une suite
d'octets. (En informatique, la base de l'information est toujours l'octet, surtout
lorsque on réalise des transferts en série).

Ces sources et destinations des séquences d'octets peuvent être des fichiers - c'est souvent le cas - mais également des connexions sur un réseau, des blocs en mémoire, le clavier de la console...

Il faut garder à l'esprit le caractère général des entrées/sorties : par exemple, l'information stockée dans des fichiers est traitée pratiquement de la même façon que celle provenant d'une connexion réseau. Bien entendu, même si le stockage des données se réduit toujours en définitive à une suite d'octets, il est souvent plus efficace de considérer que les données possèdent une structuration de plus haut niveau, comme une suite de caractères, d'entiers ou d'objets.

En java, l'objet à partir duquel on peut lire une suite d'octets se nomme flux d'entrée. Par ailleurs, on appelle flux de sortie l'objet vers lequel on peut écrire une suite d'octets.
En Java, les classes utilisées pour la gestion des flux sont nombreuses et très spécialisées. En effet, chacune d'entre elle ne s'occupe que d'un travail particulier comme, par exemple, la classe FileInputStream qui est capable de récupérer un octet dans un fichier stocké sur le disque dur. Cette classe est très compétente tout en étant simple d'utilisation puisque tout le mécanisme complexe de gestion de disque n'est pas visible à l'utilisateur. Ce dernier doit juste donner le nom du fichier concerné au constructeur de la classe. Toutefois, cette classe n'est pas compétente quand à la gestion des données de plus haut niveau comme des entiers ou des objets. Il faut alors utiliser une classe supplémentaire qui sache récupérer les octets donnés par la classe FileInputStream et qui les transforme en éléments de plus haut niveau. Par exemple, la classe DataInputStream change les octets en données : entières, réelles, booléennes...
Java utilise la notion de couche un peu comme pour les réseaux, et chaque classe filtre les informations pour obtenir la valeur désirée. Même si cela paraît complexe, c'est en fait très simple d'utilisation puisque chaque classe fait peu de choses. Ce principe a également été mis en oeuvre afin d'utiliser des classes légères et donc d'avoir une gestion des flux rapide et optimisée. Mais surtout ce système permet de construire une incroyable variété de séquences de flux effectivement utilisables.
Les deux schémas proposés ci-dessous vous permet de mieux comprendre les mécanisme que je viens d'évoquer. Remarquez qu'il est également possible de mettre en oeuvre des données compressées grâce aux classes respectives ZipInputStream et ZipOutputStream.

Quelques classes de flux en entrée avec quelques liaisons possibles

Quelques classes de flux en sortie avec quelques liaisons possibles
Ce sont les classes abstraites InputStream et OutputStream qui implémentent les types de flux d'entrée et de sortie dans lesquelles il est possible de lire ou d'écrire une suite d'octets. De plus, comme les flux d'octets conviennent mal au traitement d'informations codées en Unicode (on se souvient qu'Unicode utilise deux octets par caractère), il a été introduit une hiérarchie de classes spéciale pour le traitement de caractères. Ces classes héritent des superclasses abstraites spéciales Reader et Writer qui possèdent des opérations d'écriture et de lecture reconnaissant les caractères Unicode de deux octets et non des caractères d'un seul octet.

La classe InputStream possède une méthode abstraite read qui lit un octet et renvoie cet octet ou bien -1 si la fin de la source de données a été atteinte. Le concepteur d'une classe concrète pour un flux d'entrée va surcharger cette méthode afin de la doter des fonctionnalités nécessaires. Ainsi pour la classe FileInputStream, cette méthode lit un octet dans un fichier. L'objet prédéfini System.in de la sous-classe de InputStream permet de saisir l'information à partir du clavier. De la même manière, la classe OutputStream définit une méthode abstraite write pour écrire un octet vers une destination.
A l'issue d'une opération d'écriture ou de lecture dans un flux, il faut le fermer en appelant la méthode close (les flux consomment des ressources du système d'exploitation dont le nombre est limité). Si une application ouvre de nombreux flux sans prendre soin de les fermer, elle peut épuiser les ressources du système. La fermeture d'un flux de sortie a aussi pour effet de vider le tampon utilisé par le flux de sortie : tous les caractères se trouvant en transit dans un tampon afin de pouvoir être regroupés en paquets sont émis. Aussi, si vous ne fermez pas un fichier, le dernier paquet d'octets risque de ne jamais partir. Il est possible de forcer un vidage par la méthode flush.
D'autre part, pour le texte Unicode, il existe les sous-classes de Reader et Writer. Les méthodes de base de ces deux classes sont comparables à celles d'InputStream et d'OutputStream comme read et write. Elles opèrent exactement comme les méthodes correspondantes des classes InputStream et OutputStream, sauf bien entendu que la méthode read renvoie soit un caractère Unicode (sous forme d'un entier entre 0 et 65535), soit -1 si la fin du fichier est atteinte.
Testez le programme suivant qui permet de stocker dans le fichier "Test.dat" 5 octets (1, 2, 3, 4, 5). Ce fichier est ensuite lu pour permettre la récupération de ces octets afin de les afficher à l'écran. Vérifiez bien que la taille du fichier fait bien 5 octets.


Les flux d'octets sont rarement intéressant en tant que tel. Il est généralement nécessaire d'écrire ou de relire le résultat d'un calcul. Les flux de données possèdent des méthodes pour lire tous les types de base de Java. Les classes DataInputStream et DataOutputStream sont spécialisées dans ce domaine.


A noter que les méthodes readUTF et writeUTF permettent de manipuler des chaînes de caractères en format Unicode (UTF, Unicode Text Format). Vous remarquez que les constructeurs demandent en argument un flux d'octet respectivement InputStream et OutputStream ou bien entendu un de leurs descendants comme FileInputStream et FileOutputStream.
Testez le programme suivant qui permet de stocker dans le fichier "Test.dat" l'entier 5, le caractère 'C' et le réel -3.8. Ce fichier est ensuite lu pour permettre la récupération de ces données afin de les afficher à l'écran. Vérifiez bien que la taille du fichier correspond à votre attente soit 14 octets (4+2+8).


Vous remarquez que la construction est réalisé en une seule ligne en utilisant la technique de l'objet anonyme. Il est bien sûr possible de réaliser la construction en deux lignes en précisant le nom de l'objet intermédiaire, mais cet objet n'est pas utile par la suite. Vous remarquez également qu'il n'est pas nécessaire de fermer les flux d'octets puisque ce sont les flux de la couche supérieure qui s'en occupent intrinsèquement.
Nous venons de voir les entrées/sorties binaires (octets). Répétons-le : si les entrées/sorties binaires sont rapides et efficaces, elles ne sont pas faites pour l'oeil humain, à l'inverse des entrées/sorties texte que nous allons maintenant examiner. Par exemple l'entier 1234 est représenté en binaire (en notation hexadécimale) comme la séquence d'octets 00 00 04 D2. En format texte, ce serait une chaîne "1234".
Hélas, pour faire cela en Java, il faut un peu de travail. Nous savons que Java se sert des caractères Unicode : le codage en caractères de la chaîne "1234" en est fait (en notation hexadécimale) 00 31 00 32 00 33 00 34. La plupart des environnements possèdent aujourd'hui leur propre système de codage de caractères. Ce schéma de codage peut utiliser un octet, deux octets, ou même un nombre variable d'octets. Par exemple, sous Windows, la chaîne précédente sera en ASCII 31 32 33 34, sans les octets à zéro. Si un codage Unicode est écrit vers un fichier texte, il est très improbable que le fichier obtenu reste lisible en utilisant les outils de l'environnement de la machine hôte. Pour contourner ce problème, Java comprend tout un ensemble de flux filtrés qui permettent de passer le texte en codage Unicode aux différents codages de caractères des systèmes d'exploitation. Toutes ces classes descendent des classes abstraites Reader et Writer, et leurs noms sont calqués sur ceux que nous venons de voir. Ainsi la classe InputStreamReader transforme un flux d'entrées contenant des octets dans un codage particulier en un lecteur émettant des caractères Unicode. A l'inverse, la classe OutputStreamWriter transforme un flux de caractères Unicode en un flux d'octets dans un codage particulier de type caractères.

Voici, par exemple, comment instancier un lecteur d'entrées pour saisir des frappes clavier et les convertir automatiquement en Unicode :
|
InputStreamReader in = new InputStreamReader(System.in); |
Testez le programme suivant qui permet d'afficher à l'écran, le caractère tapé au clavier. Attention, lorsque vous avez appuyer sur le caractère, il faut ensuite le valider en appuyant sur la touche "Entrée". Lorsque vous tapez un chiffre, ce n'est pas la valeur numérique, mais bien le caractère équivalent.


Ce lecteur de flux d'entrées utilise par défaut le jeu de caractères normal du système hôte. Par exemple, sous Windows, ce sera le codage ISO 8859-1 (encore appelé ISO Latin-1 ou, pour les programmeurs Windows, "code ANSI"). On peut choisir un codage différent en le spécifiant au constructeur d'InputStreamReader. Si on désire utiliser la lecture cyrillique :
|
InputStreamReader in = new InputStreamReader(new FileInputStream("kremlin.dat"), "8859_5"); |
Il est fréquent d'associer un objet reader ou un objet writer à un fichier, et deux classes ont été créées pour faciliter l'opération, FileReader et FileWriter.
|
Par exemple, la définition de sortie : FileWriter sortie = new FileWriter("Sortie.txt"); peut remplacer : OutputStreamWriter sortie = new OutputStreamWriter(new FileOutputStream("Sortie.txt")); |

Pour sortir du texte, on peut utiliser l'objet PrintWriter. Cet outil permet d'afficher des chaînes et des nombres dans le format texte. A l'instar de DataOutputStream qui contient des méthodes agréables mais qui ne peut spécifier une destination, un objet PrintWriter doit être associé à un objet Writer vers une destination.
PrintWriter sortie = new PrintWriter(new FileWriter("employee.txt"));
Vous remarquez que le traitement des chaînes avec l'objet PrintWriter est associé à un fichier texte grâce à FileWriter. Mais, il est également possible d'associé PrintWriter à un flux d'octets comme, par exemple, lorsqu'on désire transiter des messages au travers du réseau (le réseau ne travaille qu'avec des suites d'octets). C'est là qu'on se rend compte de l'intérêt de la spécialisation des classes.

Pour écrire sur un objet PrintWriter, on utilisera les mêmes méthodes print et println que pour System.out. Ces méthodes peuvent afficher des nombres (int, short, long, float, double), des caractères, des valeurs booléennes, des chaînes, et des objets.
Si l'objet writer fonctionne en mode de vidage automatique [autoFlush], tous les caractères du tampon sont envoyés vers leur destination à chaque appel de println (les objets PrintWriter sont toujours bufférisés). Par défaut, le mode de vidage automatique n'est pas activé. Pour activer et désactiver le vidage automatique, il faut passer au constructeur la valeur booléenne appropriée dans le second argument.
|
PrintWriter out = new PrintWriter(new FileWriter("employee.txt"), true); // autoflush
|
Testez le programme suivant qui permet de stocker dans le fichier "bienvenue.txt" les chaînes de caractères "Bonjour à tous" et "Bienvenue dans le monde de Java". Vérifier que le fichier obtenu peut être lu par un logiciel d'éditeur de texte comme le bloc-note de Windows.


Pour
traiter le texte en entrée, on ne peut que recourir aux possibilités
de l'objet BufferedReader, dont la méthode
readLine() permet de lire une ligne de texte. La
méthode readLine() renvoie null
lorsqu'il ne reste plus d'entrées. Le constructeur de BufferedReader
attend un objet de type Reader, soit donc un InputStreamReader,
soit un FileReader.
Pour lire des nombres à partir d'une entrée texte, il faut d'abord lire une chaîne puis la convertir.
Testez le programme suivant qui permet de récupérer et d'afficher à l'écran les chaînes de caractères stockées dans le fichier "bienvenue.txt".


Nous pouvons travailler avec des classes qui, d'une part sont capable de travailler sur du texte, et en même temps de transiter l'information sous forme de flots d'octets - plutôt qu'avec des flots de caractères. C'est particulièrement utile lorsque nous devons propager des messages sur le réseau. En effet, la communication entre deux processus répartis sur deux ordinateurs différents ne s'effectue qu'au travers des sockets. Ces dernières ne propose le transfert d'information qu'au moyen de flux d'octets. Deux classes permettent de maitrîser parfaitement cette architecture ; Il s'agit de la classe PrintWriter pour la sortie, et la classe Scanner pour l'entrée. De plus, ces classes ont la particularité de pouvoir travailler sur du texte normal aussi bien que sur du texte formaté, c'est à dire, du texte fabriqué à partir de valeur entière, réelle, etc.
Nous pouvons travailler en entrée avec la classe BufferedReader déjà vus, mais atention cette dernière récupère des flots de caractères, il est alors nécessaire d'utiliser également la classe InputStreamReader pour transformer le flots d'octets en flots de caractères. Par ailleurs, la classe Scanner est plus avantageuse puisqu'elle est capable de reconnaître des nombres dans la suite des caractères proposés dans le texte récupéré.

Bizarrement, avant le JDK 5.0 (ou 1.5), il n'existait aucune méthode commode pour lire des entrées depuis la fenêtre de la console. Heureusement, cette situation vient d'être rectifiée.
La lecture d'une entrée au clavier s'effectue en construisant un Scanner attaché sur l'unité "entrée standard - System.in" :
Scanner clavier = new Scanner(System.in);
Les diverses méthodes de la classe Scanner permettent ensuite de lire les entrées. Par exemple, la méthode nextLine() permet de lire une ligne de saisie complète :
System.out.print("Quel est ton nom ? ");
String nom = clavier.nextLine();
Ici, nous utilisons la méthode nextLine() car la saisie pourrait contenir des espaces. Pour lire un seul mot (délimité par des espaces), appelez :
System.out.print("Quel est ton prénom ? ");
String prénom = clavier.next();
Pour lire un entier, utilisez la méthode nextInt() :
System.out.print("Quel est ton âge ? ");
int âge = clavier.nextInt();
Penser à importer le paquetage java.util.* ; ou java.util.Scanner ;
.
| méthodes | Caractéristiques |
|---|---|
Scanner(InputStream in) |
Construit un objet Scanner à partir du flux de saisie. Peut-être utilisé pour d'autres type de flux et peut donc remplacer avantageusement BufferedReader. |
String nextLine( ) |
Lit la prochaine ligne saisie. |
| String next( ) | Lit le prochain mot saisi (délimité par un espace). |
| int nextInt( ) | Lit et transforme la prochaine ligne de caractères qui représente un entier. |
| double nextDouble( ) | Lit et transforme la prochaine ligne de caractères qui représente un nombre à virgule flottante. |
| boolean hasNext( ) | Teste s'il y a un autre mot dans la saisie. |
| boolean hasNextInt( ) | Teste si la prochaine suite de catactères représente un entier. |
| boolean hasNextDouble( ) | Teste si la prochaine suite de catactères représente un nombre à virgule flottante. |
| void useLocale(Locale localité) | Permet de changer de localité. Lorsque nous utilisons la classe Scanner dans un système d'exploitation réglé en zone française, le paramètre localité est positionné par défaut à Locale.FRENCH. Du coup en France, les nombres réels s'expriment au moyen de la virgule. Si vous faites une saisie depuis le clavier, cela ne pose pas de problème, bien au contraire. Malgré tout, si vous désirez effectuer la saisie en considérant qu'il s'agit d'un double c'est-à-dire en respectant l'écriture américaine, vous devez changer de localité. Placez alors la constante Locale.US en argument de cette méthode. |
| clavier.Clavier |
|---|
package clavier; import java.util.*; import static java.lang.System.*; public class Clavier { public static void main(String[] args) { Scanner saisie = new Scanner(in); out.print("Entier : "); int entier = saisie.nextInt(); saisie.useLocale(Locale.US); out.println("Réel : "); double réel = saisie.nextDouble(); out.println("Entier = "+entier); out.println("Réel = "+réel); } } |
Attention pour la saisie des nombres réels. En france, nous devons taper par exemple "12,5" au lien de "12.5". Si vous désirez effectuer la saisie avec le point comme séparateur de la partie entière avec la partie décimale, vous devez changer de localité afin que cela soit considéré comme nombre réel écrit sous la forme US (USA). Utilisez pour cela la méthode useLocale() en spécifiant l'argument Locale.US (par défaut : Locale.FRENCH).
Nous allons maintenant fabriquer deux programmes. Le premier programme permet de stocker un ensemble d'informations de type quelconques (String, int, double) dans un fichier sous formes de texte. Le deuxième récuperera cette série d'information afin de l'afficher ensuite à l'écran.

| EcritureFichier.java |
|---|
package texte; import java.io.*; public class EcritureFichier { public static void main(String[] args) throws FileNotFoundException { PrintWriter écrire = new PrintWriter("Stockage.dat"); écrire.println("message"); int entier = 15; écrire.println(entier); double réel = -4.3; écrire.println(réel); écrire.close(); } } |
| Stockage.dat |
|---|
message 15 -4.3 |
| LectureFichier.java |
|---|
package texte; import java.io.*; import java.util.*; import static java.lang.System.*; public class LectureFichier { public static void main(String[] args) throws FileNotFoundException { Scanner lire = new Scanner(new FileInputStream("Stockage.dat")); lire.useLocale(Locale.US); String message = lire.next(); out.println("Texte = "+message); int entier = lire.nextInt(); out.println("Entier = "+entier); double réel = lire.nextDouble(); out.println("Réel = "+réel); } } |
Dans cet exemple, nous avons stocker différents types d'information dans un fichier texte. Nous pouvons utiliser ce principe mais cette fois-ci pour transiter différents types d'information sur le réseau. En réalité, mis à part la mise en oeuvre des sockets, la gestion des flux s'établie de la même façon. Ce sujet est traité dans la partie "programmation réseau".
L'emploi d'enregistrement de longueur fixe convient très bien à des données de même type. En programmation orientée objets, les objets sont rarement de même type. Considérons un tableau appelé formes constitué de l'ensemble des formes situées sur une zone graphique. Certaines cases auront des instances de Cercle, d'autres des instances de Carré.
Normalement, pour enregistrer dans des fichiers ce type d'information, il faut commencer par enregistrer le type de chaque objet, puis les données donnant l'état courant de l'objet. Inversement, à la relecture du fichier, il faudra :
Il est tout à fait possible, mais très fastidieux, de faire cela à la main. Il existe un mécanisme appelé "sérialisation des objets" qui automatise presque complètement ce processus.


Pour enregistrer les données, il faut au préalable ouvrir un objet ObjectOutputStream, puis pour enregistrer l'objet, on utilise la méthode writeObject de la classe ObjectOutputStream.
|
ObjectOutputStream sortie
= new ObjectOutputStream(new
FileOutputStream("formes.gra"));
|
Pour relire les objets, on commence par instancier un objet ObjectInputStream, puis, on lit les objets dans l'ordre dans lequel ils ont été écrits avec la méthode readObject.
|
ObjectInputStream entrée
= new ObjectInputStream(new
FileInputStream("formes.gra"));
|
Il faut, lorsqu'on recharge des objets, respecter exactement le nombre d'objets enregistrés, leur succession, et leurs types. Chaque appel à readObject lit un autre objet du type Object, qu'il faut transtyper dans son type exact.
On ne peut écrire et lire que des objets avec les méthodes writeObject / readObject, pas des nombres. Pour écrire ou lire des nombres, on utilisera des méthodes comme writeInt / readInt ou writeDouble / readDouble. Les chaînes et les tableaux, qui sont en Java des objets, relèvent par conséquent des méthodes writeObject / readObject.
Il faut cependant modifier légèrement toute classe devant être enregistrée et rechargée à partir d'un flux d'objets : la classe doit implémenter l'interface Serializable.
class Forme implements Serializable { ... }
Comme l'interface Serializable ne possède pas de méthode, il n'y a absolument rien à changer dans vos classes. Pour rendre une classe sérialisable, il n'y a rien à faire de plus.
Testez le programme suivant qui permet de récupérer et d'afficher à l'écran un tableau du personnel de l'entreprise stocké dans le fichier "personnel.lst".


En principe, nos applications ne sont directement concernées que par une seule extrémité de flux. Toutefois, PipedInputStream et PipedOutputStream (ou PipedReader et PipedWriter) permettent de créer deux extrémités d'un stream et de les connecter entre eux. Cela permet, par exemple, de faire communiquer deux Threads concurrents d'une même application au moyen d'un flux.

Pour créer un tube de communication sous forme de flux d'octets, nous utilisons un objet PipedInputStream, mais aussi un objet PipedOutputStream. Nous pouvons choisir simplement une extrémité pour construire l'autre extrémité en utilisant la première comme argument :
PipedInputStream entrée = new PipedInputStream();
PipedOutputStream sortie = new PipedOutputStream(entrée);
ou, ce qui revient au même :
PipedOutputStream sortie = new PipedOutputStream();
PipedInputStream entrée = new PipedInputStream(sortie);
Dans chacun de ces exemples, nous créons un flux d'entrée "entrée" et un flux de sortie "sortie", connectés ensemble. Les données écrites dans le tube de sortie peuvent ensuite être lues par le tube d'entrée. Il est également possible de créer séparément les objets PipedInputStream et PipedOutputStream, puis de les connecter plus tard au moyen de la méthode connect().
De toute façon, pour que ce processus puisse fonctionner, il faut impérativement qu'ils soient connecter ensemble, sinon cela n'aurait aucun sens.
Bien entendu, nous pouvons faire exactement la même chose dans le monde des caractères, en utilisant PipedReader et PipedWriter à la place de PipedInputStream et PipedOutputStream.
PipedReader entrée = new PipedReader();
PipedWriter sortie = new PipedWriter(entrée);
Une fois les deux extrémités du tube connectés, il suffit d'utiliser les deux flux comme s'il s'agissait de flux d'entrée ou de sortie classiques. Nous pouvons les utiliser tel quel en appelant la méthode read() pour lire les données à partir de PipedInputStream (ou PipedReader) et write() pour écrire des données dans PipedOutputStream (ou PipedWriter).
Si le tampon interne du tube est plein, le processus en train d'écrire est bloqué et mis en attente jusqu'à ce que la place soit disponible. Inversement, si le tube est vide, le processus de lecture est bloqué et attend que les données soient présentes
Toutefois, comme pour les autres flux d'octets, nous avons la possibilité d'utiliser des classes de flux de plus haut niveau afin d'encapsuler cette suite d'octets vers des données coorespondant à des types connus. Ainsi, nous pouvons utiliser les classes que nous connaissons déjà comme : DataInputStream, ObjectInputStream, Scanner, BufferedReader, etc.
Dans l'exemple ci-dessous, nous développons une application graphique qui permet de récupérer les événements données par la souris, notamment lorsque nous cliquons avec cette dernière. Un Thread récupère chacun de ces événements dans un fichier journal en indiquant les coordonnées de la souris par rapport à la zone cliente de la fenêtre ainsi que l'instant où a eu lieu cet événement.
| Evénement.java |
|---|
import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; import javax.swing.*; //------------------------------------------------------------------------------------------------- public class Evénement extends JFrame { PipedOutputStream tubeSortie = new PipedOutputStream(); PipedInputStream tubeEntrée = new PipedInputStream(tubeSortie); PrintWriter envoyer = new PrintWriter(tubeSortie, true); Scanner recevoir = new Scanner(tubeEntrée); public static void main(String[] args) throws IOException { new Evénement().setVisible(true); } public Evénement() throws IOException { this.setTitle("Alerte sur les événements"); this.setSize(300, 250); this.setDefaultCloseOperation(EXIT_ON_CLOSE); new Alerte(recevoir).start(); this.getContentPane().addMouseListener(new Souris(envoyer)); } } //------------------------------------------------------------------------------------------------- class Souris extends MouseAdapter { private PrintWriter envoyer; public Souris(PrintWriter envoyer) throws IOException { this.envoyer = envoyer; } public void mouseClicked(MouseEvent evt) { envoyer.println("("+evt.getX()+", "+evt.getY()+')'); } } //------------------------------------------------------------------------------------------------- class Alerte extends Thread { private Scanner recevoir; private PrintWriter journal; public Alerte(Scanner recevoir) throws FileNotFoundException { this.recevoir = recevoir; journal = new PrintWriter(new FileOutputStream("journal.txt"), true); } public void run() { while (true) { String souris = recevoir.nextLine(); journal.println("Souris : "+souris+" : "+new Date()); } } } |
| journal.txt |
|---|
Souris : (46, 38) : Mon Jan 23 08:06:37 CET 2006 Souris : (162, 86) : Mon Jan 23 08:06:38 CET 2006 Souris : (124, 200) : Mon Jan 23 08:06:39 CET 2006 Souris : (229, 163) : Mon Jan 23 08:06:39 CET 2006 Souris : (62, 97) : Mon Jan 23 08:06:40 CET 2006 Souris : (15, 59) : Mon Jan 23 08:06:40 CET 2006 Souris : (42, 30) : Mon Jan 23 08:06:40 CET 2006 Souris : (189, 24) : Mon Jan 23 08:06:41 CET 2006 |
Deux classes permettent d'encapsuler des chaînes de caractères sous forme de flux : une pour la lecture StringReader, et une pour l'écriture StringWriter.
StringReader est une autre classe très utile ; elle enveloppe la fonctionnalité d'un stream autour d'un objet String. Voici comment l'utiliser :
String texte = "Il été une fois ...";
StringReader flux = new StringReader(texte);
...
char I = (char)flux.read();
char l = (char)flux.read();
La classe StringReader s'avère utile pour lire les données d'un String comme si elle provenaient d'un flux, comme un fichier, un tube ou une socket. Par exemple, vous créez un analyseur syntaxique qui souhaite lire des motifs à partir d'un flux. Mais vous souhaitez fournir une méthode capable de traiter une grande chaîne. Vous pouvez facilement en ajouter une en utilisant StringReader.
Par ailleurs, la classe StringWriter nous permet d'écrire dans un tampon de caractères par l'intermédiaire d'un flux de sortie. Le tampon interne grossit à volonté pour s'adapter aux données. Lorsque nous avons terminé, nous pouvons récupérer son contenu sous forme de String. Dans l'exemple ci-dessous, nous créons un objet StringWriter que nous enveloppons dans un objet PrintWriter par commodité :
StringWriter tampon = new StringWriter();
PrintWriter sortie = new PrintWriter(tampon);
...
sortie.println("Un jour, un élan a frappé ma soeur ") ;
sortie.println("Non, vraiment !") ;
...
String résultat = tampon.toString() ;
Tout d'abord, nous imprimons quelques lignes sur le flux de sortie, pour lui fournir des données, puis nous récupérons le résultat sous la forme d'une chaîne de caractères avec la méthode toString().
La classe StringWriter est très utile pour capturer la sortie de quelque chose qui envoie normalement une sortie sur un flux.
.
C'est notamment le cas pour les pages JSP. En effet, lorsque nous désirons fabriquer de nouvelles balises, il est possible de récupérer le corps de cette dernière au moyen de la méthode invoke(). Cependant, cette méthode attend normalement en argument un flux de type Writer. C'est à ce moment là que nous pouvons donc proposer un flux de type StringWriter ainsi, il sera facile de retrouver le texte qui constitue le corps de la balise au moyen de la méthode toString(). Voici une portion de code qui relate cet analyse :
21 public void doTag() throws JspException, IOException { 22 StringWriter corps = new StringWriter(); 23 this.getJspBody().invoke(corps); 24 intitulé = corps.toString(); 25 ((Tableau) this.getParent()).nouvelleColonne(this); 26 }
Le paquetage java.util.zip contient des classes permettant de comprimer des données et ensuite d'assurer leurs décompressions. Les classes de ce paquetage gèrent deux formats de compression très répandus : GZIP et ZIP. Nous allons nous intéresser uniquement sur le deuxième type de compression.

Le paquetage java.util.zip propose la classe ZipOutputStream qui permet d'écrire des données compressées dans un flux dans le format ZIP. Attention, un fichier ZIP contient normalement plusieurs fichiers, dont certains ou tous peuvent être compressés. Chaque élément d'un fichier ZIP est alors représenté par un objet ZipEntry.
En tenant compte de ces considérations, pour écrire un fichier ZIP, il faut d'abord ouvrir un flux ZipOutputStream en l'empilant sur un FileOutputStream. Un ZipEntry doit ensuite être créé pour chacune des entrées futures du fichier ZIP. Il suffit de passer le nom du fichier au constructeur ZipEntry, qui déterminera les autres paramètres, comme la date de création du fichier et la méthode de décompression par défaut. Il est possible de modifier ces paramètres ci nécessaire. Il faut ensuite appeler la méthode putNextEntry() du flux ZipOutputStream pour commencer à écrire dans un nouveau fichier. Les données du fichier sont alors à envoyer dans le flux ZIP.Tout cela est à répéter pour chacun des fichiers que l'on veut archiver.
Si vous désirez envoyer des données compressées sur le réseau, vous devez employer un OutputStream donné par la socket en lieu et place de FileOutputStream.
| EcrireArchive.java |
|---|
package compression; import java.io.*; import java.util.zip.*; public class EcrireArchive { public static void main(String[] args) throws FileNotFoundException, IOException { ZipOutputStream archive = new ZipOutputStream(new FileOutputStream("archive.zip")); PrintWriter écrire = new PrintWriter(archive, true); archive.putNextEntry(new ZipEntry("Premier.txt")); écrire.println("Il s'agit juste"); écrire.println("d'un premier texte"); écrire.println("qui va être compressé."); archive.putNextEntry(new ZipEntry("Deuxieme.txt")); écrire.println("Le deuxième texte"); écrire.println("est également compressé."); archive.close(); } } |
Pour la décompression des données au format ZIP, nous effectuons l'opération inverse en enveloppant simplement un ZipInputStream autour d'un FileInputStream. Pour lire dans un ZiptInputStream, vous devez appeler la méthode getNextEntry() avant de lire chaque entrée de l'archive représentant le fichier compressé. Lorsque getNextEntry() renvoie null, il ne reste plus d'élément à lire.
| LireArchive.java |
|---|
package compression; import java.io.*; import java.util.zip.*; import java.util.Scanner; import static java.lang.System.*; public class LireArchive { public static void main(String[] args) throws FileNotFoundException, IOException { ZipInputStream archive = new ZipInputStream(new FileInputStream("archive.zip")); ZipEntry fichier; while ((fichier = archive.getNextEntry())!=null) { Scanner lecture = new Scanner(archive); out.println("Fichier : "+fichier.getName()); out.println("-------------------------------------------"); while (lecture.hasNextLine()) { out.println(lecture.nextLine()); } out.println("-------------------------------------------"); // archive.closeEntry(); } archive.close(); } } |
Dans certains cas, il peut être intéressant de travailler avec des flux directement sous forme de tableaux d'octets par l'intermédiaire des classe ByteArrayInputStream ou ByteArrayOutputStream. C'est notamment le cas lorsque nous travaillons avec des images qui transitent sur le réseau ou sur tout autre flux binaire, comme la transmission entre threads.
Effectivement, en local, pour récupérer une image, nous passons directement par la classe ImageIO, sans passer par un intermédiaire quelconque. Dans le cas du réseau, par exemple, il est plus avantageux de récupérer le fichier binaire et d'envoyer les informations brutes, c'est-à-dire le tableau d'octets correspondant sans déformation. Effectivement, la classe ImageIO propose une compression qui n'est pas toujours utile dans le cas notamment d'une simple lecture d'image.
Voici donc toute la procédure à suivre pour récupépérer une image par un tableau d'octets en restant toutefois sur le même poste local :
File fichier = new File(répertoire+"UneImage.jpg"); byte[] octets = new byte[(int)fichier.length()]; FileInputStream photo = new FileInputStream(fichier); photo.read(octets); ByteArrayInputStream fluxImage = new ByteArrayInputStream(octets); BufferedImage image = ImageIO.read(fluxImage);
Voici un autre exemple qui fabrique un tableau d'octets à partir d'une image déjà existante :
BufferedImage image = ... ; ... ByteArrayOutputStream fluxImage = new ByteArrayOutputStream(); ImageIO.write(image, "PNG", fluxImage); byte[] octets = fluxImage.toByteArray();
![]()
|
Souvenez-vous qu'à l'utilisation, la méthode concernée affiche le message désiré à l'écran et en même temps récupère la valeur saisie au clavier. Voici un exemple d'utilisation possible :
|
|
Le nombre de formes placées sur la surface de travail est limité à 30. Il doit être possible d'enregistrer l'ensemble du tracé sur le disque dur. En cliquant sur "Nouveau", vous effacer la surface de travail, et vous pouvez de nouveau tracer au maximum les 30 formes. A tout moment, il est possible de récupérer des tracés déjà sauvegardés. Enfin lorsque vous quittez l'application, le système doit vous demander de sauvegarder votre travail. Dans Java, il existe une boîte de dialogue de sélection de fichier toute faite représentée par la classe JFileChooser.
Voici les étapes à suivre pour mettre en oeuvre une boîte de dialogue de fichier et récupérer la sélection de l'utilisateur :
|