W3docs

Flux d'octets Java

Lisez et écrivez des données binaires en Java avec InputStream, OutputStream, FileInputStream et FileOutputStream.

Le chapitre 1 a présenté la conception de java.io comme une pile de décorateurs : un flux brut en bas, des couches de fonctionnalités enveloppées autour, la couche la plus haute exposant l'API que vous appelez. Les six premiers chapitres de cette partie vivaient au sommet de cette pile — Files.readString, Files.lines, Files.writeString. Ce chapitre descend d'un niveau vers l'abstraction orientée octets sur laquelle toute la pile est construite : InputStream et OutputStream.

Chaque fichier, socket, pipe et tampon en mémoire dans java.io est — au fond — un flux d'octets. Même un fichier texte UTF-8 est des octets sur le disque ; la vue "c'est du texte" provient d'un Reader superposé sur un InputStream. Connaître l'API d'octets est important lorsque les données ne sont pas du texte (images, audio, archives, protocoles réseau), lorsque vous devez copier des octets sans les décoder, et lorsque vous voulez comprendre ce que les API de plus haut niveau font réellement.

Le contrat InputStream

InputStream est une classe abstraite à une seule méthode. Cette méthode est :

public abstract int read() throws IOException;

Elle retourne le prochain octet en tant qu'int dans la plage 0..255, ou -1 lorsque le flux est épuisé. Le int n'est pas une erreur : un byte en Java est signé (-128..127), mais le contrat du flux est non signé, donc le type de retour plus large rend "fin de flux" (-1) distinguable d'une vraie valeur d'octet (0xFF se lit comme 255, pas -1).

Trois autres méthodes sont définies par-dessus read() et sont celles que vous appelez habituellement :

int read(byte[] buf);                  // read up to buf.length bytes; return count or -1
int read(byte[] buf, int off, int len); // same, into a slice
byte[] readAllBytes();                  // Java 9+: read everything into a byte[]
long transferTo(OutputStream out);       // Java 9+: pipe straight to a sink, no copy loop

readAllBytes() est la commodité pour les petits fichiers ; transferTo est la commodité pour copier sans décoder. Pour tout le reste, il y a la boucle de lecture en tampon, qui est la forme canonique :

byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) {
  out.write(buf, 0, n);                 // n bytes, not buf.length — the last chunk is short
}

Deux choses à intérioriser. Premièrement, les appels read(byte[]) retournent combien d'octets ont été réellement lus, pas toujours buf.length. La dernière lecture est presque toujours partielle ; traiter le tampon comme plein corrompt les données. Deuxièmement, read() et read(byte[]) sont bloquants — ils retournent quand au moins un octet est disponible ou que le flux se termine. Ils ne retournent pas prématurément sur un disque lent ou une socket lente.

Sauter, regarder en avant et revenir

InputStream définit également trois méthodes auxquelles vous faites moins souvent appel mais que vous devriez reconnaître :

long skip(long n);     // discard up to n bytes without copying them anywhere
int  available();      // bytes you can read right now without blocking — an estimate, not a length
boolean markSupported();
void mark(int readAheadLimit);  // remember this position
void reset();                    // jump back to the last mark

Deux pièges se cachent ici. available() n'est pas la taille du flux — pour un fichier c'est souvent le cas, mais pour une socket c'est "octets déjà mis en tampon", ce qui peut être 0 en plein transfert. N'écrivez jamais new byte[in.available()] en supposant que vous avez tout lu. Et mark/reset ne fonctionnent que si markSupported() retourne true ; un FileInputStream brut retourne false, donc enveloppez-le dans un BufferedInputStream (chapitre suivant) lorsque vous avez besoin de regarder en avant et de revenir en arrière.

Le contrat OutputStream

La classe miroir est OutputStream, également une méthode abstraite :

public abstract void write(int b) throws IOException;

Elle écrit les 8 bits de poids faible de b et ignore le reste. Les surcharges de commodité sont :

void write(byte[] buf);                    // write the whole array
void write(byte[] buf, int off, int len);  // write a slice — this is the one you usually want
void flush();                               // push buffered data to the OS
void close();                               // flush + release resources

flush() n'a d'importance que si le flux est mis en tampon. Un FileOutputStream brut ne l'est pas — chaque write appelle l'OS — donc flush est une opération nulle. BufferedOutputStream (chapitre suivant) est là où la mise en tampon, et la nécessité de vider, vivent.

close() appelle flush() en premier. C'est pourquoi "avoir oublié de fermer le flux mis en tampon" tronque silencieusement le fichier : le tampon de queue est en mémoire, attendant un vidage qui ne vient jamais.

Flux d'octets concrets

Les sous-classes concrètes que vous instancierez réellement :

ClasseCe qu'elle encapsule
FileInputStream / FileOutputStreamUn fichier sur le disque. Ouvre un descripteur de fichier.
ByteArrayInputStream / ByteArrayOutputStreamUn byte[] en mémoire. Utile pour les tests et pour capturer la sortie.
BufferedInputStream / BufferedOutputStreamUne vue mise en tampon d'un autre flux.
PipedInputStream / PipedOutputStreamUn pipe producteur/consommateur entre threads.
DataInputStream / DataOutputStreamSuperposé sur un flux d'octets pour lire/écrire des types primitifs de façon portable.

FileInputStream et FileOutputStream sont les flux de fichiers bruts. Ils sont non mis en tampon : chaque read()/write() est un appel système. C'est catastrophique pour les boucles octet par octet — des millions d'appels système — et simplement acceptable pour les lectures groupées avec un tampon de 8 Ko ou plus. Le chapitre sur la mise en tampon est ce qui rend l'API octet par octet abordable.

