W3docs

Flux de caractères en Java

Lisez et écrivez du texte en Java avec Reader, Writer, FileReader, FileWriter et la gestion de l'encodage des caractères.

Le chapitre précédent couvrait les flux d'octets — la couche brute où tout est byte. Cette couche convient aux données binaires mais pas au texte. Un caractère UTF-8 peut occuper un, deux, trois ou quatre octets ; UTF-16 utilise des unités de code de deux octets avec des paires de substitution pour tout ce qui dépasse le plan multilingue de base ; même le texte ASCII nécessite quelque part une décision "c'est de l'ASCII". Appeler InputStream.read() sur du texte et convertir le résultat en char ne fonctionne que si vous avez de la chance et que le fichier est à un octet par caractère — et dès que quelqu'un écrit "é", "日" ou "🎉", la version chanceuuse corrompt les données.

La hiérarchie des flux de caractères existe précisément pour garder ce décodage hors de votre code. Reader et Writer travaillent avec des char, pas des byte. Les classes passerelles — InputStreamReader et OutputStreamWriter — prennent un Charset et effectuent la conversion. Configurez correctement le jeu de caractères au niveau de la passerelle, et toutes les couches supérieures travailleront avec du texte décodé.

Le contrat de Reader

Reader est le miroir de InputStream, une paire abstraite de méthodes (read(char[], int, int) et close()) avec des commodités par-dessus :

int read();                            // next char as int 0..65535, or -1 at end
int read(char[] buf);                  // read up to buf.length chars; return count or -1
int read(char[] buf, int off, int len); // into a slice
String readLine();                       // only on BufferedReader — not on Reader itself
long transferTo(Writer out);             // Java 10+: pipe straight to a sink

Deux différences subtiles par rapport au côté octet. Premièrement, l'unité est char (une unité de code UTF-16 sur 16 bits), et non byte. Deuxièmement, read() renvoie 0..65535 pour une unité de code et -1 en fin de flux — la même astuce sentinelle qu'avec InputStream, mais la plage légale est plus large.

Un char n'est pas toujours un "caractère" — les caractères en dehors du plan multilingue de base (U+10000 et au-delà : la plupart des emojis, les écritures anciennes) utilisent deux unités de code UTF-16 (une paire de substitution). Si vous divisez aux frontières de char (par exemple en lisant 100 chars à la fois et en les traitant par morceaux), vous pouvez couper une paire de substitution entre deux lectures. Pour le texte orienté lignes, cela importe rarement ; pour le traitement au niveau des caractères d'Unicode arbitraire, travaillez en points de code (String.codePoints()).

Le contrat de Writer

Writer est le miroir de OutputStream :

void write(int c);                          // low 16 bits
void write(char[] buf);
void write(char[] buf, int off, int len);
void write(String s);                        // convenience — encodes a whole String
void write(String s, int off, int len);
Writer append(CharSequence csq);             // chainable: w.append("a").append("b")
void flush();
void close();                                // calls flush() first

write(String) est la commodité que vous utiliserez le plus souvent : la plupart des E/S texte consistent en un petit nombre de grandes écritures (un corps JSON, un rapport généré) plutôt qu'une sortie caractère par caractère.

append existe pour l'interopérabilité avec CharSequenceStringBuilder implémente CharSequence, donc un Writer peut être la cible de code qui écrit dans l'un ou l'autre selon un indicateur. C'est la même méthode append que StringBuilder possède lui-même, par interface.

Les flux de caractères concrets

ClasseCe qu'elle encapsule
FileReader / FileWriterUn fichier sur le disque, décodé comme texte.
CharArrayReader / CharArrayWriterUn char[] en mémoire.
StringReader / StringWriterUne String/StringBuilder en mémoire.
BufferedReader / BufferedWriterUne vue tamponnée d'un autre Reader/Writer.
InputStreamReader / OutputStreamWriterClasses passerelles : un Reader/Writer sur un flux d'octets sous-jacent, avec un Charset.
PrintWriterUn décorateur de Writer qui ajoute print, println et printf.

Les classes passerelles sont le point structurel de toute la hiérarchie. Chaque flux de caractères qui communique avec un fichier, un socket ou un pipe est — en dessous — un flux d'octets plus un jeu de caractères. FileReader est une fine couche autour de InputStreamReader(new FileInputStream(...)); FileWriter de même autour de OutputStreamWriter(new FileOutputStream(...)).

Le piège du jeu de caractères

Le bug classique en Java I/O :

// WRONG in any code that might run on more than one machine
try (FileReader in = new FileReader("data.txt")) { ... }
try (FileWriter out = new FileWriter("data.txt")) { ... }

Les constructeurs sans jeu de caractères utilisent le jeu de caractères par défaut de la JVM, déterminé au démarrage à partir des paramètres régionaux du système d'exploitation. Sur un Mac de développeur, c'est presque toujours UTF-8. Sur un serveur Linux avec un paramètre régional C, cela peut être US-ASCII. Sur Windows avec une installation en anglais, c'est Cp1252. Le bug "ça marche sur mon Mac, cassé sur la machine de production" est exactement ce constructeur.

