3. Flux de sortie

3.1. Introduction, notion de flux

La première chose à définir lorsque l'on parle de flux de sortie, est précisément ce que l'on entend par sortie . Une sortie est un endroit sur lequel, ou dans lequel, on peut écrire des données. Les sorties les plus courantes sont :
  • un fichier, qu'il soit à accès séquentiel ou aléatoire ;
  • une chaîne de caractères, instance de String ;
  • la console système ;
  • un tableau de caractères, ou d'octets ;
  • une URL (ou URI), dans le cas des flux HTTP ;
  • une socket, pour la communication ;
  • un tuyau de communication entre threads.
Pour chacune de ces sorties, il existe une classe Java qui permet d'écrire dessus. L'action d'écrire se fait en utilisant un objet qui modélise le flux des données vers chacun de ces média de sortie. Ce flux de donnée peut être construit par l'objet qui gère le médium, ou passé à cet objet lors de sa construction. Toujours est-il que du point de vue du développement de l'application, les méthodes utilisées sont celles exposées par ce flux, et toujours les mêmes. Cette approche est très puissante. Par exemple, si l'on utilise la méthode write de l'objet ZipOutputStream, les données écrites dans le fichier seront compressées de façon transparente pour l'utilisateur.

3.2. Écriture de caractères, classe Writer

Là encore, la portabilité souhaitée lors de la construction du langage a rendu les choses complexes pour les personnes qui ont écrit les API que nous allons voir. Java utilise Unicode pour coder ses caractères, alors que de nombreux langages et applications utilisent d'autres codages. La conversion se fait de façon transparente pour l'utilisateur, ou presque... La classe de base utilisée pour écrire des caractères est la classe Writer. Cette classe est abstraite, et expose les méthodes suivantes.
  • write(char[] buffer), write(char[] buffer, int offset, int length), write(String s), write(String s, int offset, int length) et write(int c). Toutes ces méthodes permettent d'écrire des caractères en provenance de différentes sources (tableau ou chaîne de caractères). La dernière écrit un unique caractère stocké dans les 16 bits de poids faible de l'entier passé en paramètre.
  • append(char c), append(CharSequence seq) et append(CharSequence seq, int start, int end). Ces méthodes fonctionnent de façon analogue aux précédentes. La différence est qu'elles retournent cette instance de Writer, ce qui permet de chaîner ces appels, comme lorsque l'on écrit dans un StringBuffer.
  • flush() : permet de vider les buffers d'écriture vers le médium de sortie.
  • close() : ferme ce flux.
Les méthodes flush() et close() sont en général présentes dans toutes les API de ce type, et ont toujours la même sémantique. Notons que ces méthodes sont abstraites dans la classe Writer. Le fonctionnement exact n'est donc pas précisé au niveau de >Write. En particulier, l'appel à la méthode flush() avant la fermeture du flux n'est pas imposé. Cette classe Writer est abstraite, on ne peut donc pas l'instancier directement. Elle est étendue par une série de classes, qui correspondent à chacun des média de sortie dont nous avons donné la liste en introduction : FileWriter, StringWriter et CharArrayWriter sont les principales. Chacune des classes concrètes qui étendent Writer exposent leurs propres constructeurs. FileWriter se construit sur un fichier (instance de File). Pour StringWriter et CharArrayWriter on indique juste la taille de la chaîne ou du tableau dans lequel l'écriture va se faire. Notons enfin que toutes ces méthodes jettent l'exception IOException, qu'il faut donc gérer. Voyons un premier exemple d'écriture dans un fichier.

Exemple 115. Utilisation de FileWriter, première version

public  static  void main(String[] args) {

    // définition d'un fichier
   File fichier =  new File("tmp/bonjour.text") ;

    try {

       // ouverture d'un flux de sortie sur un fichier
       // a pour effet de créer le fichier
      Writer writer =  new FileWriter(fichier) ;
      
       // écriture dans le fichier
      writer.write("Bonjour le monde !") ;
      
       // la méthode close de FileWriter appelle elle-même flush()
      writer.close() ;

   }  catch (IOException e) {

       // affichage du message d'erreur et de la pile d'appel
      System.out.println("Erreur " + e.getMessage()) ;
      e.printStackTrace() ;
   }
}