// Raw, unbuffered — fine for chunked reads
try (FileInputStream in = new FileInputStream("photo.jpg")) {
  byte[] buf = new byte[8192];
  int n;
  while ((n = in.read(buf)) != -1) { /* process buf[0..n] */ }
}

// Equivalent one-liner, Java 7+
byte[] all = Files.readAllBytes(Path.of("photo.jpg"));

Files.readAllBytes est le bon appel pour les petits fichiers ; pour tout ce qui pourrait ne pas tenir en mémoire, la boucle groupée est la forme sûre.

Trois modèles à mémoriser

Les trois choses que vous faites encore et encore avec les flux d'octets :

// 1. Copy a file
try (InputStream in  = Files.newInputStream(src);
     OutputStream out = Files.newOutputStream(dst)) {
  in.transferTo(out);                                 // Java 9+: no manual loop
}
// Java 7+ one-liner: Files.copy(src, dst);

// 2. Read everything into memory
byte[] all = Files.readAllBytes(path);                 // small-file shortcut

// 3. Build a byte[] you don't know the size of in advance
ByteArrayOutputStream baos = new ByteArrayOutputStream();
in.transferTo(baos);
byte[] bytes = baos.toByteArray();

ByteArrayOutputStream est le récepteur d'octets "qui grandit au fur et à mesure". C'est ainsi que le JDK lui-même implémente readAllBytes() sur des flux dont la longueur n'est pas connue à l'avance. Il ne lève jamais d'exception sur write (jusqu'à ce que vous manquiez de tas) et n'a pas de sémantique close() qui mérite réflexion, ce qui en fait le dispositif de test standard pour "capturer ce que cet écrivain a produit."

Quand utiliser les flux d'octets

La réponse honnête : quand les données ne sont pas du texte. Tout ce qui est binaire — images, audio, vidéo, archives (.zip, .tar), exécutables, protocol buffers, formats de fichiers personnalisés — est des octets et reste des octets.

Lorsque les données sont du texte, préférez le côté flux de caractères (Reader/Writer, chapitre suivant) ou les modernes Files.readString / Files.lines. Lire un fichier texte comme des octets bruts et décoder manuellement est la façon standard d'inventer votre propre bug de jeu de caractères — les caractères multi-octets UTF-8 se retrouvent divisés entre les appels read() et vous les réassemblez mal. La couche Reader existe précisément pour que vous n'ayez pas à vous en préoccuper.

Un exemple pratique : copier, hacher et capturer

Le programme ci-dessous met en pratique l'API de flux d'octets de bout en bout. Il écrit un petit fichier binaire (un en-tête plus une charge utile), le relit par morceaux dans une somme de contrôle, le copie dans un second fichier avec transferTo, et capture une autre copie dans un ByteArrayOutputStream pour que vous puissiez voir le récepteur en mémoire en action. Les fichiers temporaires se nettoient eux-mêmes à la sortie.

java— editable, runs on the server

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

  • Le côté écriture a utilisé Files.newOutputStream — une méthode fabrique de style Files qui retourne un OutputStream simple. Une fois que vous l'avez, l'API est la même que Java a depuis la version 1.0. La méthode fabrique vous épargne simplement la construction d'un FileOutputStream et les inquiétudes concernant les options d'ouverture.
  • La boucle de lecture a utilisé n, pas buf.length, lors de l'appel à crc.update. La raison est dans la ligne de sortie : "read in N chunks." Le tampon était de 256 octets et le fichier de 1004 octets, donc le dernier morceau était court. Utiliser buf.length aurait haché des données parasites au-delà des données réelles.
  • in.transferTo(out) est la boucle de copie testée du JDK. Elle est mesurément plus rapide qu'une boucle écrite à la main sur la plupart des JVM car elle peut utiliser un tampon de 16 Ko et ignorer les vérifications de point sûr, et c'est une ligne au lieu de cinq. Utilisez-la chaque fois que vous écririez autrement une boucle while ((n = in.read(buf)) != -1) sans autre logique à l'intérieur.
  • ByteArrayOutputStream s'est branché directement dans transferTo. Il ressemble à un fichier mais vit en mémoire — la même API. Cette symétrie est ce qui rend java.io testable : passez un ByteArrayInputStream pour la source, un ByteArrayOutputStream pour le récepteur, et vous pouvez tester unitairement du code qui "écrit dans un fichier" sans toucher le disque.
  • Le bloc final a affiché 255 puis -1. C'est le contrat : 0xFF est une valeur d'octet légale et se relit comme 255 ; -1 est le sentinelle hors bande qui dit "plus d'octets." Traiter le retour comme un byte (au lieu d'un int) et comparer == -1 traiterait silencieusement un vrai 0xFF comme fin de flux. Stockez toujours le résultat dans un int et comparez à -1 avant de caster.

La suite

Les octets sont la bonne abstraction pour les données binaires. Le prochain chapitre, Flux de caractères Java, couvre la hiérarchie parallèle pour le texte — Reader et Writer, le pont de jeu de caractères, et pourquoi "juste new FileReader(path)" est la source classique des bugs "ça marche sur ma machine, cassé sur le serveur".

Exercices

Pratique
Que retourne `InputStream.read()` lorsque le flux contient un seul octet de valeur `0xFF`, et que retourne-t-il à l'appel suivant ?
Que retourne `InputStream.read()` lorsque le flux contient un seul octet de valeur `0xFF`, et que retourne-t-il à l'appel suivant ?
Pratique
Dans la boucle `while ((n = in.read(buf)) != -1) out.write(buf, 0, n);`, pourquoi passer `n` plutôt que `buf.length` à `write` ?
Dans la boucle `while ((n = in.read(buf)) != -1) out.write(buf, 0, n);`, pourquoi passer `n` plutôt que `buf.length` à `write` ?
Was this page helpful?