Passez un jeu de caractères explicitement :

// Right
try (FileReader in = new FileReader("data.txt", StandardCharsets.UTF_8)) { ... }
try (FileWriter out = new FileWriter("data.txt", StandardCharsets.UTF_8)) { ... }

(Les formes à deux arguments prenant un Charset ont été ajoutées en Java 11. Avant cela, il fallait descendre aux classes passerelles — new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8) — et la ligne décorateur enchaîné est l'une des raisons pour lesquelles Files.newBufferedReader(path) a été ajouté : il utilise UTF-8 par défaut depuis Java 18 et était toujours explicite sur le jeu de caractères avant.)

L'API moderne Files a rendu cette valeur par défaut plus sûre :

String text = Files.readString(path);                // UTF-8 by default (Java 18+)
BufferedReader r = Files.newBufferedReader(path);    // UTF-8 by default (always was)

Si vous partez de zéro, utilisez les fabriques de Files. Si vous touchez du code legacy avec FileReader/FileWriter, le correctif le moins coûteux est d'ajouter le second argument StandardCharsets.UTF_8.

Les classes passerelles en direct

Vous avez besoin de InputStreamReader et OutputStreamWriter quand la source n'est pas un fichier — un ZipEntry, un socket, le corps d'une réponse HTTP, System.in, un flux enveloppé dans Inflater — et que vous voulez en extraire du texte :

// Read text from System.in as UTF-8
try (BufferedReader stdin = new BufferedReader(
        new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
  String line = stdin.readLine();
}

// Write the response of an HttpURLConnection as text
try (BufferedReader resp = new BufferedReader(
        new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
  resp.lines().forEach(System.out::println);
}

La forme est toujours la même : flux d'octets → InputStreamReader(stream, charset)BufferedReader optionnel → votre code.

Exemple pratique : texte sous trois formes

Le programme ci-dessous écrit un petit fichier texte UTF-8 contenant des caractères ASCII, des caractères accentués et un emoji multi-octets, puis le relit de quatre façons : comme une String, caractère par caractère, ligne par ligne via un BufferedReader, et via le constructeur legacy FileReader(charset). L'exemple montre également la forme des classes passerelles utilisée sur un ByteArrayInputStream pour que vous puissiez voir où Reader et InputStream se rejoignent.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • Le fichier sur le disque (23 octets) était plus grand que content.length() (20). La String a length() == 20 (en comptant chaque \n et en comptant l'emoji 🎉 comme deux unités de code UTF-16 — c'est ce que mesure un char Java) ; UTF-8 encode l'emoji sur quatre octets et é sur deux, donc le nombre d'octets est plus grand. En points de code, il n'y en a que 19 — l'emoji est un point de code mais deux chars. Le même texte logique est un nombre en chars, un autre en octets, un autre encore en points de code. Savoir lequel vous voulez dire représente la moitié des bugs liés aux jeux de caractères.
  • La boucle char par char a reconstitué exactement la même chaîne. L'API Reader a géré le décodage UTF-8 pour vous : un seul emoji apparaît comme deux appels (char) read() en raison des substituts UTF-16, mais vous n'avez jamais eu à vous soucier des frontières d'octets.
  • BufferedReader.readLine() a retourné trois lignes : hello, café, 🎉 party. C'est le vocabulaire orienté texte — ligne par ligne, conscient des terminateurs (gère \n, \r et \r\n), et construit par-dessus la classe passerelle. Chaque appel d'API dans ce chapitre et les suivants se réduit en fin de compte à "décoder des octets via un jeu de caractères et servir des caractères."
  • Le bloc direct InputStreamReader(new ByteArrayInputStream(raw), UTF_8) montre la forme structurelle : source d'octets à l'intérieur, jeu de caractères au niveau de la passerelle, API de caractères à l'extérieur. Remplacez ByteArrayInputStream par socket.getInputStream() et le reste est identique — c'est pourquoi les clients HTTP et JDBC convergent tous vers le même idiome.
  • Le dernier bloc a décodé les mêmes octets avec le mauvais jeu de caractères. Le é accentué et l'emoji sont tous deux apparus comme du charabia — le bug classique de mojibake. Les octets sur le disque étaient corrects ; le jeu de caractères au niveau de la passerelle était faux. C'est pourquoi épingler le jeu de caractères explicitement est la seule habitude la plus utile en Java pour les E/S texte.

La suite

Les flux d'octets et de caractères effectuent par défaut des E/S un à la fois, et sur un flux de fichier brut chaque appel est un appel système. Le prochain chapitre, Flux tamponnés Java, couvre les décorateurs Buffered* — un tampon en mémoire entre votre code et le système d'exploitation — et l'API readLine() qui y réside.

Pratique

Pratique
Pourquoi `new FileReader(path)` et `new FileWriter(path)` (sans argument de jeu de caractères) causent-ils des bugs 'ça marche sur ma machine, cassé sur le serveur' ?
Pourquoi `new FileReader(path)` et `new FileWriter(path)` (sans argument de jeu de caractères) causent-ils des bugs 'ça marche sur ma machine, cassé sur le serveur' ?
Was this page helpful?