Java DataInput et DataOutput Streams
Lisez et écrivez des types primitifs Java en format binaire portable avec DataInputStream et DataOutputStream.
Jusqu'ici dans cette partie : les octets (bruts ou mis en mémoire tampon) pour les données binaires arbitraires, les caractères pour le texte. Il existe un troisième cas d'usage que les chapitres précédents ne couvrent pas — écrire un int, un double ou un boolean Java dans un fichier et le relire comme le même type, dans un format sur lequel une autre JVM (fonctionnant sur un OS différent, avec un ordre d'octets natif différent) s'accordera.
C'est à cela que servent DataInputStream et DataOutputStream. Ce sont des décorateurs qui se posent sur n'importe quel flux d'octets et ajoutent des méthodes de lecture/écriture typées : writeInt, writeDouble, writeUTF, readInt, readDouble, readUTF. Le format binaire est documenté, fixe, big-endian et portable sur toutes les JVM jamais publiées.
Ce que vous écrivez, vous le relisez
DataOutputStream expose une méthode par type primitif :
void writeBoolean(boolean v); // 1 byte (0 or 1)
void writeByte(int v); // 1 byte (low 8 bits)
void writeShort(int v); // 2 bytes, big-endian
void writeChar(int v); // 2 bytes, big-endian (UTF-16 code unit)
void writeInt(int v); // 4 bytes, big-endian
void writeLong(long v); // 8 bytes, big-endian
void writeFloat(float v); // 4 bytes, IEEE 754
void writeDouble(double v); // 8 bytes, IEEE 754
void writeUTF(String s); // modified UTF-8 with a 2-byte length prefixDataInputStream dispose des méthodes correspondantes readInt, readLong, readUTF, et ainsi de suite. Le contrat est symétrique : écrivez un int avec writeInt, relisez-le avec readInt, obtenez le même nombre, à chaque fois, sur toutes les JVM, sur tous les systèmes d'exploitation.
Trois points à intégrer :
-
Le format n'a pas de séparateurs de champs. Un fichier avec
writeInt(42); writeUTF("alice"); writeDouble(3.14)représente4 + 2 + 5 + 8 = 19octets posés sans marqueur entre eux. Vous devez lire dans le même ordre avec les mêmes types. Il n'y a pas de schéma, pas d'auto-description, pas de récupération si vous vous trompez. -
writeUTFest de l'UTF-8 modifié. Le préfixe est une longueur non signée sur 16 bits (donc 65 535 octets maximum par string), etU+0000est encodé sur deux octets (0xC0 0x80) au lieu du seul octet standard. Le format est incompatible avec l'UTF-8 ordinaire — vous ne pouvez pas lire une stringwriteUTFavec unReader. Utilisez-le uniquement quand les deux côtés sont en Java. -
Big-endian, toujours. L'ordre d'octets natif de la machine varie (x86 est little-endian, les protocoles réseau sont big-endian), mais
DataOutputStreamécrit en big-endian de manière inconditionnelle. C'est ce qui rend le format portable. Si vous avez besoin de little-endian pour un protocole que vous ne contrôlez pas, utilisezjava.nio.ByteBufferà la place — il possède un ordre d'octets configurable.
Quand utiliser les flux de données
Deux cas :
- Vous contrôlez les deux côtés et souhaitez un format binaire simple, compact et portable entre langages. Un fichier de sauvegarde pour un petit jeu Java, un fichier de fixture pour un test unitaire, un cache qui n'a pas besoin de survivre à la version de la JVM. Le format est simple à écrire et à analyser ; vous n'avez pas besoin d'une bibliothèque de sérialisation.
- Vous lisez un format de fichier qui utilise par hasard la disposition des flux de données Java. Les fichiers de classe (
.class), les enregistrements formatés parRandomAccessFile, certains fichiers d'index.jar. Tous ont été écrits avecDataOutputStreamcar le JDK construit lui-même le format.
Quand vous avez besoin d'interopérabilité entre langages (Python, Go, JS), préférez JSON, Protocol Buffers ou MessagePack. Quand vous avez besoin de gestion des versions et d'évolution de schéma, ObjectOutputStream est plus adapté — mais il est plus lourd et possède ses propres écueils.
La règle de fin de fichier
Là où InputStream.read() renvoie -1 en fin de flux, DataInputStream.readInt() (et ses variantes) lève une EOFException. Il n'y a pas de sentinelle intégrée — un int légal peut être n'importe quelle valeur 32 bits, y compris -1, donc le seul moyen de signaler la fin du flux est l'exception.
try (DataInputStream in = new DataInputStream(new BufferedInputStream(Files.newInputStream(path)))) {
try {
while (true) {
int x = in.readInt();
process(x);
}
} catch (EOFException e) {
// normal end of stream
}
}Ce try/catch pour une terminaison normale est la forme idiomatique. Il est inhabituel que le JDK fasse d'une exception un signal de flux de contrôle, mais l'API de lecture typée n'a pas d'autre option — il n'y a pas de valeur à retourner qui ne soit pas aussi un int valide.
Pour les fichiers dont vous contrôlez le format, le meilleur pattern consiste à écrire un préfixe de longueur au début :
out.writeInt(n);
for (int i = 0; i < n; i++) out.writeInt(values[i]);La partie lecture boucle alors n fois et n'a jamais besoin d'attraper EOFException pour le flux de contrôle.
Mettez en mémoire tampon avant de décorer
DataInputStream ne met pas en mémoire tampon. Chaque readInt se traduit par une série d'appels read() sur le flux sous-jacent. Si ce flux sous-jacent est un FileInputStream, chaque readInt correspond à quatre appels système. Enveloppez toujours avec BufferedInputStream en premier :
// Right
DataInputStream in = new DataInputStream(new BufferedInputStream(Files.newInputStream(path)));
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(path)));C'est la pile standard à trois niveaux : fichier → tampon → données. Le même ordre s'applique en écriture. Sans le tampon, vous payez le coût d'un appel système par octet décrit dans le chapitre sur les flux mis en mémoire tampon, multiplié par le nombre d'octets par primitif.
Un exemple concret : un petit format d'enregistrement binaire
Le programme ci-dessous définit un enregistrement binaire minimal — un int id, un UTF name, un double score, un boolean active — et écrit quelques enregistrements dans un fichier temporaire avec DataOutputStream. Il les relit avec DataInputStream en utilisant à la fois le pattern préfixe de comptage et le pattern EOFException, et montre enfin le mode d'échec dû à une incompatibilité de format, où le lecteur et l'écrivain ne s'accordent pas sur les types de champs.
Ce que l'exécution nous apprend :
- La taille du fichier correspond exactement aux octets que vous obtiendriez en additionnant les tailles typées : 4 (comptage) + par enregistrement (4 + UTF préfixé par longueur + 8 + 1). Pas de rembourrage, pas de séparateurs. Un fichier de flux de données, ce sont les octets posés, rien de plus.
- Les deux patterns de lecture ont produit les mêmes trois enregistrements. Le pattern préfixe de comptage est le meilleur quand vous concevez le format ; le pattern EOFException est celui vers lequel vous vous rabattez quand vous ne pouvez pas modifier l'écrivain et que le format est ouvert.
- Le bloc d'incompatibilité de format a écrit deux
ints et lu unlong. Les octets sur disque (00 00 00 2A 00 00 00 63) étaient valides pour les deux interprétations —DataInputStreamn'a aucun moyen de le savoir. Les deux interprétations sont mutuellement cohérentes octet par octet et mutuellement incorrectes au niveau sémantique. C'est le prix d'un format binaire sans schéma : la discipline à la frontière est la seule protection. - Chaque flux était enveloppé
Files.newInputStream→BufferedInputStream→DataInputStream(et de même côté écriture). Sans le tampon,readIntdevient quatre appels système ; la couche flux de données est purement une conversion de format et n'ajoute aucune mise en tampon propre. writeUTFa été utilisé pour le nom. Le format convient pour la communication inter-Java et est inutilisable pour tout autre chose — ne le choisissez pas pour un fichier de configuration que vous pourriez un jour lire en Python. Pour « Java uniquement et je veux que ce soit compact », c'est le bon outil ; pour « quelqu'un d'autre pourrait lire ceci », passez à JSON ou Protobuf.
La suite
Les flux de données gèrent un primitif à la fois et exigent que le lecteur connaisse le format. Le chapitre suivant, Java PrintWriter, revient côté caractères et couvre le décorateur Writer qui ajoute print, println et printf — l'API que vous utilisez sur System.out depuis le chapitre 1, enfin en tant que rédacteur de fichier tel qu'il l'a toujours été.