Previous Up

B.5.6  Complément : tout ou presque sur les fichiers texte

Si Java et Unix sont bien d’accord sur ce qu’est en gros un fichier texte (un fichier qu’un humain peut lire au moins en principe), ils ne sont plus d’accord sur ce que sont les éléments d’un tel fichier. Pour Java, un fichier de texte est un flux de char (16 bits), tandis que pour Unix c’est un flux d’octets (8 bits, un byte pour Java). Le passage de l’un à l’autre demande d’appliquer un encodage.

Un exemple simple d’encodage est par exemple ISO-8859-1, l’encodage que java emploie par défaut sur les machines de l’école. C’est un encodage qui fonctionne pour presque toutes les langues européennes (mais pas pour le symbole €). C’est un encodage simple sur 8 bits, qui permet d’exprimer 256 caractères seulement parmi les 216 d’Unicode. Pour voir les caractères définis en ISO-8859-1, vous pouvez essayer man latin1 sur une machine de l’école. L’encodage ISO-8859-1 est techniquement le plus simple possible : les codes des caractères sont les mêmes à la fois en ISO-8859-1 et en Unicode. Il n’est donc tout simplement pas possible d’exprimer les char dont les codes sont supérieurs à 256 en ISO-8859-1 (dont justement €, dont la valeur Unicode hexadécimale est 0x20AC, notée U+20AC). Il existe d’autres encodages 8 bits, dont ISO-8859-15 qui entre autres établit justement la correspondance entre le caractère Unicode U+20AC et le byte 0xA4, au dépend du caractère Unicode U+00A4 (¤) qui n’est plus exprimable. Il existe bien entendu des encodages qui permettent d’exprimer tout Unicode, mais alors un caractère Unicode peut s’exprimer comme plusieurs octets. Un encodage multi-octet répandu est UTF-8, où les caractères Unicode sont représentés par un nombre variable d’octets selon un système un peu compliqué que nous ne décrirons pas (voir http://fr.wikipedia.org/wiki/UTF-8 qui est raisonnablement clair).

Revenons aux flux de Java. Pour fixer les idées nous considérons d’abord les flux en lecture. Un flux d’octets est un InputStream. La classe InputStream fonctionne sur le même principe que la classe Reader : c’est une sur-classe des divers flux de byte qui peuvent être construits. Par exemple on ouvre un flux d’octets sur un fichier name en créant un objet de la classe FileInputStream.

  InputStream inBytes = new FileInputStream (name) ;

Pour lire un flux d’octets comme un flux de char, on fabrique un InputStreamReader.

  Reader inChars = new InputStreamReader (inBytes) ;

L’encodage est ici implicite, c’est l’encodage par défaut. On peut aussi expliciter l’encodage en donnant son nom comme second argument au constructeur.

  Reader inChars = new InputStreamReader (inBytes, "UTF-8") ;

Et voilà nous disposons maintenant d’un Reader sur un fichier dont la suite d’octets définit une suite de caractères Unicode encodés en UTF-8. Ici, comme UTF-8 est un encodage multi-octets, la lecture d’un char dans inChars implique de lire un ou plusieurs byte dans le flux inBytes sous-jacent. Les flux en écriture suivent un schéma similaire, il y a des flux de byte (OutputStream) et des flux de char (Writer), avec une classe pour faire le pont entre les deux (OutputStreamWriter). Par exemple, voici comment fabriquer un Writer connecté à la sortie standard et qui écrit de l’UTF-8 sur la console :

  // System.out (un PrintStream) est aussi un OutputStream
  Writer outChar = new OutputStreamWriter (System.out, "UTF-8") ;

Notons qu’une fois obtenu un Reader ou un Writer nous pouvons fabriquer ce dont nous avons besoin, par exemple un Scanner ou un PrintWriter etc., à l’aide des constructeurs « naturels » de ces classes, qui prennent un Reader ou un Writer en argument. Les diverses classes de flux possèdent parfois des constructeurs qui semblent permettre de court-circuiter le passage par un InputStreamReader ou un OutputStreamWriter ; mais ce n’est qu’une apparence, il y aura toujours décodage et encodage. Par exemple, new PrintWriter (String name) ouvre directement le fichier name, mais à quelques optimisations internes toujours possibles près, employer ce constructeur synthétique revient à :

  new PrintWriter (new OutputStreamWriter (new FileOutputStream (name))) ;

Finalement, en théorie nous savons lire et écrire tout Unicode. En pratique, il faut encore pouvoir entrer ces caractères au clavier et les visualiser dans une fenêtre, mais c’est une autre histoire qui ne regarde plus Java.

Toutes ces histoires d’encodage et de décodage de char en byte font qu’écrire un char dans un Writer ou lire un char dans un Reader ne sont jamais des opérations simples.3 Comme on a tendance a tout simplifier on a parfois des surprises : par exemple, les FileWriter (voir B.5.3) possèdent en fait déjà un tampon. On se rend vite compte de l’existence de ce tampon si on oublie de fermer un FileWriter. Si on en croit la documentation :

Each invocation of a write() method causes the encoding converter to be invoked on the given character(s). The resulting bytes are accumulated in a buffer before being written to the underlying output stream.

Il s’agit donc d’un tampon de byte dans lequel le FileWriter stocke les octets résultants de l’encodage qu’il effectue. On peut alors se demander d’où provient le gain d’efficacité constaté en emballant un FileWriter dans un BufferedWriter (voir B.5.4), puisque le coût irréductible de la véritable sortie est déjà amorti par un tampon. Et bien, il se trouve que le coût de l’application de l’encodage des char vers les byte suit lui-aussi le modèle d’un coût constant important relativement au coût proportionnel au nombre de caractères encodés. Le tampon (de char) introduit en amont du FileWriter par new BufferedWriter (new FileWriter (name2)) a alors pour fonction d’amortir ce coût irréductible de l’encodage.


3
Encodage et décodage font aussi qu’il ne faut pas écrire Cp et Cat comme nous l’avons fait (avec des flux de char). C’est inutilement inefficace et même dangereux certains décodages pouvant parfois échouer, il aurait mieux valu employer les flux de byte (source Cp0.java).

Previous Up