Les flux et les fichiers

Chapitres traités   

Cette étude 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 nous réalisons des transferts dits 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, etc.

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, qui eux-mêmes peuvent représenter des informations d'encore plus hauts niveaux comme la musique ou les images.

Choix du chapitre Principes généraux sur les flux

Dans le langage de programmation java, l'objet à partir duquel nous pouvons lire une suite d'octets se nomme flux d'entrée. Par ailleurs, nous appelons flux de sortie l'objet vers lequel nous pouvons écrire une suite d'octets.

Lire et écrire des octets

Ce sont les classes abstraites InputStream et OutputStream qui implémentent ces deux types de flux.

Je rappelle qu'une classe abstraite apporte essentiellement un mécanisme de factorisation du comportement commun d'un essemble de classes à un niveau supérieur. Cela conduit à un code plus propre et une meilleure lisibilité de l'arbre d'héritage. Cette approche est utilisée pour les entrées/sorties en Java.

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.

  1. La classe abstraite 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.

  2. De la même manière, la classe abstraite OutputStream définit une méthode abstraite write() pour écrire un octet vers une destination.
  3. Les méthodes read() et write() peuvent toutes les deux bloquer un thread jusqu'à ce qu'un flux soit effectivement lu ou écrit. Cela signifie que, si un octet ne peut être immédiatement lu ou écrit (le plus souvent dans le cas d'une connexion réseau chargée), Java suspend le thread effectuant cet appel. Cela permet aux autres threads d'effectuer un travail utile pendant que la méthode attend le flux redevienne accessible.

    Pour en savoir plus sur la gestion des threads avec les flux.
    §

  4. La méthode available() détermine le nombre d'octets accessibles en lecture à un moment donné. Nous voyons que le morceau de code qui suit ne risque pas de rester en attente :
    int nombreDisponible = lecture.available();
    if (nombreDisponible > 0) {
      byte[] octets = new byte[nombreDisponible];
      lecture.read(octets);
    }
  5. 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(). Cet appel libère les 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 également possible de forcer un vidage au moyen de la méthode flush().

Les programmeurs Java auront peu l'utilité d'une classe flux qui ne possède que des méthodes concrètes encapsulant les fonctions de base read() et write(), car un programme n'a que rarement besoin de lire ou d'écrire des flux d'octets. Les données que vous allez rencontrer contiennent en général des nombres, des chaînes et des objets.

Java fait dériver de nombreuses classes flux des classes de base InputStream et OutputStream qui vont précisément permettre de traiter les données dans ces formats habituels et non plus au plus bas niveau de l'octet.

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.

La classe java.io.InputStream
abstract int read()
Lit puis renvoie un octet de données. La méthode read() renvoie -1 à la fin du flux.
int read(byte[] octets)
Lit dans un tableau d'octets et renvoie le nombre réel d'octets lus ou -1 à la fin du flux. La méthode read() lit au plus octets.length octets.
int read(byte[] octets, int offset, int longueur)
Lit dans un tableau d'octets. La méthode read() renvoie le nombre réel d'octets lus ou -1 si la fin du flux est atteinte.
- octets : Tableau dans lequel sont lues les données.
- offset : Position dans le tableau à partir de laquelle les premiers octets seront placés.
- longueur : Nombre maximal d'octets à lire.
long skip(long nombre)
Saute nombre octets du flux d'entrée. Renvoie le nombre d'octets effectivement sautés (qui peut donc être inférieur à nombre si la fin du flux est rencontrée).
int available()
Renvoie le nombre d'octets disponibles sans blocage (un blocage implique que le thread courant passe son tour).
void close()
Ferme le flux d'entrée.
void mark(int limite)
Place un marqueur à la position courante dans le flux d'entrée (tous les flux ne supportent pas cette fonctionnalité). Quand le nombre d'octets lus à partir du flux d'entrée dépasse limite, le marqueur disparaît du flux.
void reset()
Revient au dernier marqueur. Les appels ultérieurs à read() relisent les mêmes octets. S'il n'existe pas de marqueur courant, le flux n'est pas réinitialisé.
boolean markSupported()
Renvoie true si le flux accepte le marquage.
La classe java.io.OutputStream
abstract void write(int octet)
Ecrit un octet de données dans le flux de sortie.
void write(byte[] octets)
Ecrit tous les octets du tableau dans le flux de sortie.
void write(byte[] octets, int offset, int longueur)
Ecrit une plage d'octets du tableau dans le flux de sortie.
- octets : Tableau source de données.
- offset : Déplacement dans ce tableau du premier octet à écrire.
- longueur : Nombre d'octets à écrire.
void close()
Vide entièrement le tampon de sortie et ferme le flux.
void flush()
Vide entièrement le tampon de sortie, et plus précisément envoie vers la cible les données se trouvant dans un tampon.
Exemple de mise en oeuvre - transfert de fichiers

Comme nous venons de l'évoquer, la plupart du temps, nous n'avons pas besoin de traiter les informations venant d'un flux au niveau le plus bas, c'est-à-dire au niveau de l'octet. Toutefois, il existe bien un exemple qui semblerait s'y prêter : la copie de fichiers. Dans ce cas particulier, nous n'avons pas besoin d'interpréter le contenu du fichier. Il s'agit simplement de prendre l'ensemble des octets comme ils se présentent et de les placer ensuite dans le nouveau fichier.

Ce sujet est particulièrement intéressant, notamment lorsque nous ferrons de l'archivage en réseau. A l'attendant, je vous propose de faire une application qui permet de sélectionner les fichiers importants et d'en faire des copies vers une zone de stockage particulière.



Codage correspondant
package flux;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;

public class CopieFichier extends JFrame {
   private JFileChooser sélection = new JFileChooser();
   private JToolBar boutons = new JToolBar();
   private JLabel résultat = new JLabel("Copie de fichiers dans la zone de stockage");
   private final String stockage = "G:/Stockage/";

   public CopieFichier() {
      super("Copie de fichiers");
      add(boutons, BorderLayout.NORTH);      
      boutons.add(new AbstractAction("Sélection du fichier") {
         public void actionPerformed(ActionEvent e) {
            if (sélection.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
               try {
                  résultat.setText("Attente...");
                  File fichier = sélection.getSelectedFile();
                  FileInputStream lecture = new FileInputStream(fichier);
                  FileOutputStream écriture = new FileOutputStream(stockage+fichier.getName());
                  byte[] octets;
                  int nombre = lecture.available();
                  if (nombre > 0) {
                     octets = new byte[nombre];
                     lecture.read(octets);
                     écriture.write(octets);
                     lecture.close();
                     écriture.close();
                     résultat.setText("Copie du fichier effectuée avec succès");
                  }
               }         
               catch (FileNotFoundException ex) {
                  résultat.setText("Fichier inexistant");
               }
               catch (IOException ex) {
                  résultat.setText("Problème de lecture du fichier");
               }
            }
         }
      });
      boutons.addSeparator();
      boutons.add(résultat);
      boutons.addSeparator();
      pack();
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] args) { new CopieFichier(); }
}
  1. Les classes FileInputStream et FileOutputStream sont des classes concrètes qui descendent directement des classes abstraites, respectivement des InputStream et OutputStream.
  2. A ce titre, elles peuvent donc tout-à-fait traiter les informations de flux au niveau de l'octet.
  3. Attention, ce programme fonctionne très bien pour les petits fichiers (jusqu'à quelques Méga-Octets toutefois). Comme nous plaçons la totalité du contenu du fichier en mémoire, il pourrait y avoir des saturations légitimes.
  4. Il serait donc préférable de faire des copies par paquets, comme le montre l'exemple qui suit, afin que nous puissions prendre en compte tous les fichiers, quelque soit leurs dimensions.
Modification correspondante
...
public class CopieFichier extends JFrame {
   private JFileChooser sélection = new JFileChooser();
   private JToolBar boutons = new JToolBar();
   private JLabel résultat = new JLabel("Copie de fichiers dans la zone de stockage");
   private final String stockage = "G:/Stockage/";
   private final int BUFFER = 4096;

   public CopieFichier() {
      super("Copie de fichiers");
      add(boutons, BorderLayout.NORTH);      
      boutons.add(new AbstractAction("Sélection du fichier") {
         public void actionPerformed(ActionEvent e) {
            if (sélection.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
               try {
                  résultat.setText("Attente...");
                  File fichier = sélection.getSelectedFile();
                  FileInputStream lecture = new FileInputStream(fichier);
                  FileOutputStream écriture = new FileOutputStream(stockage+fichier.getName());
                  byte[] octets = new byte[BUFFER];
                  while (lecture.available() > 0) {
                     if (lecture.available() < BUFFER) octets = new byte[lecture.available()];
                     lecture.read(octets);
                     écriture.write(octets);
//                     écriture.flush();
                  }                  
                  lecture.close();
                  écriture.close();
                  résultat.setText("Copie du fichier effectuée avec succès");
               }         
               catch (FileNotFoundException ex) {
                  résultat.setText("Fichier inexistant");
               }
               catch (IOException ex) {
                  résultat.setText("Problème de lecture du fichier");
               }
            }
         }
      });
...

La faune des flux

En Java, les classes utilisées pour la gestion des flux sont très nombreuses et spécialisées. En effet, chacune d'entre elles 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 transforment en éléments de plus haut niveau. Par exemple, la classe DataInputStream change les octets en données : entières, réelles, booléennes...

Hiérarchie des classes d'entrées/sorties sous forme de flux d'octets

Séparons les représentants de la faune des flux selon leur utilité. Quatre classes abstraites sont à l'origine de la faune : InputStream, OutputStream, Reader et Writer. Nous ne pouvons créer des objets directement sur ces classes puisqu'elles sont abstraites, mais d'autres méthodes peuvent tout-à-fait faire appel ou renvoyer ces classes.

Par exemple, la classe Socket, classe qui implémente les fonctionnalités du réseau, possède deux méthodes getInputStream() et getOutputStream() qui renvoient respectivement un InputStream ou un OutputStream. Nous utilisons ensuite des objets issus de la hiérarchie des classes de flux pour lire ou écrire des informations de haut niveau qui transitent sur le réseau.

Nous avons vu que les classes InputStream et OutputStream ne permettent que de lire et d'écrire des octets un par un ou des tableaux d'octets. Elles ne possèdent pas de méthode pour lire ou écrire des chaînes de caractères ou des nombres. Des classes plus puissantes sont nécessaires. Par exemple, DataInputStream et DataOutputStream permettent de lire et d'écrire tous les types de base de Java.


Texte Unicode

Pour le texte Unicode, il existe donc des sous-classes de Reader et de Writer. Les méthodes de base de ces deux classes sont comparables à celles d'InputStream et d'OuputStream :

abstract int read() et abstract void write(int octet) : Ces deux méthodes opèrent comme les méthodes correspondantes des classes InputStream et OutputStream, sauf bien entendu que la méthode read() renvoie soit une unité de code Unicode (sous forme d'un entier entre 0 et 65535), soit -1 si la fin du flux est atteint.

Hiérarchies des flux de texte Unicode en entrée et en sortie

Les interfaces liées aux flux

De plus, depuis Java SE 5.0, nous avons à notre disposition quatre nouvelles interfaces : Closeable, Flushable, Readable et Appendable :

Interfaces liées aux flux


  1. Les deux premières interfaces sont simples, avec respectivement , les méthodes associées close() et flush().
  2. Les classes InputStream, OutputStream, Reader et Writer implémentent toute l'interface Closeable. OutputStream et Writer implémentent l'interface Flushable.
  3. L'interface Readable dispose d'une seule méthode : read().
  4. La classe CharBuffer possède des méthodes pour l'accès en lecture/écriture séquentiel et aléatoire. Elle représente un tampon en mémoire ou un fichier à concordance de mémoire.
  5. L'interface Appendable possède deux méthodes append() pour annexer des caractères uniques et des suites de caractères.
  6. Le type CharSequence est une autre interface décrivant les propriétés minimales d'une suite de valeurs char. Il est implémenté par String, CharBuffer et StringBuilder/StringBuffer.
  7. Parmi les classes de la faune des flux, seul Writer implémente Appendable.
L'interface java.io.Closable
void close()
Ferme ce Closable. Cette méthode peut déclencher une exception IOException
L'interface java.io.Flushable
void flush()
Vide ce Flushable.
L'interface java.lang.Readable
int read(CharBuffer tampon)
Tente de lire autant de valeurs char dans tampon qu'il peut contenir. Renvoie le nombre de valeurs lues ou -1 si aucune valeur n'est disponible à partir de ce Readable.
L'interface java.lang.Appendable
Appendable append(char caractère)
Annexe l'unité de code à cet Appendable ; renvoie this.
Appendable append(CharSequence séquence)
Annexe toutes les unités de code de séquence à cet Appendable ; renvoie this.
L'interface java.lang.CharSequence
char charAt(int index)
Renvoie l'unité de code à l'indice donné.
int length()
Renvoie le nombre d'unités de code de cette suite.
CharSequence subSequence(int indexDébut, int indexFin)
Renvoie un CharSequence constitué des unités de code stockées à l'indice indexDébut jusqu'à indexFin - 1.
String toString()
Renvoie une chaîne constituée des unités de code de cette suite.

Empilement des flux filtrés

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 réalité 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écanismes 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

Techniques de l'empilement des flux filtrés

A titre informatif prenons tout-de-suite des exemples de structures :

  1. Comme nous l'avons déjà mis en oeuvre, FileInputStream et FileOutputStream fournissent des flux d'entrée et de sortie associés à un fichier de disque dur. Il suffit alors de préciser le nom du fichier ou le chemin complet dans le constructeur. Ainsi, lecture va rechercher dans le répertoire courant un fichier appelé "Formes.dessin" :

    FileInputStream lecture = new FileInputStream("Formes.dessin");

    Comme nous l'avons découvert dans nos exemples, il est possible également d'utiliser un objet File intermédiaire :

    File fichier = new File("Formes.dessin");
    FileInputStream lecture = new FileInputStream(fichier);

    Il peut être utile de connaître le répertoire courant de l'utilisateur puisque toutes les classes de java.io interprètent les chemins relatifs à partir de ce dernier : cette information est obtenue par un appel à System.getProperty("user.dir").

  2. Comme les classes abstraites InputStream et OutputStream, ces classes ne sont capables que de lire ou d'écrire des octets. Ainsi, nous ne pouvons lire qu'une suite d'octet individuel ou un tableau d'octets à partir de l'objet lecture :

    byte octet = (byte) lecture.read();

  3. Nous verrons plus loin que la classe DataInputStream est spécialisée sur la lecture de type primtif numérique, comme les int, les double, etc. à partir d'une suite d'octets.

    DataInputStream primitif = ... ;
    double
    réel = primitif.readDouble();

    De même que FileInputStream ne possède pas de méthode pour lire les types numériques, DataInputStream n'a pas de méthode pour accéder aux données d'un fichier. Chaque classe de flux proposent des fonctionnalités spécialisées et réduites. Ainsi, au travers de cet exemple, nous voyons que FileInputStream est capable de proposer une suite d'octets issus d'un fichier sur le disque dur (ce qui d'ailleurs n'est pas rien) et que cette suite d'octets peut ensuite être formatée à l'aide de la classe DataInputStream pour aboutir à une information plus intuitive et plus adaptée à la situation, en retrouvant ainsi les valeurs numériques pouvant être exploitées directement. Pour conclure, chaque classe de flux possède ses propres compétences.

    Pour en savoir plus sur le flux de données.
    §

  4. Java a donc recours à un mécanisme astucieux pour séparer les deux rôles. Certains flux (comme FileInputStream ou le flux d'entrée renvoyé par la méthode openStream() de la classe URL) peuvent accéder aux octets se trouvant dans les fichiers ou à d'autres endroits plus exotiques. D'autres flux (comme DataInputStream et PrintWriter) savent assembler les octets en des types de données plus utiles. En java, il relève de la responsabilité du programmeur de combiner les deux flux en ce que nous appelons souvent des flux filtrés, plus précisément en passant un flux existant au constructeur d'un autre flux. Par exemple, pour lire des nombres dans un fichier, nous commencerons par créer un FileInputStream que nous passerons ensuite au constructeur d'un DataInputStream :

    FileInputStream lecture = new FileInputStream("Formes.dessin");
    DataInputStream
    primitif = new DataInputStream(lecture) ;
    double
    réel = primitif.readDouble();

    Comme au préalable, le flux ainsi obtenu continue à accéder aux données du fichier associé au flux d'entrée, mais nous disposons maintenant d'une interface beaucoup plus puissante.

  5. Vous pouvez combiner leurs sous-classes pour construire les flux dont vous avez besoin. Par exemple, par défaut, les flux ne sont pas bufférisés. Cela implique un appel système pour chaque octet lu ou écrit. Supposons que vous vouliez à la fois la bufférisation et les méthodes d'entrée de type primitif du fichier "Formes.dessin" se trouvant dans le répertoire courant. Il vous faudra la séquence de constructeurs suivante :

    DataInputStream primitif = new DataInputStream(new BufferedInputStream(new FileInputStream("Formes.dessin")));
    double
    réel = primitif.readDouble();

    DataInputStream se trouve le dernier dans la chaîne des constructeurs - parce que nous voulons disposer des méthodes de DataInputStream et que celles-ci doivent elles-mêmes utiliser la méthode bufférisée read(). Malgré sa grande laideur, ce type de codage est incontournable et finalement très simple à réalisé : vous devez continuer à empiler des constructeurs de flux jusqu'à obtenir les fonctionnalités voulues.

  6. Il peut parfois être nécessaire de garder une trace des flux intermédiaires du chaînage. Par exemple, en entrée, nous avons quelquefois besoin de tester la valeur de l'octet suivant. Java fournit à cet effet la classe PushbackInputStream.

    PushbackInputStream intermédiaire = new PushbackInputStream(new BufferedInputStream(new FileInputStream("Formes.dessin")));

    Parcourez à tout hasard l'octet suivant :

    int octet = intermédiaire.read();

    Quitte à le renvoyer à sa place s'il ne correspond pas à votre attente :

    if (octet != '<') intermédiaire.unread(octet);

    Le problème est que read() et unread() sont les seules méthodes applicables à ce type de flux d'entrée. Si vous désirez à la fois anticiper sur la lecture et lire les nombres, il vous faut un flux d'entrée du type précédent et un flux d'entrée de données :

    PushbackInputStream intermédiaire = new PushbackInputStream(new BufferedInputStream(new FileInputStream("Formes.dessin")));
    DataInputStream primitif = new DataInputStream(intermédiaire);
    double réel = primitif.readDouble();

    La possibilité de combiner des classes de filtres permet de construire une incroyable variété de séquences de flux effectivement utilisables. Par exemple, si le problème est de lire des nombres se trouvant dans un fichier ZIP, nous utiliserons la séquence de flux suivante :

    ZipInputStream compressé = new ZipInputStream(new FileInputStream("Formes.dessin"));
    DataInputStream primitif = new DataInputStream(compressé);

    En définitive, si l'on passe sur les monstrueux constructeurs nécessaires à l'empilement des flux, pouvoir combiner les flux en Java est une fonctionnalité très agréable.

    Pour en savoir plus sur les flux compressés.
    §

La classe java.io.FileInputStream
FileInputStream(String nomFichier)
Crée un nouveau flux de fichiers en entrée, pour le fichier dont le chemin est passé dans la chaîne nomFichier.
FileInputStream(File fichier)
Crée un nouveau flux de fichiers en entrée, à partir de l'information encapsulée dans l'objet File.
La classe java.io.FileOutputStream
FileOutputStream(String nomFichier)
Crée un nouveau flux de fichiers en sortie spécifié par la chaîne nomFichier. Les chemins qui ne sont pas absolus sont considérés comme relatifs au répertoire courant. Attention : un fichier existant portant le même nom sera automatiquement détruit.
FileOutputStream(String nomFichier, boolean ajout)
Crée un nouveau flux de fichiers en sortie spécifié par la chaîne nomFichier. Les chemins qui ne sont pas absolus sont considérés comme relatifs au répertoire courant. Si le paramètre ajout est true, les données sont placées à la fin du fichier. Ainsi, un fichier existant possédant le même nom ne sera pas écrasé dans ce seul cas.
FileOutputStream(File fichier)
Crée un nouveau flux de fichiers en sortie, à partir de l'information encapsulée dans l'objet File. Attention : un fichier existant portant le même nom sera automatiquement détruit.
La classe java.io.BufferedInputStream
BufferedInputStream(InputStream fluxEntrée)
Crée un nouveau flux bufférisé avec une taille de tampon par défaut. Un flux d'entrées bufférisées lit les caractères à partir du flux sans avoir à accéder à chaque caractère depuis le dispositif source des données. Quand le tampon est vide, le système lit un nouveau bloc de données qui est placé automatiquement dans le tampon.
BufferedInputStream(InputStream fluxEntrée, int taille)
Crée un nouveau flux bufférisé avec une taille de tampon définie par l'utilisateur.
La classe java.io.BufferedOutputStream
BufferedOutputStream(OutputStream fluxSortie)
Crée un nouveau flux bufférisé avec une taille de tampon par défaut. Un flux de sorties bufférisées accepte des caractères devant être écrits sans avoir à accéder à chaque caractère depuis le dispositif source des données. Quand le tampon est plein ou sur un ordre de vidage du flux, les données du tampon sont effectivement écrites.
BufferedOutputStream(OutputStream fluxSortie, int taille)
Crée un nouveau flux bufférisé avec une taille de tampon définie par l'utilisateur.
La classe java.io.PushbackInputStream
PushbackInputStream(InputStream fluxEntrée)
Crée un flux avec anticipation sur un caractère.
PushbackInputStream(InputStream fluxEntrée, int taille)
Crée un flux bufférisé avec un tampon d'anticipation de la taille spécifié par l'utilisateur.
void unread(int octet)
Renvoie un caractère dans le flux d'entrée. Ce caractère sera relu par un appel ultérieur. Nous devons renvoyer les caractères un par un.
Exemple de mise en oeuvre - reprise du transfert de fichiers

Nous allons reprendre tout simplement le sujet précédent auquel nous rajoutons une simple bufférisation intermédiaire.


Codage correspondante
package flux;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;

public class CopieFichier extends JFrame {
   private JFileChooser sélection = new JFileChooser();
   private JToolBar boutons = new JToolBar();
   private JLabel résultat = new JLabel("Copie de fichiers dans la zone de stockage");
   private final String stockage = "G:/Stockage/";
   private final int BUFFER = 4096;

   public CopieFichier() {
      super("Copie de fichiers");
      add(boutons, BorderLayout.NORTH);      
      boutons.add(new AbstractAction("Sélection du fichier") {
         public void actionPerformed(ActionEvent e) {
            if (sélection.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
               try {
                  résultat.setText("Attente...");
                  File fichier = sélection.getSelectedFile();
                  BufferedInputStream lecture = new BufferedInputStream(new FileInputStream(fichier));
                  BufferedOutputStream écriture = new BufferedOutputStream(new FileOutputStream(stockage+fichier.getName()));
                  byte[] octets = new byte[BUFFER];
                  while (lecture.available() > 0) {
                     if (lecture.available() < BUFFER) octets = new byte[lecture.available()];
                     lecture.read(octets);
                     écriture.write(octets);
//                     écriture.flush();
                  }                  
                  lecture.close();
                  écriture.close();
                  résultat.setText("Copie du fichier effectuée avec succès");
               }         
               catch (FileNotFoundException ex) {
                  résultat.setText("Fichier inexistant");
               }
               catch (IOException ex) {
                  résultat.setText("Problème de lecture du fichier");
               }
            }
         }
      });
      boutons.addSeparator();
      boutons.add(résultat);
      boutons.addSeparator();
      pack();
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] args) { new CopieFichier(); }
} 

 

Choix du chapitre Flux de texte

Lorsque vous sauvegardez vos données dans un fichier (ou autre type de flux de destination), quelque soit leurs natures, vous avez toujours le choix entre le format binaire, donc non éditable, et l'enregistrement sous forme de texte. 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, c'est la chaîne "1234". 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 textes que nous allons maintenant examiner.

Lorsque nous sauvegardons des chaînes de caractères, nous devons prendre en compte l'encodage des caractères en Java. Ainsi dans l'encodage UTF-16 (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.

Malheureusement, 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, avec l'encodage ISO-8859-1, encodage très utilisé aux états-unis et dans l'Europe de l'ouest, la chaîne précédente sera 31 32 33 34, sans les octets à zéro.

Le codage ISO-8859-15 gagne maintenant en importance, il remplace certains caractères les moins utiles du jeu ISO-8859-1 avec les lettres accentuées en français et en finnois, et surtout, il remplace le caractère de devise internationnale ¤ par le symbole de l'euro dans le point de code 0xA4.

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.

  1. 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);

    Ce lecteur de flux d'entrées utilise par défaut le jeu de caractères normal du système hôte. Par exemple, se sera le codage ISO 8859-1 pour l'Europe de l'ouest (encore appelé ISO Latin-1 ou, pour les programmeurs Windows, "code ANSI"). Nous pouvons choisir un codage différent en le spécifiant au moment de la construction de l'InputStreamReader.

  2. Ainsi, si nous désirons utiliser la lecture qui permet de prendre en compte le symbole de l'€uro :

    InputStreamReader in = new InputStreamReader(new FileInputStream("monnaie.txt"), "ISO8859_15");

  3. Parcequ'il est fréquent d'associer un objet Reader ou un objet Writer à un fichier, deux classes ont été créées pour faciliter l'opération, FileReader et FileWriter. ainsi les deux écritures suivantes sont équivalentes :

    FileWriter fichier = new FileWriter("Texte.txt"));
    OutputStreamWriter
    out = new OutputStreamWriter(new FileOutputStream("Texte.txt"));

Comment écrire un texte dans un flux de sortie

Pour sortir du texte, nous pouvons utiliser l'objet PrintWriter. Cet outil permet d'afficher des chaînes et des nombres dans le format texte. Il existe pas mal de constructeur adaptés à la situation que vous désirez. Par défaut, la construction d'un PrintWriter est associée à un FileWriter. Ainsi les deux constructions suivantes sont équivalentes :

PrintWriter sortie = new PrintWriter("Texte.txt");
PrintWriter
sortie = new PrintWriter(new FileWriter("Texte.txt"));

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, lorsque nous désiront transiter des messages au travers du réseau (le réseau ne travaille qu'avec des suites d'octets). C'est là que nous nous rendons compte de l'intérêt de la spécialisation des classes.

PrintWriter sortie = new PrintWriter(new FileOutputStream("Texte.txt"));

Le constructeur PrintWriter(OutputStream ...) ajoute automatiquement un OutputStreamWriter pour convertir le flux de caractères Unicode vers un flux d'octets classique.
§

  1. Pour écrire sur un objet PrintWriter, nous utilisont les mêmes méthodes print(), println() et printf() 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 (au travers de la méthode toString()).

    PrintWriter sortie = new PrintWriter("Texte.txt");
    String nom = "REMY";
    double réel = 12.5;
    sortie.print(nom);
    sortie.print(' ');
    sortie.println(réel);
    //-------------------------------- Résultat
    REMY 12.5

    L'ensemble des valeurs envoyées dans le flux sont converties en caractères pour être ensuite transformées en flot d'octets, et pour finir, sont enregistrées dans le fichier "Texte.txt".

    La méthode println() ajoute automatiquement, à la suite des caractères envoyées qui constituent la chaîne, les caractères supplémentaires de fin de ligne qui conviennent au système cible ("\r\n" pour Windows, "\n" pour UNIX ou "\r" pour Macintosh). Ces chaînes supplémentraires sont renvoyées par l'appel à la méthode System.getProperty("line.separator").

  2. 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 sortie = new PrintWriter(new FileWriter("Texte.txt"), true); // autoflush

    Les méthodes print() ne lancent pas d'exception. La méthode checkError() permet de savoir s'il s'est produit un problème avec le flux.
    §

    Nous ne pouvons pas envoyer du binaire à un objet PrintWriter. Il ne sait traiter que des sorties au format texte.
    §


    La classe java.io.PrintWriter
    PrintWriter(Writer fluxSortie)
    PrintWriter(Writer fluxSortie, boolean autoFlush)
    Crée un nouveau flux de sortie au format texte avec ou sans vidage automatique (autoFlush à true pour valider cette fonctionnalité).
    PrintWriter(OutputStream fluxSortie)
    PrintWriter(OutputStream fluxSortie, boolean autoFlush)
    Crée un nouveau flux de sortie au format texte avec ou sans vidage automatique (autoFlush à true pour valider cette fonctionnalité), à partir d'un objet existant OutputStream en créant automatiquement l'objet intermédiaire nécessaire OutputStreamWriter.
    PrintWriter(String nomFichier)
    PrintWriter(File fichier)
    Crée un nouveau flux de sortie au format texte à partir d'un fichier ou de son nom en créant automatiquement l'objet intermédiaire nécessaire FileWriter.
    void print(Object objet)
    Affiche un objet à partir de la chaîne de caractères provenant de la méthode toString().
    void println(String chaîne)
    Affiche une chaîne suivie par une fin de ligne. Vide le flux dans le cas où le mode de vidage automatique est activé.
    void print(char[ ] tableau)
    Affiche un tableau de caractères Unicode.
    void print(char caractère)
    Affiche un caractère Unicode.
    void print(int entier)
    void print(long entier)
    void print(float réel)
    void print(double réel)
    void print(boolean validation)
    Affiche la valeur proposée au format texte.
    boolean checkError()
    Renvoie true s'il se produit une erreur de formatage ou une erreur de sortie. Si une erreur se produit, le flux devient corrompu et tous les appels à checkError() renvoient true.

L'entrée du texte

Nous savons déjà que nous pouvons utiliser :

  1. DataOutputStream pour écrire des données au format binaire.
  2. PrintWriter pour écrire au format texte.

Ainsi, nous pouvons légitimement penser qu'il existe une classe permettant de lire les données au format texte symétrique de DataInputStream.

  1. La symétrie la plus proche est la classe Scanner, que nous avons largement utilisée.

    Scanner entrée = new Scanner(new FileInputStream("Texte.txt"));

  2. Pour traiter le texte en entrée, bien qu'elle soit moins performante que la classe Scanner, nous pouvons également recourrir aux possibilités de l'objet BufferedReader, dont la méthode readLine() permet de lire une ligne de texte.

    BufferedReader entrée = new BufferedReader(new FileReader("Texte.txt"));

  3. 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 un FileReader, soit un InputStreamReader. Voici un exemple typique de boucle de lecture en entrée :
    String ligne;
    while ((ligne = entrée.readLine()) != null ) {
       // Faire quelque chose avec ligne
    }
  4. La classe FileReader convertit déjà les octets en caractères Unicode. Pour les autres sources en entrée, il faut par contre utiliser le filtre InputStreamReader. A la différence de PrintWriter, l'objet InputStreamReader ne possède pas de méthode remplaçant automatiquement les caractères Unicode par des octets :

    BufferedReader clavier = new BufferedReader(new InputStreamReader(System.in));

  5. Pour lire des nombres à partir d'une entrée de texte, il faut d'abord lire une chaîne, puis la convertir :
    String ligne = clavier.readLine();
    double réel = Double.parseDouble(ligne);
    Ce qui précède est correct s'il n'y qu'un seul nombre par ligne. Le problème, c'est que la classe BufferedReader est finalement très rudimentaire, elle ne possède pas de méthodes pour lire des nombres. Dans notre exemple précédent, si nous possédons plusieurs nombres par ligne, ou si le texte est mélangé avec les nombres, cela ne peut fonctionner tel quel. Il est possible de travailler un peu plus et de découper alors la chaîne en entrée en utilisant par exemple les utilitaires de la classe StringTokenizer. Cependant, la meilleure solution reste, et de très loin, l'utilisation de la classe Scanner. Voici comment coder simplement une saisie d'un nombre réel à partir du clavier :

    Scanner clavier = new Scanner(System.in);
    double réel = clavier.nextDouble();

  6. Il existe une classe, LineNumberReader qui hérite de BufferedReader qui possède également, par héritage, la méthode readLine(), mais qui propose en plus la prise en compte automatique de la numérotation de ligne actuellement récupérée à l'aide de la méthode getLineNumber(). Il est même possible de passer directement à la ligne souhaitée en utilisant cette fois-ci la méthode setLineNumber(int).
  7. Pour finir, Java possède des classes StringReader et StringWriter pour traiter une chaîne exactement comme un flux de données, ce qui peut se révéler très pratique puisque le même code peut analyser des chaînes et des données se trouvant dans un flux.
Exemples de mises en oeuvre au travers d'un éditeur

Nous allons démontrer les fonctionnalités de chacun de ces flux d'entrée de texte. Pour cela, nous allons fabriquer un tout petit éditeur qui affiche et qui édite le contenu d'un fichier texte. Nous utilisons sur l'interface graphique un JTextArea. Je rappelle que ce composant est tout-à-fait capable de récupérer le texte d'un fichier directement à partir de sa méthode read(). Toutefois, afin de bien valider le comportement de chacun de ces flux d'entrée de texte, nous proposerons, à la place de cette méthode native read(), tous les flux ainsi que toutes les procédures de lecture correspondant au choix effectué.

Editeur de fichier texte au travers d'un composant JTextArea

Code source de base au travers de la méthode read() de la classe JTextArea
package flux;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;

public class Edition extends JFrame {
   private JFileChooser sélection = new JFileChooser();
   private JToolBar boutons = new JToolBar("Choix du fichier texte");
   private JTextArea éditeur = new JTextArea(30, 60);
   private JLabel nomFichier = new JLabel();

   public Edition() {
      super("Editeur de fichiers");
      add(boutons, BorderLayout.NORTH);     
      add(new JScrollPane(éditeur));
      éditeur.setBackground(Color.YELLOW);
      boutons.add(new AbstractAction("Sélection du fichier") {
         public void actionPerformed(ActionEvent e) {
            if (sélection.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
                File fichier = sélection.getSelectedFile();
                nomFichier.setText(fichier.getPath());                
                try {
                   éditeur.read(new FileReader(fichier), null);
                } 
                catch (FileNotFoundException ex) {
                    nomFichier.setText("Fichier non trouvé");
                }
                catch (IOException ex) {
                    nomFichier.setText("Problème de lecture dans le fichier");
                }
            }
         }
      });
      boutons.addSeparator();
      boutons.add(nomFichier);
      pack();
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] args) { new Edition(); }
}
Même comportement, mais cette fois-ci au travers du flux d'entrée BufferedReader
...
      boutons.add(new AbstractAction("Sélection du fichier") {
         public void actionPerformed(ActionEvent e) {
            if (sélection.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
                File fichier = sélection.getSelectedFile();
                nomFichier.setText(fichier.getPath());                
                try {
                   BufferedReader lecture = new BufferedReader(new FileReader(fichier));
                   éditeur.setText("");
                   String ligne;
                   while ((ligne = lecture.readLine()) != null) éditeur.append(ligne+"\n");
                } 
                catch (FileNotFoundException ex) {
                    nomFichier.setText("Fichier non trouvé");
                }
                catch (IOException ex) {
                    nomFichier.setText("Problème de lecture dans le fichier");
                }
            }
         }
      });
...
Même comportement, mais cette fois-ci au travers du flux d'entrée LineNumberReader

...
boutons.add(new AbstractAction("Sélection du fichier") { public void actionPerformed(ActionEvent e) { if (sélection.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { File fichier = sélection.getSelectedFile(); nomFichier.setText(fichier.getPath()); try { LineNumberReader lecture = new LineNumberReader(new FileReader(fichier)); éditeur.setText(""); String ligne; while ((ligne = lecture.readLine()) != null) { int numéro = lecture.getLineNumber(); éditeur.append(numéro+" : "+ligne+"\n"); } } catch (FileNotFoundException ex) { nomFichier.setText("Fichier non trouvé"); } catch (IOException ex) { nomFichier.setText("Problème de lecture dans le fichier"); } } } }); ...
Même comportement, mais cette fois-ci au travers du flux d'entrée Scanner
...
      boutons.add(new AbstractAction("Sélection du fichier") {
         public void actionPerformed(ActionEvent e) {
            if (sélection.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
                File fichier = sélection.getSelectedFile();
                nomFichier.setText(fichier.getPath());                
                try {
                   Scanner lecture = new Scanner(fichier);
                   éditeur.setText("");
                   while (lecture.hasNextLine())  éditeur.append(lecture.nextLine()+"\n");
                } 
                catch (FileNotFoundException ex) {
                    nomFichier.setText("Fichier non trouvé");
                }
            }
         }
      });
...

Le passage par la classe Scanner simplifie considérablement le code :

  1. Nous pouvons construire un Scanner directement à partir d'un File sans passer par un FileReader intermédiaire.
  2. Pas besoin non plus d'utiliser une variable de type String qui récupère la ligne. Il existe en effet la méthode hasNextLine() qui contrôle si une nouvelle ligne est présente dans le flux, et si c'est le cas, vous récupérer cette ligne au travers de la méthode nextLine().
  3. De plus, nous avons besoin que d'un seul catch(...).
Ce programme ne gère pas la numérotation de ligne automatique. Dans le code précédent LineNumberReader remplaçait BufferedReader. Elle pouvait donc être intéressante pour intégrer, en plus de la lecture de la ligne en cours, sa numérotation. Dans le cas de la classe Scanner, il suffit juste de prévoir une variable supplémentaire qui comptabilise les lignes dèjà lues.
...
      boutons.add(new AbstractAction("Sélection du fichier") {
         public void actionPerformed(ActionEvent e) {
            if (sélection.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
                File fichier = sélection.getSelectedFile();
                nomFichier.setText(fichier.getPath());                
                try {
                   Scanner lecture = new Scanner(fichier);
                   éditeur.setText("");
                   int numéro = 0;
                   while (lecture.hasNextLine())  éditeur.append(++numéro+" : "+lecture.nextLine()+"\n");
                } 
                catch (FileNotFoundException ex) {
                    nomFichier.setText("Fichier non trouvé");
                }
            }
         }
      });
...

La classe Scanner

La classe Scanner est une classe très polyvalente. Sa spécialité, c'est la décomposition de texte. Certe, cette classe Scanner peut prendre en entrée des textes venant d'un fichier ou d'un flux quelconque, mais également et tout simplement des chaînes de caractères.

Si vous ne spécifiez pas de jeu de délimiteurs, le paramètre par défaut est " \t\n\r", à savoir tous les caractères d'espace vide (espace, tabulation, nouvelle ligne et retour chariot).
§

Un objet Scanner peut décomposer le texte qu'il reçoit en entrée en occurences séparées par un espace blanc ou par tout autre caractère de délimitation ou expression régulière. Scanner définit aussi une variété de méthodes utilitaires pour analyser les occurences sous forme de valeur booléenne, entières ou à virgule flottante, avec une analyse syntaxique des nombres prenant en compte les paramètres de localisation. Elle possède des méthodes skip() pour éviter les occurences correspondant à un motif spécifié, ainsi que des méthodes permettant de chercher vers l'avant des occurences correspondante à un motif spécifié.

La classe Scanner n'analyse pas seulement des littéraux entiers ou réels. Elle sait aussi reconnaître les séparateurs de milliers ainsi que le séparateur de la partie décimale conformément au pays concerné. Dans le cas de la France, nous pouvons donc avoir en entrée la valeur suivante : 1 234,45.

La classe java.util.Scanner
Scanner(File fichier)
Scanner(InputStream flux)
Construit un objet Scanner à partir du flux d'entrée. Le flux est alors interprété sous forme de flux de texte. Toutefois, cette classe est capable de décomposer du texte en élément. Ces éléments peuvent être de différentes natures. Ainsi par exemple, il est possible de récupérer une suite de caractères représentant une valeur entière directement transformée dans le type int.
Scanner(Readable chaîne)
Construit un objet Scanner à partir d'un flux de lecture quelconque, que ce soit un InputStream ou un Reader.
Scanner(String chaîne)
Construit un objet Scanner à partir cette fois-ci d'une chaîne de caractères qui sera alors possible de décomposer en éléments de nature différente.
void findInLine(String motif)
void
findInLine(Pattern motif)
Cherche, dans la ligne courante, du texte semblable à l'expression régulière spécifiée. Si une correspondance est trouvée, le Scanner avance jusqu'au texte et le retourne.
boolean hasNext()
Retourne true si un mot est présent dans la ligne courante. En réalité, Scanner découpe la chaîne de caractères d'entrée en plusieurs morceaux. Une série d'espaces blancs constitue le délimiteur par défaut. Nous pouvons spécifer un autre délimiteur au travers de la méthode useDelimiteur().

Si vous ne spécifiez pas de jeu de délimiteurs, le paramètre par défaut est " \t\n\r", à savoir tous les caractères d'espace vide (espace, tabulation, nouvelle ligne et retour chariot).

boolean hasNext(String motif)
boolean hasNext(Pattern motif)
Cette méthode est similaire à la précédente. Ici, la méthode accepte une expression régulière en argument et retourne true si les données en entrée sont semblables à cette expression.
boolean hasNextBoolean()
boolean hasNextByte()
boolean hasNextByte(int base)
boolean hasNextDouble()
boolean hasNextFloat()
boolean hasNextInt()
boolean hasNextInt(int base)
boolean hasNextLong()
boolean hasNextLong(int base)
boolean hasNextShort()
boolean hasNextShort(int base)
Contrôle si la prochaine valeur en entrée (suite de caractères) peut se transformer dans la valeur correspondant au type spécifié dans le nom de la méthode. Pour les types entiers, ces méthodes sont mêmes capables de déterminer si la suite de caractères proposée respecte la base spécifiée en argument.
boolean hasNextLine()
Contrôle si une ligne est présente. Cette fois-ci, il s'agit d'une chaîne de caractères qui se termine par une fin de ligne.
String next()
String next(String motif)
String next(Pattern motif)
Découpe les données en entrée en une série d'occurence String, séparés par des espaces blancs ou par le délimiteur spécifié par la méthode useDelimiter(). Chaque appel à next(), par défaut, retourne le mot pointé par le curseur courant. Il est également possible de travailler avec les expressions régulières pour récupérer la valeur correspond au motif établi.
boolean nextBoolean()
byte nextByte()
byte nextByte(int base)
double nextDouble()
float nextFloat()
int nextInt()
int nextInt(int base)
long nextLong()
long nextLong(int base)
short nextShort()
short nextShort(int base)
Interprète la chaîne de caractère présente dans la ligne courante et la tranforme dans le type correspondant à la méthode choisie. Si la tranformation se déroule correctement la méthode renvoie la valeur. Pour les types entiers, il est possible de travailler avec des valeurs numériques de base quelconque. La méthode retourne alors la valeur décimale équivalente.
String nextLine()
Retourne toute la chaîne jusqu'à la fin de ligne.
Scanner skip(String motif)
Scanner skip(Pattern motif)
Cette méthode ignore les délimiteurs et saute le texte correspondant à l'expression régulière spécifiée.
Scanner useDelimiter(String motif)
Scanner useDelimiter(Pattern motif)
Cette méthode spécifie une expression régulière qui représente le délimiteur d'occurence. Une série d'espace blancs constitue le délimiteur par défaut.

Si vous ne spécifiez pas de jeu de délimiteurs, le paramètre par défaut est " \t\n\r", à savoir tous les caractères d'espace vide (espace, tabulation, nouvelle ligne et retour chariot).

Scanner useLocale(Locale pays)
Spécifie le pays pour analyser les nombres. Cela peut influencer des éléments tels que les caractères utilisés pour le point décimal ou comme le séparateur de milliers.
Scanner useRadix(int base)
Spécifie la base sur laquelle les nombres devraient être analysés. Toutes les valeurs entre 2 et 36 sont autorisées.

Chaînes de caractères vers des flux et inversement

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

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.

StringWriter

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 cette 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    }

Entrée et sortie de texte avec des flux d'octets

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 proposent 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 attention 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é.

Ecriture et lecture dans un fichier

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. Ce fichier, lui, ne sera pas éditable. Le deuxième programme récupère cette série d'information afin de l'afficher ensuite à l'écran.

Sauvegarde et restitution de textes (formatés) dans un fichier non éditable

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 // Attention, l'écriture du réel reste en format américain. Il faudra en tenir compte lors de la lecture par la classe Scanner.
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 stocké 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.
§

Sauvegarder et restituer des objets enregistrés en format texte

A titre de conclusion, je vous propose de prendre un dernier exemple qui valide bien les fonctionnalités des flux de texte, en sortie comme en entrée. Nous allons mettre en oeuvre une petite application graphique qui réalise du tracé de formes (Cercle, Carré, etc.). Ces formes seront introduites, après sélection de son type et de sa largeur, au moyen d'un clic de la souris sur la zone d'édition. Une fois que le tracé est réalisé, il est possible d'enregistrer l'ensemble du dessin dans un fichier au format texte.

Les formes sont en réalité des objets. Chaque objet sera inscrit dans une ligne à part entière dans le fichier texte. Le début de la ligne stipulera le type de la forme. Les informations écrites ensuites sur la même ligne précisera la valeur des attributs de l'objet. Chacune de ces informations atomiques seront séparées par le symbole ":".

Codage correspondant
package flux;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.*;

public class Formes extends JFrame {
   private JRadioButton cercle = new JRadioButton("Cercle", true);
   private JRadioButton carré = new JRadioButton("Carré");
   private JFormattedTextField largeur = new JFormattedTextField(50);
   private ButtonGroup groupe = new ButtonGroup();
   private JPanel boutons = new JPanel();
   private Panneau panneau = new Panneau();
   private JToolBar barre = new JToolBar();

   public Formes() {
      super("Formes");
      largeur.setColumns(3);
      add(barre, BorderLayout.NORTH);
      barre.add(new AbstractAction("Nouveau", new ImageIcon("nouveau.gif")) {
         public void actionPerformed(ActionEvent e) {
            panneau.effacer();
         }
      });
      barre.add(new AbstractAction("Ouvrir", new ImageIcon("ouvrir.gif")) {
         public void actionPerformed(ActionEvent e) {
            panneau.ouvrir();
         }
      });
      barre.add(new AbstractAction("Enregistrer", new ImageIcon("enregistrer.gif")) {
         public void actionPerformed(ActionEvent e) {
            panneau.enregistrer();
         }
      });
      panneau.addMouseListener(new MouseAdapter() {
         @Override
         public void mouseClicked(MouseEvent e) {
            int dimension = (Integer)largeur.getValue();
            if (cercle.isSelected()) panneau.ajoutForme(new Cercle(e.getX(), e.getY(), dimension));
            else  panneau.ajoutForme(new Carré(e.getX(), e.getY(), dimension));
         }
      });
      add(panneau);
      boutons.add(cercle);
      boutons.add(carré);
      boutons.add(largeur);
      groupe.add(cercle);
      groupe.add(carré);
      add(boutons, BorderLayout.SOUTH);
      setSize(400, 300);
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   public static void main(String[] args) { new Formes(); }

   abstract class Forme {
     protected int x, y;
     public Forme() {}
     public Forme(int x, int y) { this.x = x;  this.y = y; }
     public void déplace(int dx, int dy) { x += dx; y += dy; }
     abstract public void affiche(Graphics g);
     abstract public int getDimension();
     public int getX() { return x; }
     public int getY() { return y; }
   }

   class Cercle extends Forme {
     private int rayon = 50;

     public Cercle(int x, int y, int r) { super(x, y);  rayon = r; }
     public Cercle(int x, int y) { super(x, y); }
     public Cercle() {}
     public int getDimension() { return rayon; }
     public void affiche(Graphics g) { g.drawOval(x-rayon, y-rayon, 2*rayon, 2*rayon); }
   }

   class Carré extends Forme {
     private int côté = 100;

     public Carré(int x, int y, int c) { super(x, y);  côté = c; }
     public Carré(int x, int y) { super(x, y); }
     public Carré() {}
     public int getDimension() { return côté; }
     public void affiche(Graphics g) { g.drawRect(x-côté/2, y-côté/2, côté, côté); }
   }

   class Panneau extends JComponent {
     private ArrayList<Forme> formes = new ArrayList<Forme>();

     @Override
     protected void paintComponent(Graphics g) {
        Graphics2D surface = (Graphics2D) g;
        surface.setStroke(new BasicStroke(5));
        surface.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        for (Forme forme : formes) forme.affiche(g);
     }

     public void ajoutForme(Forme forme) {
        formes.add(forme);
        repaint();
     }

     public void effacer() {
        formes.clear();
        revalidate();
        repaint();
     }

     public void enregistrer() {
       try {
          PrintWriter écrire = new PrintWriter("dessins.sto");
          for (Forme f : formes)
             écrire.printf("%s:%d:%d:%d\n", f.getClass().getSimpleName(), f.getX(), f.getY(), f.getDimension());
          écrire.close();
       }
       catch (FileNotFoundException ex) { setTitle("Fichier inexistant");  }
     }

     public void ouvrir() {
       try {
         Scanner lecture = new Scanner(new FileReader("dessins.sto"));
         formes.clear();
         while (lecture.hasNextLine()) {
            Scanner ligne = new Scanner(lecture.nextLine());
            ligne.useDelimiter(":");
            if (ligne.next().equals("Cercle")) formes.add(new Cercle(ligne.nextInt(), ligne.nextInt(), ligne.nextInt()));
            else formes.add(new Carré(ligne.nextInt(), ligne.nextInt(), ligne.nextInt()));
         }
         repaint();
       } 
       catch (FileNotFoundException ex)   { setTitle("Fichier inexistant");  }
     }
   }
}

Nous retrouvons les deux principales classes pour la gestion des flux de texte en sortie et en entrée, savoir PrintWriter et Scanner. De plus, dans ce projet, j'utilise deux fois la classe Scanner, la première pour récupérer chaque ligne du fichier et la seconde pour récupérer chaque élément de la ligne en prenant en compte l'opérateur de séparation ":".

 

Choix du chapitre Flux de données

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.

En réalité, ces classes implémentent les interfaces respectives DataInput et DataOutput qui spécifient les méthodes de traitement sur les nombres, les caractères, les valeurs booléennes, et même les chaînes de caractères (en format binaire).

Par exemple, la méthode writeInt() écrit toujours un entier sur 4 octets consécutifs, quel que soit le nombre de chiffres, et la méthode writeDouble() écrit toujours un réel avec 8 octets consécutifs. Les sorties binaires qui en résultent ne sont évidemment pas directement lisibles par l'homme, mais l'espace nécessaire en mémoire ou sur le disque sera le même pour une valeur d'un type donné, optimisant ainsi les entrées/sorties. C'est notamment beaucoup plus rapide que le transcodage supplémentaire d'une chaîne vers son équivalent numérique.

La méthode writeUTF() écrit les données des chaînes en utilisant une version modifiée au format UTF 8 bits (Unicode Transformation Format). Au lieu de simplement utiliser le codage standard UTF-8, les chaînes de caractères sont d'abord représentées au format UTF-16, puis le résultat est encodé à l'aide des règles UTF-8. L'encodage modifié diffère pour les caractères ayant un code supérieur à 0xFFFF. Il est utilisé pour une compatibilité en amont avec les machines virtuelles qui ont été construites lorsque l'Unicode n'allait pas au delà de 16 bits.

Personne d'autre n'utilisant UTF-8 modifié, n'employez la méthode writeUTF() que pour écrire des chaînes destinées à une même machine virtuelle Java, par exemple si vous écrivez un programme qui génère des bytecodes. Vous utiliserez la méthode writeChars() dans tous les autres cas.

Le format de données binaires est compact et indépendant des plate-formes. Il convient à l'accès direct, sauf pour les chaînes UTF. Le seul inconvénient - majeur - des fichiers binaires est que l'oeil humain ne peut les lires.

  1. La classe DataInputStream implémente l'interface DataInput. Ainsi, pour lire des données binaires à partir d'un fichier, il faut associer un flux DataInputStream avec un FileInputStream, comme suit :

    DataInputStream lecture = new DataInputStream(new FileInputStream("sauvegarde.sto"));

  2. De façon identique, pour écrire des données binaires, vous utiliserez la classe DataOutputStream qui implémente l'interface DataOutput :

    DataOutputStream écriture = new DataOutputStream(new FileIOutputStream("sauvegarde.sto"));

L'interface java.io.DataInput
boolean readBoolean()
byte readtByte()
char readChar()
double readDouble()
float readFloat()
int readInt()
long readLong()
short readShort()
Récupère la valeur dans le format spécifié par la méthode.
void readFully(byte[] octets)
Lit des octets en attendant que tous les octets soient lus.
void readFully(byte[] octets, int offset, int taille)
Lit des octets dans le tableau octets, bloquant tout jusqu'à ce que tous les octets soient lus.
String readUTF()
Lit une chaîne de caractères au format UTF-8 modifié.
int skipBytes(int nombre)
Saute nombre octets et reste en attente jusqu'à ce que tous les octets soient sautés.
L'interface java.io.DataOutput
void writeBoolean(boolean valeur)
void writeByte(byte valeur)
void writeChar(char valeur)
void writeDouble(double valeur)
void writeFloat(float valeur)
void writeInt(int valeur)
void writeLong(long valeur)
void writeShort(short valeur)
Ecrit la valeur dans le format spécifié par la méthode.
void writeChars(String chaîne)
Ecrit l'ensemble des caractères dans une chaîne.
void writeUTF(String chaîne)
Ecrit une chaîne de caractères au format UTF-8 modifié.

Sauvegarder et restituer des objets enregistrés en format binaire

A titre de conclusion, je vous propose de reprendre l'application précédente. Cette fois-ci toutefois, l'enregistrement des objets ne se fera plus en format texte, mais plutôt au travers de données primitives.

La partie principale de l'application demeure totalement identique. Seules les phases d'enregistrement et de lecture du fichier sont à revoir.

Partie de code correspondant
...
     public void enregistrer() {
       try {
          DataOutputStream écrire = new DataOutputStream(new FileOutputStream("dessins.sto"));
          écrire.writeInt(formes.size()); // précise le nombre de formes sauvegardées
          for (Forme forme : formes) {
             écrire.writeUTF(forme.getClass().getSimpleName());
             écrire.writeInt(forme.getX());
             écrire.writeInt(forme.getY());
             écrire.writeInt(forme.getDimension());
          }
          écrire.close();
       }
       catch (FileNotFoundException ex) { setTitle("Fichier inexistant");  }
       catch (IOException ex) { setTitle("Problèmes pour enregistrer les données"); }
     }

     public void ouvrir() {
       try {
         DataInputStream lecture = new DataInputStream(new FileInputStream("dessins.sto"));
         formes.clear();
         int nombreForme = lecture.readInt();
         for (int i=0; i<nombreForme; i++) {
            String typeForme = lecture.readUTF();
            int x = lecture.readInt();
            int y = lecture.readInt();
            int dimension = lecture.readInt();
            if (typeForme.toString().equals("Cercle")) formes.add(new Cercle(x, y, dimension));
            else  formes.add(new Carré(x, y, dimension));
         }
         lecture.close();
         repaint();
       }
       catch (FileNotFoundException ex)   { setTitle("Fichier inexistant");  }
       catch (IOException ex) { setTitle("Problèmes pour récuperer les données stockées dans le fichier"); }
     }
   }
}

 

Choix du chapitreFlux de fichiers en accès direct

La classe de flux RandomAccessFile permet de chercher ou d'écrire des données depuis n'importe quel emplacement d'un fichier. Elle implémente les deux interfaces DataInput et DataOutput.

Les fichiers sur disque sont en accès direct, mais le flux de données provenant d'un réseau ne le sont pas.
§

Nous ouvrons un fichier à accès direct soit en lecture seule, soit en lecture-écriture. Cette option est précisée par l'une des chaînes suivante que nous passons au constructeur comme second argument :
  1. "r" (en lecture seule)
  2. "rw" (en lecture-écriture ou read-write)

RandomAccessFile lecture = new RandomAccessFile("direct.sto", "r");
RandomAccessFile lectureEcriture = new RandomAccessFile("direct.sto", "rw");

Un fichier existant ouvert en accès direct en tant que RandomAccessFile n'est pas écrasé.
§

  1. Un fichier en accès direct possède, de plus, un pointeur de fichier qui indique constamment la position de l'enregistrement suivant (celui qui sera lu ou écrit). La méthode seek() place le pointeur de fichier sur un octet ayant une position arbitraire dans le fichier. L'argument passé à seek() est un entier long, compris entre 0 et la longueur du fichier exprimée en octet.
  2. La méthode getFilePointer() renvoie la position courante du pointeur de fichier.
En réalité, la classe RandomAccessFile implémente les deux interfaces que nous venons d'étudier dans le chapitre précédent, DataInput et DataOutput. Je rappelle que ces deux interfaces proposent des méthodes fortes intéressantes pour lire et écrire directement avec des données primitives comme les entiers, les réels, les caractères, etc. Nous nous retrouvons donc avec les mêmes méthodes que pour la classe DataInputStream et DataOutputStream, comme readInt()/writeInt(), readDouble()/writeDouble(), etc.

L'intérêt de la classe RandomAccessFile est qu'elle implément simultanément DataInput et DataOutput, ce qui permet d'utiliser des méthodes (pour lire et pour écrire) dont les types d'arguments sont ceux des interfaces DataInput et DataOutput. Ainsi par exemple, lorsqu'une méthode attend un DataInput, nous pouvons tout aussi bien choisir un DataInputStream qu'un RandomAccessFile.


La classe java.io.RandomAccessFile
RandomAccessFile(String nomFichier, String modeOuverture)
RandomAccessFile(File fichier, String modeOuverture)
Crée un flux à accès direct en spécifiant soit le nom du fichier où soit le fichier directement en précisant le mode d'ouverture : "r" ou "rw".
long getFilePointer()
Renvoie la position courante du pointeur de fichier.
void seek(long position)
Fixe la position du pointeur de fichier à position (nombre d'octets à partir du début du fichier).
long length()
Renvoie la longueur du fichier en octets.

Changer un objet de type Forme sauvegardé en format binaire à un endroit spécifique du fichier

Nous allons reprendre l'application précédente dans laquelle nous allons rajouter de nouvelles fonctionnalités. Effectivement, je vous propose de permettre la modification d'une forme déjà introduite. Cette modification est alors prise en compte instantanément dans le fichier directement à l'endroit où elle été déjà enregistrée.

Quelques petites modifications supplémentaires ont été apportées. D'une part, vous remarquez que le bouton d'enregistrement n'existe plus. Effectivement à chaque introduction d'une nouvelle forme, elle est automatiquement enregistrée dans le fichier. A ce sujet, j'ai conservé l'ancienne façon d'enregistrer, avec donc un DataOutputStream, pour bien montrer que cette classe est tout-à-fait compatible avec un RandomAccessFile. Par ailleurs, j'ai rajouté une case à cocher pour permettre le mode modification ainsi qu'une liste déroulante pour sélectionner la forme à rééditer.


Codage correspondant
package flux;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

public class Formes extends JFrame {
   private JRadioButton cercle = new JRadioButton("Cercle", true);
   private JRadioButton carré = new JRadioButton("Carré");
   private JFormattedTextField largeur = new JFormattedTextField(50);
   private ButtonGroup groupe = new ButtonGroup();
   private JPanel boutons = new JPanel();
   private Panneau panneau = new Panneau();
   private JToolBar barre = new JToolBar();
   private JCheckBox modification = new JCheckBox("Modification");
   private JComboBox choix = new JComboBox();

   public Formes() {
      super("Formes");
      largeur.setColumns(3);
      add(barre, BorderLayout.NORTH);
      barre.add(new AbstractAction("Nouveau", new ImageIcon("nouveau.gif")) {
         public void actionPerformed(ActionEvent e) {
            panneau.effacer();
         }
      });
      barre.add(new AbstractAction("Ouvrir", new ImageIcon("ouvrir.gif")) {
         public void actionPerformed(ActionEvent e) {
            panneau.ouvrir();
         }
      });
      barre.add(modification);
      barre.add(choix);
      ActionListener rafraîchir = new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            if (choix.getItemCount()>0) panneau.rafraîchir();
         }
      };
      modification.addActionListener(rafraîchir);      
      choix.addActionListener(rafraîchir);
      ActionListener changerForme = new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            if (modification.isSelected()) panneau.changerForme();
         }
      };
      cercle.addActionListener(changerForme);
      carré.addActionListener(changerForme);
      largeur.addActionListener(changerForme);
      panneau.addMouseListener(new MouseAdapter() {
         @Override
         public void mouseClicked(MouseEvent e) {
            int dimension = (Integer)largeur.getValue();
            if (cercle.isSelected()) panneau.gérerForme(new Cercle(e.getX(), e.getY(), dimension));
            else  panneau.gérerForme(new Carré(e.getX(), e.getY(), dimension));
         }
      });
      add(panneau);
      boutons.add(cercle);
      boutons.add(carré);
      boutons.add(largeur);
      groupe.add(cercle);
      groupe.add(carré);
      add(boutons, BorderLayout.SOUTH);
      setSize(400, 300);
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] args) { new Formes(); }

   abstract class Forme { ... }

   class Cercle extends Forme { ... }

   class Carré extends Forme { ... }

   class Panneau extends JComponent {
     private ArrayList<Forme> formes = new ArrayList<Forme>();

     @Override
     protected void paintComponent(Graphics g) {
        Graphics2D surface = (Graphics2D) g;
        surface.setStroke(new BasicStroke(5));
        surface.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        for (Forme forme : formes) forme.affiche(g);
        if (modification.isSelected()) {
           surface.setColor(Color.RED);
           formes.get(choix.getSelectedIndex()).affiche(g);
        }
     }

     public void gérerForme(Forme forme) {
        if (modification.isSelected()) {
            formes.set(choix.getSelectedIndex(), forme);     
            modifier(forme);
        }
        else {
           formes.add(forme);
           enregistrer();
        }        
        repaint();
     }
     
     public void rafraîchir() {
         Forme forme = formes.get(choix.getSelectedIndex());
         if (forme.getClass().getSimpleName().equals("Cercle")) cercle.setSelected(true);
         else carré.setSelected(true);
         largeur.setValue(forme.getDimension());
         repaint();       
     }

     public void changerForme() {
         Forme forme = formes.get(choix.getSelectedIndex());    
         int x = forme.getX();
         int y = forme.getY();
         int dimension = (Integer)largeur.getValue();
         if (cercle.isSelected()) forme = new Cercle(x, y, dimension);
         else forme = new Carré(x, y, dimension);
         formes.set(choix.getSelectedIndex(), forme);
         modifier(forme);
         repaint();
     }

     public void effacer() {
        formes.clear();
        choix.removeAllItems();
        modification.setSelected(false);
        revalidate();
        repaint();
     }

     public void enregistrer() {
       try {
          DataOutputStream écrire = new DataOutputStream(new FileOutputStream("dessins.sto"));
          écrire.writeInt(formes.size());
          for (Forme forme : formes) {
             écrireChaîneFixe(écrire, forme.getClass().getSimpleName());
             écrire.writeInt(forme.getX());
             écrire.writeInt(forme.getY());
             écrire.writeInt(forme.getDimension());
          }
          écrire.close();
       }
       catch (FileNotFoundException ex) { setTitle("Fichier inexistant");  }
       catch (IOException ex) { setTitle("Problèmes pour enregistrer les données"); }
     }

     public void ouvrir() {
       try {
         DataInputStream lecture = new DataInputStream(new FileInputStream("dessins.sto"));
         formes.clear();
         choix.removeAllItems();
         modification.setSelected(false);
         int nombreForme = lecture.readInt();
         for (int i=0; i<nombreForme; i++) {
            StringBuilder type = new StringBuilder(7);
            for (int c=0; c<7; c++) {
               char ch = lecture.readChar();
               if (ch != 0) type.append(ch);
            }
            int x = lecture.readInt();
            int y = lecture.readInt();
            int dimension = lecture.readInt();
            if (type.toString().equals("Cercle")) formes.add(new Cercle(x, y, dimension));
            else  formes.add(new Carré(x, y, dimension));
            choix.addItem(type.toString()+" ("+x+", "+y+")");
         }
         lecture.close();
         repaint();
       }
       catch (FileNotFoundException ex)   { setTitle("Fichier inexistant");  }
       catch (IOException ex) { setTitle("Problèmes pour récuperer les données stockées dans le fichier"); }
     }
     
     public void modifier(Forme forme) {
         try {
            RandomAccessFile écrire = new RandomAccessFile("dessins.sto", "rw");
            final int TAILLE_FORME = 2*7+4+4+4;
            écrire.seek(4+choix.getSelectedIndex()*TAILLE_FORME);
            écrireChaîneFixe(écrire, forme.getClass().getSimpleName());
            écrire.writeInt(forme.getX());
            écrire.writeInt(forme.getY());
            écrire.writeInt(forme.getDimension());
            écrire.close();
         }
         catch (FileNotFoundException ex) { setTitle("Fichier inexistant"); }
         catch (IOException ex) { setTitle("Problèmes pour enregistrer les données"); }
     }
     
     private void écrireChaîneFixe(DataOutput écrire, String type) throws IOException {
        for (int i=0; i<7; i++) {
           char ch = 0;
           if (i < type.length()) ch = type.charAt(i);
           écrire.writeChar(ch);
        }
     }
   }
}