Cette première version fonctionne sans problème, et écrit ce que l'on souhaite dans le bon fichier, si l'on a pris garde de créer le répertoire tmp avant de la lancer. Cela dit, ce n'est pas comme cela que l'on doit écrire ce code, notamment dans les applications serveur, susceptibles de fonctionner sans interruption pendant des mois. Le problème vient de ce que la méthode close() n'est appelée que si aucune erreur n'est rencontrée lors de l'exécution du code. Par exemple, si l'écriture ne peut se faire, et jette une exception, alors l'exécution du code passe dans le bloc catch. Dans ce cas, le fichier qui a été ouvert n'est pas fermé. Bien que cela ne pose pas de problème dans le cas de ce code, puisque la machine Java s'éteint immédiatement, il faut tout de même corriger cette erreur tout de suite, pour ne pas prendre de mauvaises habitudes !

Exemple 116. Utilisation de FileWriter, , version corrigée

public  static  void main(String[] args) {

    // définition d'un fichier
   File fichier =  new File("tmp/bonjour.text") ;
   
    // la définition du writer doit se faire ici
    // pour des raisons de visibilité
   Writer writer = null ;

    try {

       // ouverture d'un flux de sortie sur un fichier
       // a pour effet de créer le fichier
      writer =  new FileWriter(fichier) ;
      
       // écriture dans le fichier
      writer.write("Bonjour le monde !") ;

   }  catch (IOException e) {

       // affichage du message d'erreur et de la pile d'appel
      System.out.println("Erreur " + e.getMessage()) ;
      e.printStackTrace() ;
      
   }  finally {
      
       // il se peut que l'ouverture du flux ait échoué, 
       // et que ce writer n'ait pas été initialisé
       if (writer != null) {

          try {

             // la méthode close de FileWriter appelle elle-même flush()
            writer.close() ;
					
         }  catch (IOException e) {

            System.out.println("Erreur " + e.getMessage()) ;
            e.printStackTrace() ;
         }
      }
   }
}

Ce code peut paraître complexe de prime abord, c'est pourtant le pattern d'ouverture et de fermeture d'un fichier qu'il faut utiliser (et pas un autre !).

3.3. Bufferisation, construction d'un flux sur un autre

Le mécanisme de bufferisation est bien connu. Dans le cas de l'écriture dans un fichier, il consiste à stocker ce qui est écrit dans une zone mémoire à accès rapide (en tout cas plus rapide qu'un disque), et de ne déclencher l'écriture effective sur le disque que lorsque cette zone mémoire arrive à saturation, ou à intervalles de temps réguliers. On peut également forcer cette écriture par un appel à flush(). L'API Java IO offre le support de la bufferisation par la classe BufferedWriter. Cette classe étend la classe Writer, et son constructeur prend en paramètre un flux de type Writer. L'utilisation de cette classe dans notre exemple précédent donne ceci.

Exemple 117. Utilisation d'un buffer d'écriture

// le début du code est inchangé

       // ouverture d'un flux de sortie sur un fichier
       // a pour effet de créer le fichier
      writer =  new FileWriter(fichier) ;
      Writer bufferedWriter =  new BufferedWriter(writer) ;
      
       // écriture dans le fichier
      bufferedWriter.write("Bonjour le monde !") ;

 // la fin du code est inchangée

Appelé la méthode flush() d'un flux bufferisé est important, car cela permet de vider les buffers vers le medium de sortie, disque ou réseau. Cette instance de BufferedWriter ne crée pas à proprement parler de nouveau flux. Plutôt, elle enveloppe le flux existant sur le fichier fichier, et ajoute au flux existant les fonctionnalités de bufferisation. Fermer le médium de sortie via l'instance bufferedWriter ou writer donne le même résultat, puisqu'il n'y a qu'une unique ouverture de fichier.

3.4. Utilisation de PrintWriter

