34. Streams I/O Java
Table des matières
- 34.1 Qu’est-ce qu’un flux I/O en Java
- 34.2 Flux d’octets vs flux de caractères
- 34.3 Flux de bas niveau vs flux de haut niveau
- 34.3.1 Flux de bas niveau Node-Streams
- 34.3.2 Flux de bas niveau courants
- 34.3.3 Flux de haut niveau Filter--Processing-Streams
- 34.3.4 Flux de haut niveau courants
- 34.3.5 Règles de chaînage des flux et erreurs courantes
- 34.3.5.1 Règle fondamentale de chaînage
- 34.3.5.2 Incompatibilité flux d’octets vs flux de caractères
- 34.3.5.3 Chaînage invalide erreur-de-compilation
- 34.3.5.4 Pont entre flux d’octets et flux de caractères
- 34.3.5.5 Patron correct de conversion
- 34.3.5.6 Règles d’ordre dans les chaînes de flux
- 34.3.5.7 Ordre logique correct
- 34.3.5.8 Règle de gestion des ressources
- 34.3.5.9 Pièges courants
- 34.4 Classes de base principales de javaio et méthodes clés
- 34.5 Flux tamponnés et performance
- 34.6 java io vs java nio et java nio file
- 34.7 Quand utiliser quelle API
- 34.8 Pièges courants et conseils
Ce chapitre fournit une explication détaillée des flux I/O Java.
Il couvre les flux classiques java.io, les compare à java.nio / java.nio.file, et explique les principes de conception, les API, les cas limites et les distinctions pertinentes.
34.1 Qu’est-ce qu’un flux I/O en Java ?
Un flux I/O représente un flux de données entre un programme Java et une source ou une destination externe.
Les données circulent de manière séquentielle, comme de l’eau dans un tuyau.
- Un flux n’est pas une structure de données ; il ne stocke pas les données de manière permanente
- Les flux sont unidirectionnels (entrée OU sortie)
- Les flux abstraient la source sous-jacente (fichier, réseau, mémoire, périphérique)
- Les flux fonctionnent de manière bloquante, synchrone (I/O classique)
En Java, les flux sont organisés autour de deux dimensions majeures :
Direction: Entrée vs SortieType de données: Octets vs Caractères
34.2 Flux d’octets vs flux de caractères
Java distingue les flux selon l’unité de données qu’ils traitent.
34.2.1 Flux d’octets
- Fonctionnent avec des octets bruts 8 bits
- Utilisés pour les données binaires (images, audio, PDF, ZIP)
- Classes de base :
InputStreametOutputStream
34.2.2 Flux de caractères
- Fonctionnent avec des caractères Unicode 16 bits
- Gèrent automatiquement l’encodage des caractères
- Classes de base :
ReaderetWriter
34.2.3 Tableau récapitulatif
| Aspect | Flux d’octets | Flux de caractères |
|---|---|---|
Unité de données |
byte (8 bits) | char (16 bits) |
Gestion de l’encodage |
Aucune | Oui (conscient du charset) |
Classes de base |
InputStream / OutputStream | Reader / Writer |
Usage typique |
Fichiers binaires | Fichiers texte |
Focus |
I/O bas niveau | Traitement de texte |
34.3 Flux de bas niveau vs flux de haut niveau
Les flux dans java.io suivent un pattern decorator. Les flux sont empilés pour ajouter des fonctionnalités.
34.3.1 Flux de bas niveau (Node Streams)
Les flux de bas niveau se connectent directement à une source ou un puits de données.
- Ils savent lire/écrire des octets ou des caractères
- Ils ne fournissent PAS de mise en tampon, de formatage ou de gestion d’objets
34.3.2 Flux de bas niveau courants
| Classe de flux | Objectif |
|---|---|
FileInputStream |
Lire des octets depuis un fichier |
FileOutputStream |
Écrire des octets dans un fichier |
FileReader |
Lire des caractères depuis un fichier |
FileWriter |
Écrire des caractères dans un fichier |
- Exemple : flux d’octets de bas niveau
try (InputStream in = new FileInputStream("data.bin")) {
int b;
while ((b = in.read()) != -1) {
System.out.println(b);
}
}
Note
Les flux de bas niveau sont rarement utilisés seuls dans des applications réelles en raison de performances médiocres et de fonctionnalités limitées.
34.3.3 Flux de haut niveau (Filter / Processing Streams)
Les flux de haut niveau enveloppent d’autres flux pour ajouter des fonctionnalités.
- Mise en tampon
- Conversion de type de données
- Sérialisation d’objets
- Lecture/écriture de primitifs
34.3.4 Flux de haut niveau courants
| Classe de flux | Ajoute des fonctionnalités |
|---|---|
BufferedInputStream |
Mise en tampon |
BufferedReader |
Lecture par lignes |
DataInputStream |
Types primitifs |
ObjectInputStream |
Sérialisation d’objets |
PrintWriter |
Sortie texte formatée |
- Exemple : chaînage de flux
try (BufferedReader reader =
new BufferedReader(
new InputStreamReader(
new FileInputStream("text.txt")))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
34.3.5 Règles de chaînage des flux et erreurs courantes
L’exemple précédent illustre le chaînage des flux, un concept central de java.io basé sur le pattern decorator.
Chaque flux enveloppe un autre flux, ajoutant des fonctionnalités tout en préservant une hiérarchie de types stricte.
34.3.5.1 Règle fondamentale de chaînage
Un flux ne peut envelopper qu’un autre flux d’un niveau d’abstraction compatible.
- Les flux d’octets ne peuvent envelopper que des flux d’octets
- Les flux de caractères ne peuvent envelopper que des flux de caractères
- Les flux de haut niveau nécessitent un flux de bas niveau sous-jacent
Note
Vous ne pouvez pas mélanger arbitrairement InputStream avec Reader ou OutputStream avec Writer.
34.3.5.2 Incompatibilité flux d’octets vs flux de caractères
Une erreur très courante consiste à tenter d’envelopper un flux d’octets directement avec une classe basée sur les caractères (ou inversement).
34.3.5.3 Chaînage invalide (erreur de compilation)
BufferedReader reader =
new BufferedReader(new FileInputStream("text.txt"));
Note
Cela échoue parce que BufferedReader attend un Reader, pas un InputStream.
34.3.5.4 Pont entre flux d’octets et flux de caractères
Pour convertir entre les flux basés sur les octets et ceux basés sur les caractères, Java fournit des classes passerelles qui effectuent un décodage/encodage explicite du charset.
InputStreamReaderconvertit octets → caractèresOutputStreamWriterconvertit caractères → octets
34.3.5.5 Patron correct de conversion
BufferedReader reader =
new BufferedReader(
new InputStreamReader(new FileInputStream("text.txt")));
Note
La passerelle gère le décodage des caractères en utilisant un charset (par défaut ou explicite).
34.3.5.6 Règles d’ordre dans les chaînes de flux
L’ordre d’enveloppement n’est pas arbitraire.
- Le flux de bas niveau doit être le plus interne
- Les passerelles (si nécessaires) viennent ensuite
- Les flux tamponnés ou de traitement viennent en dernier
34.3.5.7 Ordre logique correct
FileInputStream → InputStreamReader → BufferedReader
34.3.5.8 Règle de gestion des ressources
Fermer le flux le plus externe ferme automatiquement tous les flux enveloppés.
Note
C’est pourquoi try-with-resources devrait référencer uniquement le flux de plus haut niveau.
34.3.5.9 Pièges courants
- Essayer de tamponner un flux du mauvais type
- Oublier la passerelle entre flux d’octets et flux de caractères
- Supposer que
Readerfonctionne avec des données binaires - Utiliser le charset par défaut involontairement
- Fermer manuellement les flux internes (risque de double-close) :
close()sur l’enveloppe externe suffit et est recommandé
34.4 Classes de base principales de java.io et méthodes clés
Le package java.io est construit autour d’un petit ensemble de classes de base abstraites.
Comprendre ces classes et leurs contrats est essentiel, car toutes les classes I/O concrètes s’appuient sur elles.
34.4.1 InputStream
Classe de base abstraite pour l’entrée orientée octets. Tous les flux d’entrée lisent des octets bruts (valeurs 8 bits) depuis une source telle qu’un fichier, un socket réseau, ou un buffer mémoire.
34.4.1.1 Méthodes clés
| Méthode | Description |
|---|---|
int read() |
Lit un octet (0–255) ; retourne -1 à la fin du flux |
int read(byte[]) |
Lit des octets dans un buffer ; retourne le nombre d’octets lus ou -1 |
int read(byte[], int, int) |
Lit jusqu’à length octets dans une tranche de buffer |
int available() |
Octets lisibles sans bloquer (indice, pas garantie) |
void close() |
Libère la ressource sous-jacente |
Note
Les méthodes read() sont bloquantes par défaut.
Elles suspendent le thread appelant jusqu’à ce que des données soient disponibles, que la fin de flux soit atteinte, ou qu’une erreur I/O se produise.
La méthode read() à octet unique est principalement un primitif de bas niveau.
En pratique, lire un octet à la fois est inefficace et devrait presque toujours être évité au profit de lectures tamponnées.
34.4.1.2 Exemple d’utilisation typique
try (InputStream in = new FileInputStream("data.bin")) {
byte[] buffer = new byte[1024];
int count;
while ((count = in.read(buffer)) != -1) {
// traiter buffer[0..count-1]
}
}
34.4.2 OutputStream
Classe de base abstraite pour la sortie orientée octets.
Elle représente une destination où des octets bruts peuvent être écrits.
34.4.2.1 Méthodes clés
| Méthode | Description |
|---|---|
void write(int b) |
Écrit les 8 bits de poids faible de l’entier |
void write(byte[]) |
Écrit un tableau d’octets entier |
void write(byte[], int, int) |
Écrit une tranche d’un tableau d’octets |
void flush() |
Force l’écriture des données tamponnées |
void close() |
Effectue flush et libère la ressource |
Note
Appeler close() appelle implicitement flush().
Ne pas faire flush ou close sur un OutputStream peut entraîner une perte de données.
34.4.2.2 Exemple d’utilisation typique
try (OutputStream out = new FileOutputStream("out.bin")) {
out.write(new byte[] {1, 2, 3, 4});
out.flush();
}
34.4.3 Reader et Writer
Reader et Writer sont les équivalents orientés caractères de InputStream et OutputStream.
Ils fonctionnent sur des caractères Unicode 16 bits au lieu d’octets bruts.
| Classe | Direction | Basée sur caractères | Consciente de l’encodage |
|---|---|---|---|
Reader |
Entrée | Oui | Oui |
Writer |
Sortie | Oui | Oui |
Readers et Writers impliquent toujours un charset, explicitement ou implicitement.
Cela en fait l’abstraction correcte pour le traitement de texte.
34.4.3.1 Gestion du charset
Reader reader = new InputStreamReader(
new FileInputStream("file.txt"),
StandardCharsets.UTF_8
);
Note
InputStreamReader et OutputStreamWriter sont des classes passerelles.
Ils convertissent entre flux d’octets et flux de caractères en utilisant un charset.
34.5 Flux tamponnés et performance
Les flux tamponnés enveloppent un autre flux et ajoutent un buffer en mémoire.
Au lieu d’interagir avec le système d’exploitation à chaque read ou write, les données sont accumulées en mémoire et transférées en plus gros blocs.
BufferedInputStream/BufferedOutputStreampour les flux d’octetsBufferedReader/BufferedWriterpour les flux de caractères
Note
Les flux tamponnés sont des decorators : ils ne remplacent pas le flux sous-jacent, ils l’améliorent en ajoutant un comportement de mise en tampon.
34.5.1 Pourquoi la mise en tampon compte
| Aspect | Non tamponné | Tamponné |
|---|---|---|
Appels système |
Fréquents | Réduits |
Performance |
Médiocre | Élevée |
Utilisation mémoire |
Minimale | Légèrement plus élevée |
Les appels système sont des opérations coûteuses.
La mise en tampon les minimise en regroupant plusieurs lectures ou écritures logiques en moins d’opérations I/O physiques.
34.5.2 Comment fonctionne la lecture non tamponnée
Dans un flux non tamponné, chaque appel à read() peut entraîner un appel système natif.
C’est particulièrement inefficace lorsqu’on lit de grandes quantités de données.
try (InputStream in = new FileInputStream("data.bin")) {
int b;
while ((b = in.read()) != -1) {
// chaque read() peut déclencher un appel système
}
}
Note
Lire octet par octet sans mise en tampon est presque toujours un anti-pattern de performance.
34.5.3 Comment fonctionne BufferedInputStream
BufferedInputStream lit en interne un grand bloc d’octets dans un buffer.
Les appels read() suivants sont servis directement depuis la mémoire jusqu’à ce que le buffer soit vide.
try (InputStream in =
new BufferedInputStream(new FileInputStream("data.bin"))) {
int b;
while ((b = in.read()) != -1) {
// la plupart des lectures sont servies depuis la mémoire, pas depuis l’OS
}
}
Note
Le programme appelle toujours read() de manière répétée, mais le système d’exploitation n’est accédé que lorsque le buffer interne doit être rempli à nouveau.
34.5.4 Exemple de sortie tamponnée
La sortie tamponnée accumule les données en mémoire et les écrit en plus gros blocs.
L’opération flush() force l’écriture immédiate du buffer.
try (OutputStream out =
new BufferedOutputStream(new FileOutputStream("out.bin"))) {
for (int i = 0; i < 1_000; i++) {
out.write(i);
}
out.flush(); // force les données tamponnées sur disque
}
Note
close() appelle automatiquement flush().
Appeler flush() explicitement est utile lorsque les données doivent être visibles immédiatement.
34.5.5 BufferedReader vs Reader
BufferedReader ajoute une **lecture par lignes** efficace au-dessus d’un Reader.
Sans mise en tampon, chaque caractère lu peut impliquer un appel système.
try (BufferedReader reader =
new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
Note
La méthode readLine() n’est disponible que sur BufferedReader (pas sur Reader), car elle s’appuie sur la mise en tampon pour détecter efficacement les limites de ligne.
34.5.6 Exemple de BufferedWriter
try (BufferedWriter writer =
new BufferedWriter(new FileWriter("file.txt"))) {
writer.write("Hello");
writer.newLine();
writer.write("World");
}
BufferedWriter minimise l’accès disque et fournit des méthodes pratiques telles que newLine().
Note
Enveloppez toujours les flux de fichiers avec une mise en tampon sauf s’il y a une forte raison de ne pas le faire
Préférez BufferedReader / BufferedWriter pour le texte
Préférez BufferedInputStream / BufferedOutputStream pour les données binaires
34.6 java.io vs java.nio (et java.nio.file)
Les applications Java modernes favorisent de plus en plus les API NIO et NIO.2, mais java.io reste fondamental et largement utilisé.
34.6.1 Différences conceptuelles
| Aspect | java.io | java.nio / nio.2 |
|---|---|---|
Modèle de programmation |
Basé sur les flux | Basé sur buffers / channels |
I/O bloquantes |
bloquantes par défaut | Capable de non-bloquant |
API fichiers |
File | Path + Files |
Scalabilité |
Limitée | Élevée |
Introduit |
Java 1.0 | Java 4 / Java 7 |
Note
java.nio ne remplace pas java.io.
De nombreuses classes NIO s’appuient en interne sur des flux ou coexistent avec eux.
34.6.2 java.nio (I/O de fichier moderne)
Le package java.nio.file (NIO.2) fournit une API fichiers de haut niveau, expressive et plus sûre.
C’est l’approche préférée pour les opérations sur fichiers en Java 11+.
Exemple : lire un fichier (NIO)
Path path = Path.of("file.txt");
List<String> lines = Files.readAllLines(path);
Code java.io équivalent
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
34.7 Quand utiliser quelle API
| Scénario | API recommandée |
|---|---|
Lecture/écriture simple de fichier |
java.nio.file.Files |
Streaming binaire |
InputStream / OutputStream |
Traitement de texte en caractères |
Reader / Writer |
Serveurs haute performance |
java.nio.channels |
API legacy |
java.io |
34.8 Pièges courants et conseils
- La fin de fichier est indiquée par -1, pas par une exception
- Fermer un flux wrapper ferme automatiquement le flux enveloppé
BufferedReader.readLine()supprime les séparateurs de ligneInputStreamReaderimplique toujours un charset- Les méthodes utilitaires Files lancent des IOException checked
available()ne doit pas être utilisé pour détecter EOF
Note
La plupart des bugs I/O proviennent d’hypothèses incorrectes sur le blocking, la mise en tampon ou l’encodage des caractères.