Personnellement, je trouve que l'utilisation de ce type de flux est plus compliqué à gérer que tout ce que nous venons de voir. Effectivement, vous êtes obligés d'avoir une structure rigide au niveau de l'enregistrement de vos données. Il est impératif d'avoir une taille identique pour chaque élément que vous introduisez. Dans notre exemple, je suis dans l'obligation de gérer ma chaîne de caractères, qui évoque le type de forme, avec une taille bien précise, en proposant un enregistrement caractère par caractère. Sauf pour les très gros fichiers, je préfère faire une gestion de modification en mémoire et d'enregistrer ensuite tout le contenu du fichier à l'aide de la classe DataOutputStream.

 

Choix du chapitre Compression et décompression des données

Les fichiers ZIP sont des archives contenant un ou plusieurs fichiers dans un format en principe compressé. Un fichier ZIP possède un en-tête comprenant un certain nombre d'informations comme le nom du fichier et la méthode de compression utilisée.

Décompression des données

  1. Nous lisons en Java un fichier ZIP à l'aide de l'objet ZipInputStream en empilant le constructeur ZipInputStream sur un FileInputStream.

    Si vous désirez récupérer des données compressées sur le réseau, vous devez employer un InputStream donné par la socket en lieu et place du flux de fichier FileInputStream.

  2. Normalement, un fichier ZIP regroupe un ensemble de fichiers. Nous pouvons accéder à chaque entrée individuelle de l'archive représentant chacun de ces fichiers. Ainsi, pour lire dans un ZiptInputStream, vous devez d'abord appeler la méthode getNextEntry() qui renvoie un objet ZipEntry qui représente le fichier compressé. Lorsque getNextEntry() renvoie null, il ne reste plus d'élément à lire.
  3. La méthode read() de ZipInputStream a été modifiée pour renvoyer -1, non pas à la fin du fichier ZIP, mais à la fin de l'entrée courante. ll est, par conséquent, obligatoire d'appeler la méthode closeEntry() pour pouvoir passer à l'entrée suivante.
  4. Pour lire le contenu d'une entrée ZIP, nous n'utiliserons généralement pas la méthode read() sans formatage. Nous nous servirons en principe des méthodes d'un flux filtré mieux approprié. Par exemple, nous prendrons la classe Scanner pour lire un fichier texte se trouvant dans un fichier ZIP.
    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();
        }
    }

    Le flux d'entrée ZIP lance une exception ZipException quand une erreur de lecture au niveau d'un fichier ZIP se produit. Cette erreur apparaît normalement lorsque le fichier ZIP est corrompu.

