W3docs

Introduction à Java I/O

Aperçu de Java I/O : flux d'octets et de caractères, I/O bufférisée, java.io vs java.nio.file.

La partie 12 s'est terminée avec un vocabulaire que vous pouvez directement réutiliser ici : les lambdas, Consumer<T> et Supplier<T> comme formes derrière « donne-moi une ligne » et « fais quelque chose avec cette ligne », try-with-resources pour tout ce qui nécessite un nettoyage déterministe, et le pipeline Stream pour les données orientées lignes. Les API I/O de Java ont été conçues autour de ces formes exactes — bien avant que les mots « interface fonctionnelle » n'existent, les objets sous-jacents avaient déjà une méthode chacun, et la façade post-Java-8 a fait le reste.

Cette partie couvre quatre boîtes à outils qui se chevauchent :

  1. Flux java.io — l'API Java 1.0 originale : InputStream/OutputStream pour les octets, Reader/Writer pour les caractères, et les décorateurs Buffered*, Data*, Print* qui les enveloppent.
  2. java.io.File — la classe héritée « cette chaîne est un chemin ». Encore omniprésente dans l'ancien code ; remplacée par java.nio.file.Path pour les nouveaux développements.
  3. java.nio.file — l'API moderne (Java 7+) : Path, Files, et les assistants statiques (Files.readString, Files.writeString, Files.lines, Files.walk) qui réduisent la plupart des opérations sur fichiers à une seule ligne.
  4. La sérialisation — transformer des graphes d'objets en octets et vice versa avec ObjectOutputStream / ObjectInputStream.

Les six premiers chapitres traitent des opérations de fichiers de haut niveau (ouvrir, créer, lire, écrire, supprimer) en utilisant à la fois java.io et java.nio.file afin que vous puissiez voir la même tâche de deux manières différentes. Les chapitres intermédiaires zooment sur les classes de flux elles-mêmes — octets vs caractères, mise en mémoire tampon, données, impression. Les derniers chapitres couvrent la sérialisation et l'API path/walk.

La distinction octets/caractères

Chaque API I/O dans java.io a l'une des deux formes suivantes :

InputStream  /  OutputStream     — byte-oriented   (raw bytes: int read() returns 0..255 or -1)
Reader       /  Writer            — character-oriented (decoded text: int read() returns a char or -1)

Cette distinction n'est pas cosmétique. Les octets sont ce que les disques et les sockets stockent ; les caractères sont ce que les humains lisent. Un fichier .png est constitué d'octets ; un fichier .txt est aussi composé d'octets sur le disque, mais on souhaite généralement le décoder en caractères à l'aide d'un jeu de caractères. Mélanger les deux sans jeu de caractères est la source la plus fréquente de bugs liés aux « caractères étranges » dans l'ancien code Java.

Les classes passerelles — InputStreamReader et OutputStreamWriter — font la conversion entre les deux et prennent un argument Charset. Utilisez StandardCharsets.UTF_8 à moins d'avoir une raison documentée d'utiliser autre chose ; les formes sans argument utilisent le paramètre par défaut de la plateforme, qui varie selon les systèmes d'exploitation et constitue la source classique des bugs « ça marche sur mon Mac, mais c'est cassé sur le serveur Linux ».

Le patron de conception Décorateur

java.io est construit sur le patron de conception Décorateur : un petit ensemble de flux bruts (FileInputStream, FileOutputStream, FileReader, FileWriter) enveloppés dans des fonctionnalités en couches (mise en mémoire tampon, texte ligne par ligne, types primitifs, sortie formatée). Vous composez ce dont vous avez besoin au point d'appel :

// Read a UTF-8 text file line by line:
try (BufferedReader in = new BufferedReader(
        new InputStreamReader(new FileInputStream("a.txt"), StandardCharsets.UTF_8))) {
  String line;
  while ((line = in.readLine()) != null) {
    System.out.println(line);
  }
}

Trois couches, de bas en haut : FileInputStream lit les octets bruts ; InputStreamReader les décode en caractères UTF-8 ; BufferedReader ajoute un tampon en mémoire et la méthode readLine(). Chaque couche est une classe distincte avec une seule responsabilité. Java 11 a simplifié ce schéma exact à une seule ligne — Files.newBufferedReader(path) — mais la décoration reste ce qui se passe en dessous.

try-with-resources est la règle

Chaque flux, lecteur, écrivain et canal dans java.io et java.nio implémente AutoCloseable. La fermeture est importante : un FileOutputStream non fermé peut perdre son tampon de fin ; un socket non fermé provoque une fuite de descripteur de fichier ; un lecteur non fermé sous Windows maintient un verrou que le système d'exploitation ne libèrera pas. La construction try-with-resources (Java 7+) garantit que close() est appelé sur chaque chemin, qu'il soit normal ou exceptionnel :

try (BufferedReader in = Files.newBufferedReader(path)) {
  return in.readLine();
}                                  // close() runs here, even if readLine() throws

Vous pouvez déclarer plus d'une ressource dans le même try ; elles sont fermées dans l'ordre inverse. L'ancien code try/finally qui appelle close() manuellement est presque toujours incorrect — l'exception interne écrase l'exception de fermeture, ou la fermeture elle-même est oubliée sur le chemin d'erreur. Utilisez try-with-resources pour tout ce qui ouvre un handle.

java.io versus java.nio.file

