Java PrintStream
Comment PrintStream alimente System.out et System.err et comment l'utiliser pour une sortie formatée orientée octets.
PrintStream est la classe qui se cache sous votre code depuis le chapitre 1. System.out est un PrintStream. System.err est un PrintStream. Chaque System.out.println(...) que vous avez jamais écrit est passé par cette classe.
Elle possède la même surface que le PrintWriter que vous venez de rencontrer — print, println, printf, format — et le même comportement d'absorption des exceptions. La différence réside dans ce sur quoi elle repose : PrintStream étend OutputStream (octets), tandis que PrintWriter étend Writer (caractères). Pour la sortie vers des fichiers, la distinction octet/caractère abordée précédemment dans cette partie s'applique toujours : des caractères en entrée, des caractères en sortie, et l'encodage se situe à la frontière.
Pourquoi deux classes avec la même API ?
L'histoire. Java 1.0 avait PrintStream mais aucune hiérarchie Writer du tout — chaque « print » allait vers un flux d'octets. Java 1.1 a introduit la hiérarchie Reader/Writer pour une gestion correcte des caractères et a ajouté PrintWriter afin que le code d'écriture de fichiers puisse utiliser la même API sur les caractères. PrintStream ne pouvait pas être retiré car System.out et System.err étaient déjà typés comme PrintStream dans les API publiées, et les modifier aurait cassé tous les programmes du monde.
Ainsi, les deux coexistent. La règle pratique :
- Utilisez
PrintWriterpour les fichiers. La hiérarchie orientée caractères est là où l'encodage appartient. - Utilisez
PrintStreamquand vous y êtes obligé — c'est-à-dire quandSystem.out/System.errest la cible, ou quand vous écrivez dans unOutputStreamque vous ne souhaitez pas envelopper.
Les cas « obligatoires » sont rares. La plupart du temps, vous pouvez faire ceci :
PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8);
out.println("hello");et oublier que PrintStream existe.
L'API
Identique à PrintWriter :
void print(boolean | char | int | long | float | double | String | Object);
void println(...); // adds the platform line separator
PrintStream printf(String format, Object... args);
PrintStream format(String format, Object... args);
PrintStream append(CharSequence s);Plus les méthodes OutputStream héritées (write(int), write(byte[]), flush, close). Le même piège que BufferedWriter et PrintWriter s'applique : println écrit System.lineSeparator(), qui est \r\n sur Windows. Écrivez \n explicitement lorsque la sortie doit être portable.
Constructeurs
new PrintStream(OutputStream out); // platform default charset
new PrintStream(OutputStream out, boolean autoFlush, Charset cs); // explicit charset
new PrintStream(File file, Charset charset); // open a file
new PrintStream(String filename, Charset charset);Comme avec PrintWriter, les constructeurs sans charset se rabattent sur l'encodage par défaut de la JVM — le même risque de portabilité décrit dans le chapitre sur les flux de caractères. Passez toujours un charset.
Le flag autoFlush a la même sémantique que PrintWriter : lorsqu'il est activé, println, printf, format, et write(byte[], int, int) sur un saut de ligne déclenchent un flush. print ne le fait pas. Désactivé par défaut.
L'IOException avalée (toujours)
Même conception que PrintWriter. Aucune des méthodes print/println/printf ne lève d'IOException. Un échec d'écriture positionne un flag d'erreur que vous lisez avec checkError(). Le compromis est le même : pratique pour du code occasionnel, dangereux si vous ne vérifiez pas.
Pour System.out/System.err spécifiquement, avaler l'exception est la bonne décision — il n'y a rien d'utile à faire quand une écriture vers le terminal échoue. Pour un PrintStream sauvegardé dans un fichier, préférez PrintWriter, ou vérifiez checkError() avant la fermeture.
System.out et System.err
Ces deux instances de PrintStream sont créées au démarrage de la JVM. Elles enveloppent les descripteurs de fichiers stdout et stderr du système d'exploitation. Leur encodage de caractères suit stdout.encoding (Java 18+) ou file.encoding (versions plus anciennes), ce qui explique pourquoi la sortie redirigée via un pipe produit parfois des caractères incorrects sur une console Windows — la page de code de la console ne correspond pas à l'idée qu'a la JVM de l'encodage.
Vous pouvez les remplacer avec System.setOut(PrintStream) et System.setErr(PrintStream), ce qui est parfois utile pour capturer la sortie dans les tests :
ByteArrayOutputStream captured = new ByteArrayOutputStream();
PrintStream original = System.out;
System.setOut(new PrintStream(captured, true, StandardCharsets.UTF_8));
try {
runTheCodeUnderTest();
assertEquals("expected\n", captured.toString(StandardCharsets.UTF_8));
} finally {
System.setOut(original);
}Pour le code de production, laissez-les tranquilles. Les frameworks de journalisation (java.util.logging, SLF4J/Logback) adoptent une approche différente et structurée pour écrire la sortie de diagnostic.
print(Object) et null
Un comportement subtil partagé avec PrintWriter : print(Object o) appelle String.valueOf(o), qui retourne la chaîne de quatre caractères "null" pour une référence null plutôt que de lancer une NullPointerException. C'est la raison pour laquelle
System.out.println(maybeNullList); // prints "null", not NPEfonctionne. Pratique pour la journalisation occasionnelle ; trompeur si vous écrivez la chaîne dans un fichier de données que vous allez re-analyser plus tard — "null" en tant que chaîne est indiscernable du mot littéral "null."
write(int) écrit un octet, pas un caractère
PrintStream est un OutputStream. La méthode héritée write(int b) écrit l'octet de poids faible :
System.out.write(65); // writes 'A' — the byte 0x41
System.out.write('é'); // writes a single byte 0xE9 — NOT UTF-8 for 'é'La deuxième ligne est incorrecte sur un terminal UTF-8 — 'é' représente deux octets en UTF-8 (0xC3 0xA9), et vous n'en avez écrit qu'un seul. N'utilisez pas write(int) sur un PrintStream pour les caractères ; utilisez print/println, qui passent par le charset configuré.
Un exemple concret : System.out redirigé et inspecté
Le programme ci-dessous capture System.out dans un ByteArrayOutputStream afin que vous puissiez voir exactement quels octets la JVM émet quand vous appelez println. Il exécute le même println("Café") avec deux charsets différents pour rendre le comportement d'encodage concret, démontre checkError() sur un flux défaillant, et montre enfin la différence entre print(Object) pour une référence null et une vérification null délibérée.
Ce qu'il faut retenir de l'exécution :
System.setOut(new PrintStream(buffer, ...))a capturé ce qui serait autrement allé vers la console. Les tests utilisent ce pattern tout le temps. Restaurez l'original avant d'imprimer votre rapport — sinon le rapport va lui aussi dans le buffer, et la confusion s'ensuit.- La ligne "Café" a émis 5 octets en UTF-8 (
43 61 66 C3 A9) et 4 octets en ISO-8859-1 (43 61 66 E9). Même entrée, largeurs d'octets différentes, les deux corrects — l'encodage est le mapping octet → caractère, etPrintStreamrespecte le charset donné à son constructeur. Le constructeur sans charset choisirait celui que la JVM utilise par hasard. - Le bloc avec le flux défaillant a prouvé l'absorption :
printlns'est terminé normalement, l'IOExceptionsous-jacente a disparu, etcheckError()était le seul moyen de découvrir que l'écriture avait échoué. Même contrat quePrintWriter. Si vous vous souciez de l'échec, vous devez le demander. - L'impression de la référence null a produit la chaîne de quatre caractères
null, pas uneNullPointerException. C'est ainsi queprintln(someList)fonctionne même quandsomeListestnull— pratique, mais cela signifie que vous ne pouvez pas distinguer le texte littéral "null" d'une référence null une fois sur le disque. UtilisezObjects.requireNonNullou une vérification null explicite à la frontière si cette distinction importe. - Rien dans l'exemple n'a appelé un
PrintWriter. PourSystem.out, vous n'en avez pas besoin —PrintStreamest le type que Java vous a déjà donné, l'API est identique, et le comportement de flush automatique surprintlnest ce que vous souhaitez au terminal.
La suite
Les treize premiers chapitres de cette partie ont couvert toutes les formes d'I/O en streaming : octets, caractères, bufferisation, primitives, texte formaté. Ils transmettent tous du contenu — des octets et des chars. Le prochain chapitre, Java Serialization, concerne la transmission de graphes d'objets — une structure entière de références liées, écrite dans un flux et reconstruite de l'autre côté, avec une seule annotation sur la classe.