Comprimer les données

  1. Pour écrire un fichier ZIP, il faut d'abord ouvrir un flux ZipOutputStream en l'empilant sur un FileOutputStream.
  2. Un objet 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.
  3. Il faut ensuite appeler la méthode putNextEntry() du flux ZipOutputStream pour commencer à écrire dans un nouveau fichier.
  4. Les données du fichier sont alors à envoyer dans le flux ZIP, après quoi il faut appeler la méthode closeEntry().
  5. Tout cela est à répéter pour chacun des fichiers que l'on veut archiver.
    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();        
        }
    }

Conclusion

Les fichiers ZIP illustrent bien la puissance d'abstraction d'un flux. Aussi bien la source que la destination de données sont totalement modifiables. Vous empilez l'objet Reader le mieux approprié sur un flux de fichiers ZIP pour lire les données se trouvant sous une forme compressée. L'objet Reader ne sait pas que les données sont décompressées lorsque nous l'activons.

De plus, la source d'octets au format ZIP n'est pas nécessairement un fichier : les données peuvent provenir d'une connexion réseau. De même, lorsque le chargeur de classes d'un applet lit un fichier JAR, il lit et décompresse des données provenant du réseau.

Les fichier JAR sont tout simplement des fichiers ZIP possédant une entrée appelée manifeste. Nous pouvons lire le manifeste avec les classes JarInputStream et JarOutputStream.
§