java.io.File (1996) modélisait un chemin comme une String et offrait une poignée d'opérations (exists, isFile, delete, listFiles). Cette classe est encore omniprésente dans l'ancien code, et de nombreuses API retournent ou acceptent encore un File. Mais ses limites sont désormais reconnues par le JDK :

  • Aucune façon de savoir pourquoi une opération a échoué — file.delete() retourne false pour « le fichier n'existe pas », « permission refusée » et « le fichier est ouvert ». Impossible de faire la différence.
  • Aucune prise en charge des liens symboliques, des attributs de fichier, des permissions, ni des opérations atomiques.
  • Aucune façon de parcourir une arborescence de répertoires sans écrire soi-même la récursion.

java.nio.file (Java 7) le remplace. Path est le nouveau type « voici un chemin », et Files est une classe utilitaire static avec environ 80 méthodes pour tout ce que vous pourriez vouloir faire avec :

Path p = Path.of("data", "users.txt");                       // platform-independent path
String text = Files.readString(p, StandardCharsets.UTF_8);   // whole file, one call
List<String> lines = Files.readAllLines(p, StandardCharsets.UTF_8);
Files.writeString(p, "hello\n", StandardCharsets.UTF_8);
try (Stream<String> s = Files.lines(p, StandardCharsets.UTF_8)) {
  s.filter(l -> !l.isBlank()).forEach(System.out::println);
}

Deux points à noter. Premièrement, Files.lines(path) retourne un Stream<String> — le pipeline de flux appris dans la partie 12 lit directement les fichiers. Deuxièmement, le flux possède un handle de fichier ouvert, donc le wrapper try-with-resources est obligatoire — sans lui, le fichier reste ouvert jusqu'au prochain GC.

Tout au long de la partie 13, nous montrerons les deux API côte à côte. Le nouveau code devrait d'abord utiliser java.nio.file ; les chapitres sur le code hérité existent parce que vous rencontrerez les anciennes formes dans toute base de code antérieure à Java 11.

Où va cette partie

  • Le prochain chapitre, Java File Class, parcourt l'API héritée java.io.File — ses méthodes de requête, son listage, et les limites qui ont motivé java.nio.file.
  • Les quatre chapitres suivants (Creating Files, Reading Files, Writing Files, Deleting Files) couvrent les opérations de haut niveau « faire une chose à un fichier » en utilisant les deux API.
  • Les chapitres sur les flux d'octets, de caractères et les flux bufférisés zooment ensuite sur la pile de décorateurs java.io sous-jacente.
  • La sérialisation, puis Path, Files et l'API de parcours de répertoires ferment la partie.

Un exemple concret : la même tâche, quatre façons

Le programme ci-dessous écrit un court fichier texte de quatre manières — une fois avec le moderne Files.writeString, une fois avec le classique FileWriter + try-with-resources, une fois décoré avec BufferedWriter, et une fois avec PrintWriter pour la sortie formatée. Il relit ensuite le fichier de deux façons — une fois avec Files.readString (fichier entier, un seul appel) et une fois avec Files.lines sous forme de Stream<String> filtré avec un Predicate<String>. L'exemple utilise un fichier temporaire système pour fonctionner dans n'importe quel environnement.

java— editable, runs on the server

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

  • Le même fichier a été écrit de quatre manières différentes. Files.writeString est le chemin le plus court pour « déposer cette chaîne dans ce fichier » ; FileWriter est le classique écrivain brut ; BufferedWriter ajoute un tampon en mémoire (pratique quand on écrit de nombreux petits morceaux) ; PrintWriter ajoute printf. Chacun a écrasé le contenu précédent car le mode d'ouverture par défaut est « tronquer puis écrire » — il faut passer StandardOpenOption.APPEND (traité dans le chapitre sur l'écriture) pour ajouter à un fichier.
  • Chaque écrivain s'est exécuté dans un try-with-resources. Omettre cela sur un écrivain bufférisé est le bug où les derniers caractères n'atteignent jamais le disque — close() est ce qui vide le tampon de fin.
  • Files.readString a retourné tout le fichier sous forme d'une seule String — acceptable pour les petits fichiers, le mauvais choix pour un journal de 4 Go. Files.lines a retourné un Stream<String> que vous pouvez pipeliner à travers filter, map et count sans charger tout le fichier en mémoire. Le try-with-resources obligatoire sur le flux est dû au fait que le flux possède un handle de fichier ouvert.
  • La ligne Predicate<String> nameLine = l -> l.startsWith("name") utilise le même vocabulaire que la partie 12 — une valeur Predicate, passée à Stream.filter. Files.lines est l'endroit où l'API de flux rencontre l'API I/O.
  • Files.deleteIfExists est la suppression sans exception : retourne true si le fichier a été supprimé, false s'il n'existait pas. Le File.delete() hérité retourne un boolean pour « supprimé » et « impossible de supprimer » — Files distingue les deux en levant une exception.

La suite

Avant que l'API moderne java.nio.file ne prenne en charge le reste de la partie, le prochain chapitre couvre la classe que vous rencontrerez en premier dans toute base de code plus ancienne : Java File Class. C'est le type hérité pour les chemins et les métadonnées — limité, mais omniprésent — et voir ce qu'il ne peut pas faire justifie l'existence de Path et Files.

Pratique

Pratique
Pourquoi `java.io` sépare-t-il `InputStream`/`OutputStream` de `Reader`/`Writer` ?
Pourquoi `java.io` sépare-t-il `InputStream`/`OutputStream` de `Reader`/`Writer` ?
Was this page helpful?