La classe PrintWriter étend la classe Writer. Elle expose de nombreux constructeurs, qui permettent de créer une instance de PrintWriter sur de nombreux types de flux, dont une instance de Writer. Cette classe fonctionne donc aussi comme une enveloppe sur le flux passé en paramètre. Elle expose des méthodes bien connues en C : print(...), println(...) et printf(...). Ces méthodes peuvent prendre des formats en paramètres, ainsi que des locales. Elles permettent donc de formatter des affichages, en prenant en compte les conventions linguistiques de formatage, lorsque la locale est passée en paramètre. Notons que pour l'écriture dans des fichiers, la classe PrintWriter expose également un constructeur qui prend une instance de File. En l'utilisant on n'a donc pas besoin de construire soi-même l'instance de Writer. Voyons un exemple d'utilisation d'un PrintWriter.

Exemple 118. Utilisation de PrintWriter

File fichier =  new File("tmp/bonjour.text") ;
Writer writer = null ;

 try {

    // ouverture d'un flux de sortie sur un fichier
   writer =  new FileWriter(fichier) ;
   
    // création d'un PrintWriter sur ce flux
   PrintWriter pw =  new PrintWriter(writer) ;
   
    // écriture d'un marin dans le fichier
   Marin m =  new Marin("Surcouf",  "Robert") ;
   
    // la méthode toString() est appelée
   pw.println(m) ;

}  catch (IOException e) {

    // gestion des erreurs
}  finally {

    // fermeture du flux
}

3.5. Écriture d'octets, OuputStream

L'écriture d'octets suit exactement la même logique que l'écriture de caractères. Ces écritures sont gérées par la classe abstraite OutputStream, qui joue un rôle équivalent à celui joué par la classe Writer. Cette classe est abstraite, et étendue par les classes FileOutputStream pour l'écriture dans des fichiers, et ByteArrayOutputStream pour l'écriture dans des tableaux d'octets. La classe OutputStream expose les méthodes suivantes.
  • write(byte [] b), write(byte [] b, int offset, int length) et write(int b). Ces méthodes permettent d'écrire des octets directement sur le flux. Dans le cas de l'écriture d'un entier, seuls les 8 bits de poids faibles sont pris en compte.
  • close() et flush() qui ont la même sémantique que celles de la classe Writer.
Voyons une première utilisation de la classe FileOutputStream. Le pattern suivi est identique à celui de FileWriter.

Exemple 119. Utilisation de FileOutputStream

public  static  void main(String[] args) {
   
    // définition d'un fichier
   File fichier =  new File("tmp/array.bin") ;
   OutputStream os = null ;
   
    // création d'un tableau d'octets, qui sera
    // écrit dans le fichier
    byte [] byteArray = {  0,  1,  2 } ;
   
    try {
      
       // ouverture d'un flux de sortie sur un fichier
      os =  new FileOutputStream(fichier) ;
       // écriture proprement dite
      os.write(byteArray) ; 
      
   }  catch (IOException e) {
      
      System.out.println("Erreur " + e.getMessage()) ;
      e.printStackTrace() ;
   }  finally {
      
       // fermeture du fichier dans le bloc finally
       if (os != null) {
      
          try {
            
             // la méthode close de FileOutputStream appelle elle-même flush()
            os.close() ;
            
         }  catch (IOException e) {
            
            System.out.println("Erreur " + e.getMessage()) ;
            e.printStackTrace() ;
         }
   }
}

La classe ByteArrayOutputStream permet de diriger le flux de sortie vers un tableau d'octets, qui sert alors de buffer . Cette classe expose quelques méthodes supplémentaires pour la gestion de ce buffer.
  • reset() : permet de remettre à zéro le pointeur d'écriture de ce buffer, ce qui a pour effet d'annuler tout ce qui a été écrit.
  • size() : retourne la taille du buffer. Notons que la taille de ce buffer peut être fixée à la construction de cette instance de ByteArrayOutputStream.
  • toByteArray() : retourne le tableau d'octets sur lequel ce buffer est construit.
  • toString(String charsetName) : permet de convertir le contenu de ce tableau d'octets en utilisant le jeu de caractères précisé en argument. La liste des jeux de caractères supportés est donnée dans la javadoc de la classe Charset.

3.6. Écriture de types primitifs : DataOutputStream

L'écriture de types primitifs est fournie par la classe DataOutputStream, extension de FilterOutputStream, elle-même extension de OutputStream. Cette classe expose une méthode par type primitif, qui permet décrire ce type sur un flux d'octets. La construction d'une instance de DataOutputStream prend en paramètre une instance de OutputStream, de façon classique. Voyons ceci sur un exemple.

Exemple 120. Utilisation de DataOutputStream

public  static  void main(String[] args) {

   File fichier =  new File("tmp/integers.bin") ;
   OutputStream os = null ;

    try {
       // ouverture d'un flux de sortie sur un fichier
      os =  new FileOutputStream(fichier) ;
       // ouverture d'un flux de type DataOutputStream
       // sur ce même fichier
      DataOutputStream dos =  new DataOutputStream(os) ; 

       for (int i : Arrays.asList(1,  2,  3,  4,  5)) {
         dos.writeInt(i) ;
      }

      }  catch (IOException e) {
          // gestion de l'erreur
      }  finally {
          // fermeture du flux
      }
   }
}