La classe java.util.zip.ZipInputStream
ZipInputStream(InputStream flux)
Crée un flux de type ZipInputStream qui permet de décompresser les données se trouvant dans le flux InputStream passé en paramètre.
ZipEntry getNextEntry()
Renvoie un objet ZipEntry pour l'entrée suivante ou null s'il n'existe plus.
void closeEntry()
Ferme l'entrée courante du fichier ZIP. Il est alors possible de lire l'entrée suivante en se servant de getNextEntry().
La classe java.util.zip.ZipOutputStream
ZipOutputStream(OutputStream flux)
Crée un flux de type ZipOutputStream qui permettant d'écrire des données compressées dans le flux spécifié de type OutputStream passé en paramètre.
void putNextEntry(ZipEntry nouvelleEntrée)
Ecrit l'information dans le flux ZipEntry et prépare le flux à accepter les données. Les données seront écrites dans le flux par write().
void closeEntry()
Ferme l'entrée courante du fichier ZIP. Nous nous servons de la méthode putNextEntry() pour passer à l'entrée suivante.
void setLevel(int niveau)
Fixe le niveau de compression par défaut des entrées DEFLATED (valeur par défaut : Deflater.DEFAULT_COMPRESSION). Lance une exception IllegalArgumentException si la valeur du niveau n'est pas correcte. niveau est compris entre 0 (NO_COMPRESSION : pas de compression) et 9 (BEST_COMPRESSION : compression maximale).
void setMethod(int méthode)
Spécifie la méthode de compression par défaut pour le flux ZipOutputStream courant, et ce pour toute entrée ne spécifiant pas de méthode (DEFLATED ou STORED).
La classe java.util.zip.ZipEntry
ZipEntry(String nom)
Spécifie l'entrée associé au nom proposé en argument.
long getCrc()
Renvoie la valeur de contrôle CRC32 pour la ZipEntry traitée.
String getName()
Renvoie le nom de l'entrée traitée.
long getSize()
Renvoie la taille non compressée de l'entrée traitée ou -1 si cette taille n'est pas connue.
boolean isDirectory()
Renvoie un booléen pour préciser si l'entrée traitée est un répertoire.
void setMethod(int méthode)
Spécifie la méthode de compression pour l'entrée traitée (DEFLATED ou STORED).
void setSize(long taille)
Fixe la taille de l'entrée. Seulement nécessaire pour la méthode de compression STORED.
void setCrc(long crc)
Fixe la somme de contrôle CRC32 de cette entrée. La classe CRC32 permet de calculer la somme de contrôle. Seulement nécessaire pour la méthode de compression STORED.
La classe java.util.zip.ZipFile
ZipFile(String nom)
ZipFile(File fichier)
Le constructeur courant crée un ZipFile pour lire à partir de la source spécifié.
Enumeration entries()
Renvoie un objet Enumeration donnant les objets ZipEntry qui décrivent les entrées du ZipFile.
ZipEntry getEntry(String nom)
Renvoie l'entrée dont le nom est passé en argument ou null s'il n'existe pas d'entrée de ce nom.
InputStream getInputStream(ZipEntry entrée)
Renvoie l'InputStream pour une entrée donnée.
String getName()
Renvoie le chemin du fichier ZIP traité.

 

