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
.
Pour lire un flux d’octets comme un flux de char
, on
fabrique un InputStreamReader.
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.
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 :
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 à :
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.