Les méthodes d'écriture suivent toutes le même modèle de nommage : writeInt(int), writeLong(long), etc...

3.7. Écriture d'objets : ObjectOutputStream

La classe ObjectOutputStream supporte l'écriture directe d'objets sur des flux. Ce mécanisme particulier s'appelle serialization , et a un fonctionnement particulier. Il permet de garantir, entre autres, qu'une instance d'une classe écrite dans un fichier est bien recréée dans la bonne classe, identique à la première. Ce mécanisme peut aussi être surchargé de différentes façons. Nous verrons ces techniques dans une section particulière, en fin de ce chapitre.
Java langage & API
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
Introduction : un peu d'histoire
1. Java : genèse d'un nouveau langage
Programmer en Java
1. Un premier exemple
1.1. Aperçu général, cycle de vie
1.2. Un premier programme
1.3. Programmes, applets, servlets, etc...
2. Une première classe
2.1. Écriture d'une classe
2.2. Instanciation de cette classe
3. Types de base, classes et objets
3.1. Types de base
3.2. Classes et objets
Classes Object et String
1. Introduction
2. La classe Object
2.1. La méthode toString()
2.2. La méthode clone()
2.3. La méthode equals()
2.4. La méthode hashCode()
2.5. La méthode finalize()
2.6. La méthode getClass()
3. La classe String
3.1. Introduction
3.2. Construction d'un objet de type String
3.3. Concaténation, StringBuffer et StringBuilder
3.4. Concaténations de chaînes de caractères depuis Java 8
3.5. Concaténations de chaînes de caractères depuis Java 11
3.6. Extraction d'une sous-chaîne de caractères
3.7. Comparaison de deux chaînes de caractères
3.8. Méthodes de comparaisons lexicographiques
3.9. Méthode de recherche de caractères
3.10. Méthode de modification de chaîne
3.11. Méthode de duplication
3.12. Support de l'unicode, internationalisation
Structure d'une classe
1. Introduction
2. Classes
2.1. Classes publiques
2.2. Classes internes
2.3. Classe membre
2.4. Classes locales
2.5. Classes anonymes
2.6. Le mot-clé this
3. Éléments statiques
3.1. Champ statique
3.2. Cas des constantes
3.3. Bloc statique
3.4. Classe membre statique
4. Membres d'une classe, visibilité
4.1. Bloc non statique
4.2. Accès à un membre, visibilité
4.3. Les champs
4.4. Signature d'une méthode
4.5. Les méthodes
4.6. Getters et Setters
5. Constructeur, instanciation
5.1. Chargement d'une classe
5.2. Constructeurs d'une classe
5.3. Instanciation d'un objet
5.4. Destruction d'objets
5.5. Le mot-clé final
6. Énumérations
6.1. Déclaration d'une énumération
6.2. Classe énumération
6.3. Méthode toString()
6.4. Méthode valueOf()
6.5. Méthode values()
6.6. Méthode ordinal()
6.7. Méthode compareTo()
6.8. Constructeurs privés
6.9. Classe utilitaire : EnumSet
Noms, opérateurs, tableaux
1. Introduction
2. Identificateurs, noms et expressions
2.1. Identificateurs et noms
2.2. Expressions
3. Opérateurs, ordre d'exécution
3.1. Ordre d'exécution
3.2. Les opérateurs
3.3. Les opérateurs ++ et --
3.4. Les opérateurs % et /
3.5. Les opérateurs <<, >> et >>>
3.6. L'opérateur instanceof
3.7. Les opérateurs &, | et ^
3.8. Les opérateurs && et ||
3.9. L'opérateur ? ... :
3.10. Les opérateurs d'affectation
4. Tableaux
4.1. Création d'un tableau
4.2. Initialisation d'un tableau
4.3. Utilisation d'un tableau comme un Object
4.4. Tableaux de tableaux
4.5. Copie de tableaux
5. Blocs, boucles et contrôles
5.1. Blocs
5.2. Mots-clés réservés
5.3. Tests : if et switch
5.4. Boucles : for, while, do ... while
5.5. Commandes continue et break
5.6. Commandes return et goto
Nombres, précision, calculs
1. Introduction
2. Calculs
2.1. Précision
2.2. Codage des nombres flottants
2.3. Le mot-clé strictfp
2.4. Conversion de types
3. Dépassements de capacité
3.1. Cas des entiers
3.2. Cas des flottants
3.3. Bibliothèques BigInteger et BigDecimal
4. Fonctions mathématiques
4.1. Fonctions usuelles
4.2. Générateurs aléatoires
5. Classes enveloppe
5.1. Associer les types de base à des objets
5.2. Auto-boxing
Héritage, abstraction, interfaces
1. Introduction
2. Abstraction et encapsulation
2.1. Abstraction
2.2. Encapsulation
3. Héritage
3.1. Définition de l'héritage
3.2. Conséquences pour les membres
3.3. Polymorphisme
3.4. Empêcher l'héritage
4. Classes abstraites
5. Interfaces
5.1. Introduction
5.2. Définition
5.3. Java 8 et les interfaces
5.4. Utilisation des interfaces
5.5. Définition de constantes dans les interfaces
5.6. Utilité des interfaces
Packages
1. Introduction
2. Notion de paquet
2.1. Déclaration d’appartenance à un paquet
2.2. Chargement d’une classe
2.3. Choix de nom
3. Archives, chemin de recherche, classpath
3.1. Archives
3.2. Variable CLASSPATH
3.3. Notion de classloader
3.4. Bilan sur les classes chargées
3.5. Visibilité
3.6. Conseils d'écriture, bibliothèque standard
Exceptions
1. Introduction
2. Erreurs et Exceptions
2.1. Classe Throwable, notion de stack trace
2.2. Classe Error
2.3. Classe RuntimException
2.4. Classe Exception
3. Déclenchement d'une exception
3.1. Exceptions déclenchées par la JVM
3.2. Exceptions déclenchées par l'application
4. Capter une exception
4.1. Traiter une exception localement
4.2. Code de captage
5. Créer ses propres exceptions
Entrées / sorties
1. Introduction
2. Notion de fichier
2.1. Introduction
2.2. La classe File
2.3. Construction d'une instance de File
2.4. Méthodes exposées
3. Flux de sortie
3.1. Introduction, notion de flux
3.2. Écriture de caractères, classe Writer
3.3. Bufferisation, construction d'un flux sur un autre
3.4. Utilisation de PrintWriter
3.5. Écriture d'octets, OuputStream
3.6. Écriture de types primitifs : DataOutputStream
3.7. Écriture d'objets : ObjectOutputStream
4. Flux d'entrée
4.1. Introduction
4.2. Lecture de caractères, classe Reader
4.3. Bufferisation, lecture ligne par ligne
4.4. Lecture d'octets : InputStream
4.5. Lecture de types primitifs : DataInputStream
4.6. Lecture d'objets : ObjectInputStream
5. Lecture et écriture de flux croisés
5.1. Introduction
5.2. Lire des caractères sur un flux binaire : classe InputStreamReader
5.3. Écrire des caractères sur un flux binaire : classe OutputStreamWriter
5.4. Remarque sur les jeux de caractères
6. Serialization d'objets
6.1. Enjeu de la sérialization d'objets
6.2. Serialization d'un objet
6.3. Sérialization d'une grappe d'objets
6.4. Première surcharge : méthode writeObject() readObject()
6.5. Deuxième surcharge : utilisation d'un externalizer
6.6. Troisième surcharge : utilisation d'un objet proxy
7. Flux compressés
7.1. Introduction
7.2. Flux de type gzip
7.3. Flux de type zip