Choix du chapitre Flux d'objets

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é, etc.

Normalement, pour enregistrer dans des fichiers ce type d'information, et comme nous l'avons déjà mis en oeuvre plus haut dans cette étude, il faudrait commencer par enregistrer le type de chaque objet, puis les attributs donnant l'état courant de l'objet. Inversement, à la relecture du fichier, il faudra :
  1. lire le type de l'objet ;
  2. créer un objet vide de ce type ;
  3. y placer les données provenant du fichier.

Il est tout à fait possible, mais très fastidieux, de faire cela à la main. Heureusement, il existe un mécanisme très puissant, appelé "sérialisation des objets" qui travaille à votre place en automatisant presque complètement le processus précédent.

Processus de mise en oeuvre

  1. Pour enregistrer les données, il faut au préalable ouvrir un objet ObjectOutputStream :

    ObjectOutputStream écrire = new ObjectOutputStream(new FileOutputStream("dessins.sto"));

  2. Puis pour enregistrer l'objet, nous utilisons la méthode writeObject() de la classe ObjectOutputStream :

    écrire.writeObject(new Cercle(10, 25, 100));
    écrire.writeObject(new Carré(75, 12, 48));

  3. Pour relire les objets, nous commençons par instancier un objet ObjectInputStream :

    ObjectInputStream lire = new ObjectInputStream(new FileInputStream("dessins.sto"));

  4. Puis, nous lisons les objets dans l'ordre dans lequel ils ont été écrits à l'aide de la méthode readObject() :

    Cercle cercle = (Cercle) lire.readObject();
    Carré carré = (Carré) lire.readObject();

    Il faut, lorsque nous rechargeons 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 est alors nécessaire de transtyper dans son type exact.

    Si le véritable type de l'objet n'est pas connu ou n'est pas utile, il suffit de le transtyper dans le type d'une quelconque de ses superclasses, ou même de le laisser dans le type Object. Il est possible d'obtenir dynamiquement le type d'un objet à l'aide de la méthode getClass().

  5. Nous ne pouvons écrire et lire que des objets avec les méthodes writeObject() et readObject(), pas des nombres. Pour des valeurs de type primitif, nous utiliserons des méthodes comme writeInt() et readInt() ou writeDouble() et readDouble().

    En réalité, les classes de flux d'objet implémentent les interfaces DataInput et DataOutput au travers des interfaces plus spécifiques ObjectInput et ObjectOutput.

    Bien évidemment, les valeur numériques se trouvant à l'intérieur des objets (x, y et rayon d'un objet de type Cercle par exemple) sont enregistrées et rechargées automatiquement.
    §

  6. Les chaînes et les tableaux, et plus généralement les collections, qui sont en Java des objets, relèvent par conséquent des méthodes writeObject() et readObject().

La sérialisation

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


La classe java.io.ObjectOutputStream
ObjectOutputStream(OutputStream flux)
Crée un flux de type ObjectOutputStream pour permettre d'écrire des objets dans le flux spécifié.
void writeObject(Object objet)
Ecrit l'objet spécifié dans un flux ObjectOutputStream. Sont écrites la classe de l'objet, la signature de la classe, la valeur de tous ses attributs non marqués transient ainsi que les attributs non statiques de toutes les superclasses.
La classe java.io.ObjectInputStream
ObjectInputStream(InputStream flux)
Crée un flux de type ObjectInputStream pour relire l'information sur l'objet issu du flux spécifié.
Object readObject()
Lit un objet spécifié dans le flux ObjectInputStream. Cette relit, entre autres, la classe de l'objet, la signature de la classe, la valeur de tous ses attributs non marqués transient ainsi que les attributs non statiques de toutes les superclasses. Elle effectue la désérialisation pour permettre la récupération des références multiples à un objet.

Sauvegarder et restituer des objets de type Forme

Nous allons reprendre notre petite application graphique qui réalise du tracé de formes (Cercle, Carré, etc.). Je rappelle que ces formes sont introduites, après sélection de son type et de sa largeur, au moyen d'un clic de la souris sur la zone d'édition. Une fois que le tracé est réalisé, il est possible de l'enregistrer dans un fichier en sauvegardant directement chacun des objets graphiques.

Les formes étant des objets, il est donc possible de les enregistrer directement dans l'ordre dans lequel ils ont été introduits. La seule condition, c'est qu'ils soient sérialisables. Il faut aussi penser à dénombrer l'ensemble des éléments introduits.

Codage correspondant
package flux;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.*;

public class Formes extends JFrame {
   private JRadioButton cercle = new JRadioButton("Cercle", true);
   private JRadioButton carré = new JRadioButton("Carré");
   private JFormattedTextField largeur = new JFormattedTextField(50);
   private ButtonGroup groupe = new ButtonGroup();
   private JPanel boutons = new JPanel();
   private Panneau panneau = new Panneau();
   private JToolBar barre = new JToolBar();

   public Formes() {
      super("Formes");
      largeur.setColumns(3);
      add(barre, BorderLayout.NORTH);
      barre.add(new AbstractAction("Nouveau", new ImageIcon("nouveau.gif")) {
         public void actionPerformed(ActionEvent e) {
            panneau.effacer();
         }
      });
      barre.add(new AbstractAction("Ouvrir", new ImageIcon("ouvrir.gif")) {
         public void actionPerformed(ActionEvent e) {
            panneau.ouvrir();
         }
      });
      barre.add(new AbstractAction("Enregistrer", new ImageIcon("enregistrer.gif")) {
         public void actionPerformed(ActionEvent e) {
            panneau.enregistrer();
         }
      });
      panneau.addMouseListener(new MouseAdapter() {
         @Override
         public void mouseClicked(MouseEvent e) {
            int dimension = (Integer)largeur.getValue();
            if (cercle.isSelected()) panneau.ajoutForme(new Cercle(e.getX(), e.getY(), dimension));
            else  panneau.ajoutForme(new Carré(e.getX(), e.getY(), dimension));
         }
      });
      add(panneau);
      boutons.add(cercle);
      boutons.add(carré);
      boutons.add(largeur);
      groupe.add(cercle);
      groupe.add(carré);
      add(boutons, BorderLayout.SOUTH);
      setSize(400, 300);
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   public static void main(String[] args) { new Formes(); }

   abstract class Forme implements Serializable { // changement important à réaliser pour que toute la hiérarchie de forme soit sérialisable
     protected int x, y;
     public Forme() {}
     public Forme(int x, int y) { this.x = x;  this.y = y; }
     public void déplace(int dx, int dy) { x += dx; y += dy; }
     abstract public void affiche(Graphics g);
     abstract public int getDimension();
     public int getX() { return x; }
     public int getY() { return y; }
   }

   class Cercle extends Forme {
     private int rayon = 50;

     public Cercle(int x, int y, int r) { super(x, y);  rayon = r; }
     public Cercle(int x, int y) { super(x, y); }
     public Cercle() {}
     public int getDimension() { return rayon; }
     public void affiche(Graphics g) { g.drawOval(x-rayon, y-rayon, 2*rayon, 2*rayon); }
   }

   class Carré extends Forme {
     private int côté = 100;

     public Carré(int x, int y, int c) { super(x, y);  côté = c; }
     public Carré(int x, int y) { super(x, y); }
     public Carré() {}
     public int getDimension() { return côté; }
     public void affiche(Graphics g) { g.drawRect(x-côté/2, y-côté/2, côté, côté); }
   }

   class Panneau extends JComponent {
     private ArrayList<Forme> formes = new ArrayList<Forme>();

     @Override
     protected void paintComponent(Graphics g) {
        Graphics2D surface = (Graphics2D) g;
        surface.setStroke(new BasicStroke(5));
        surface.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        for (Forme forme : formes) forme.affiche(g);
     }

     public void ajoutForme(Forme forme) {
        formes.add(forme);
        repaint();
     }

     public void effacer() {
        formes.clear();
        revalidate();
        repaint();
     }

     public void enregistrer() {
       try {
          ObjectOutputStream écrire = new ObjectOutputStream(new FileOutputStream("dessins.sto"));
          écrire.writeInt(formes.size());
          for (Forme forme : formes) écrire.writeObject(forme);
          écrire.close();
       }
       catch (FileNotFoundException ex) { setTitle("Fichier inexistant");  }
       catch (IOException ex) { setTitle("Problème de sauvegarde");  }
     }

     public void ouvrir() {
       try {
         ObjectInputStream lire = new ObjectInputStream(new FileInputStream("dessins.sto"));
         formes.clear();
         int nombre = lire.readInt();
         for (int i=0; i<nombre; i++) formes.add((Forme)lire.readObject());
         lire.close();
         repaint();
       }
       catch (FileNotFoundException ex)   { setTitle("Fichier inexistant");  }
       catch (ClassNotFoundException ex) { setTitle("Problème de sauvegarde");  }
       catch (IOException ex) { setTitle("Problème de lecture du fichier");  }
     }
  }
}
Comme les collections sont également des objets, au lieu d'enregistrer chaque objet séparément, il peut être judicieux de sauvegarder directement la collection en entier, sans se préoccuper du nombre d'objets présents.
...
     public void enregistrer() {
       try {
          ObjectOutputStream écrire = new ObjectOutputStream(new FileOutputStream("dessins.sto"));
          écrire.writeObject(formes);
          écrire.close();
       }
       catch (FileNotFoundException ex) { setTitle("Fichier inexistant");  }
       catch (IOException ex) { setTitle("Problème de sauvegarde");  }
     }

     public void ouvrir() {
       try {
         ObjectInputStream lire = new ObjectInputStream(new FileInputStream("dessins.sto"));
         formes = (ArrayList<Forme>) lire.readObject());
         lire.close();
         repaint();
       }
       catch (FileNotFoundException ex)   { setTitle("Fichier inexistant");  }
       catch (ClassNotFoundException ex) { setTitle("Problème de sauvegarde");  }
       catch (IOException ex) { setTitle("Problème de lecture du fichier");  }
     }
  }
}

L'enregistrement des objets directement dans le fichier devient alors la technique la plus simple à réaliser grâce à la sérialisation. Par contre la taille du fichier est plus conséquente.
§

Sur cette même application, nous avons réalisé plein de sauvegardes différents. Nous remarquons ici l'intérêt de Java, puisque nous pouvons décider du type d'enregistrement que nous souhaitons faire.

 

Choix du chapitre Les Tubes et les Threads

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.

  1. 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);

  2. 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.
    §

  3. 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);

  4. 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 correspondant à des types connus. Ainsi, nous pouvons utiliser les classes que nous connaissons déjà comme : DataInputStream, ObjectInputStream, Scanner, BufferedReader, etc.

Exemple de développement avec les tubes et les threads

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

 

Choix du chapitre Les flux sous forme de tableau d'octets

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();

Vous allez mettre en oeuvre la classe Clavier que vous avez déjà utilisé qui devra se trouver dans le paquetage saisie et comporter les quatre méthodes comme cela vous est présenté ci-contre.

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 :

int valeur = saisie.Clavier.lireInt("Donnez la valeur de l'axe des x : ");

Vous allez mettre en oeuvre ce petit logiciel qui permet de tracer des formes de tailles fixes, des cercles et des carrés.

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.

Les boîtes de dialogue JFileChooser sont toujours modales. Une boîte de dialogue modale ne permet pas à l'utilisateur d'interagir avec d'autres fenêtres de l'application tant qu'elle demeure ouverte. Vous appellerez la méthode showOpenDialog pour afficher une boîte de dialogue d'ouverture de fichier ou showSaveDialog pour afficher une boîte de dialogue d'enregistrement de fichier. Le bouton utilisé pour accepter un fichier est automatiquement libellé "Open" ou "Save".

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 :

  1. Créer un objet JFileChooser :
    JFileChooser fichier = new JFileChooser( ); // le répertoire utilisé est le répertoire de l'utilisateur
    JFileChooser fichier = new JFileChooser("."); // le répertoire courant est le répertoire de l'application
  2. Afficher la boîte de dialogue en appelant la méthode showOpenDialog ou showSaveDialog. Vous devez indiquer le composant parent pour ces appels :
    int résultat = fichier.showOpenDialog(this); ou
    int résultat = fichier.showSaveDialog(this);
  3. Suite à cet appel, le flux d'exécution du programme ne revient pas tant que l'utilisateur n'a pas accepter un fichier ou annulé son action. la valeur de retour est JFileChooser.APPROVE_OPTION ou JFileChooser.CANCEL_OPTION.
  4. Vous récupérez le fichier sélectionné au moyen de la méthode getSelectedFile. Cette méthode retourne un objet File. Le but de cette boîte de dialogue est de récupérer uniquement le nom du fichier. Il suffit d'appeler la méthode getName de l'objet File retourné :
    String nomFichier = fichier.getSelectedFile().getName();