Comment fonctionnent les flux d’I/O en Java?

Maintenant que nous avons les bases, passons aux stream d’I/O, qui sont bien plus intéressants. Dans cette section, nous vous montrons comment utiliser les stream d’I/O pour lire et écrire des données. Le terme “I/O” fait référence à la nature de l’accès aux données, soit en lisant les données à partir d’une ressource (entrée) soit en écrivant des données vers une ressource (sortie).

Lorsque nous parlons de stream d’I/O dans ce chapitre, nous faisons référence à ceux qui se trouvent dans l’API java.io. Si nous disons simplement flux, cela désigne ceux du Chapitre 10. Nous convenons que la terminologie peut être un peu déroutante !

Comprendre les Fondamentaux des stream d’I/O

Le contenu d’un fichier peut être accédé ou écrit via un stream d’I/O, qui est une liste d’éléments de données présentés séquentiellement. Un stream d’I/O peut être conceptuellement considéré comme un long, presque interminable flux d’eau avec des données présentées une vague à la fois.

Nous démontrons ce principe dans la Figure 14.5. Le stream d’I/O est si grand qu’une fois que nous commençons à le lire, nous n’avons aucune idée d’où se trouve le début ou la fin. Nous avons simplement un pointeur vers notre position actuelle dans le stream d’I/O et nous lisons les données un bloc à la fois.

FIGURE 14.5 Représentation visuelle d’un stream d’I/O

Chaque type de stream d’I/O segmente les données en vague ou bloc d’une manière particulière. Par exemple, certaines classes de stream d’I/O lisent ou écrivent des données sous forme d’octets individuels. D’autres classes de stream d’I/O lisent ou écrivent des caractères individuels ou des chaînes de caractères. De plus, certaines classes de stream d’I/O lisent ou écrivent des groupes plus importants d’octets ou de caractères à la fois, en particulier celles avec le mot Buffered dans leur nom.

Bien que l’API java.io soit pleine de stream d’I/O qui gèrent des caractères, des chaînes, des groupes d’octets, etc., presque tous sont construits sur la base de la lecture ou de l’écriture d’un octet individuel ou d’un tableau d’octets à la fois. Des stream d’I/O de plus haut niveau existent pour des raisons de commodité ainsi que de performance.

Bien que les stream d’I/O soient couramment utilisés avec les fichiers, ils sont plus généralement utilisés pour gérer la lecture/écriture de toute source de données séquentielle. Par exemple, vous pourriez construire une application Java qui soumet des données à un site web en utilisant un flux de sortie et lit le résultat via un flux d’entrée.

Les stream d’I/O Peuvent Être Volumineux

Lors de l’écriture de code où vous ne savez pas quelle sera la taille du stream d’I/O à l’exécution, il peut être utile de visualiser un stream d’I/O comme étant si grand que toutes les données qu’il contient ne pourraient pas tenir en mémoire. Par exemple, un fichier de 1 To ne pourrait pas être stocké entièrement en mémoire par la plupart des systèmes informatiques (au moment où ce livre est écrit). Le fichier peut quand même être lu et écrit par un programme avec très peu de mémoire, car le stream d’I/O permet à l’application de se concentrer uniquement sur une petite portion du stream d’I/O à un moment donné.

Apprendre la Nomenclature des stream d’I/O

L’API java.io fournit de nombreuses classes pour créer, accéder et manipuler des stream d’I/O — tellement que cela tend à submerger de nombreux nouveaux développeurs Java. Restez calme ! Nous passons en revue les principales différences entre chaque classe de stream d’I/O et vous montrons comment les distinguer.

L’objectif de cette section est de vous familiariser avec la terminologie commune et les conventions de nommage utilisées avec les stream d’I/O.

Stockage des Données en Octets

Les données sont stockées dans un système de fichiers (et en mémoire) sous forme de 0 ou 1, appelé un bit. Comme il est vraiment difficile pour les humains de lire/écrire des données qui ne sont que des 0 et des 1, ils sont regroupés en un ensemble de 8 bits, appelé un octet.

Qu’en est-il du type primitif byte de Java ? Comme vous l’apprendrez plus tard, lorsque nous utilisons des stream d’I/O, les valeurs sont souvent lues ou écrites en utilisant des valeurs et des tableaux de bytes.

Flux d’Octets vs. Flux de Caractères

L’API java.io définit deux ensembles de classes de stream d’I/O pour lire et écrire des stream d’I/O : les stream d’I/O d’octets et les stream d’I/O de caractères. Nous utilisons les deux types de stream d’I/O tout au long de ce chapitre.

Différences entre les stream d’I/O d’Octets et de Caractères

  • Les stream d’I/O d’octets lisent/écrivent des données binaires (0 et 1) et ont des noms de classe qui se terminent par InputStream ou OutputStream.
  • Les stream d’I/O de caractères lisent/écrivent des données textuelles et ont des noms de classe qui se terminent par Reader ou Writer.

L’API inclut fréquemment des classes similaires pour les stream d’I/O d’octets et de caractères, comme FileInputStream et FileReader. La différence entre les deux classes est basée sur la façon dont les octets sont lus ou écrits.

Il est important de se rappeler que même si les stream d’I/O de caractères ne contiennent pas le mot Stream dans leur nom de classe, ce sont toujours des stream d’I/O. L’utilisation de Reader/Writer dans le nom est juste pour les distinguer des flux d’octets.

Tout au long du chapitre, nous faisons référence à la fois à InputStream et à Reader comme flux d’entrée, et nous faisons référence à la fois à OutputStream et à Writer comme flux de sortie.

Les stream d’I/O d’octets sont principalement utilisés pour travailler avec des données binaires, comme une image ou un fichier exécutable, tandis que les stream d’I/O de caractères sont utilisés pour travailler avec des fichiers texte. Par exemple, vous pouvez utiliser une classe Writer pour sortir une valeur String vers un fichier sans nécessairement avoir à vous soucier de l’encodage de caractères sous-jacent du fichier.

L’encodage de caractères détermine comment les caractères sont encodés et stockés en octets dans un stream d’I/O puis lus ou décodés comme caractères. Bien que cela puisse sembler simple, Java prend en charge une grande variété d’encodages de caractères, allant de ceux qui utilisent un octet pour les caractères latins, UTF-8 et ASCII par exemple, à ceux qui utilisent deux octets ou plus par caractère, comme UTF-16.

Encodage de Caractères en Java

En Java, l’encodage de caractères peut être spécifié en utilisant la classe Charset en passant une valeur de nom à la méthode statique Charset.forName(), comme dans les exemples suivants :

Charset usAsciiCharset = Charset.forName("US-ASCII");
Charset utf8Charset = Charset.forName("UTF-8");
Charset utf16Charset = Charset.forName("UTF-16");

Java prend en charge de nombreux encodages de caractères, chacun spécifié par une valeur de nom standard différente.

Flux d’Entrée vs. Flux de Sortie

La plupart des classes InputStream ont une classe OutputStream correspondante, et vice versa. Par exemple, la classe FileOutputStream écrit des données qui peuvent être lues par un FileInputStream. Si vous comprenez les caractéristiques d’un flux d’entrée ou de sortie particulier, vous devriez naturellement savoir ce que fait sa classe complémentaire.

Il s’ensuit donc que la plupart des classes Reader ont une classe Writer correspondante. Par exemple, la classe FileWriter écrit des données qui peuvent être lues par un FileReader.

Il existe des exceptions à cette règle. Vous devriez savoir que PrintWriter n’a pas de classe PrintReader correspondante. De même, PrintStream est un OutputStream qui n’a pas de classe InputStream correspondante. Il n’a pas non plus Output dans son nom. Nous discuterons de ces classes plus tard dans ce chapitre.

Flux de Bas Niveau vs. Flux de Haut Niveau

Une autre façon de vous familiariser avec l’API java.io est de segmenter les flux en flux de bas niveau et de haut niveau.

Un flux de bas niveau se connecte directement avec la source des données, comme un fichier, un tableau, ou une chaîne String. Les stream d’I/O de bas niveau traitent les données ou ressources brutes et sont accessibles de manière directe et non filtrée. Par exemple, un FileInputStream est une classe qui lit les données d’un fichier un octet à la fois.

Alternativement, un flux de haut niveau est construit au-dessus d’un autre stream d’I/O en utilisant l’enveloppement. L’enveloppement est le processus par lequel une instance est passée au constructeur d’une autre classe, et les opérations sur l’instance résultante sont filtrées et appliquées à l’instance originale. Par exemple, jetez un coup d’œil aux objets FileReader et BufferedReader dans l’exemple de code suivant :

try (var br = new BufferedReader(new FileReader("zoo-data.txt"))) {
    System.out.println(br.readLine());
}

Dans cet exemple, FileReader est le stream d’I/O de bas niveau, tandis que BufferedReader est le stream d’I/O de haut niveau qui prend un FileReader comme entrée. De nombreuses opérations sur le stream d’I/O de haut niveau passent comme opérations au stream d’I/O de bas niveau sous-jacent, comme read() ou close(). D’autres opérations remplacent ou ajoutent de nouvelles fonctionnalités aux méthodes du stream d’I/O de bas niveau. Le stream d’I/O de haut niveau peut ajouter de nouvelles méthodes, comme readLine(), ainsi que des améliorations de performance pour la lecture et le filtrage des données de bas niveau.

Les stream d’I/O de haut niveau peuvent également prendre d’autres stream d’I/O de haut niveau comme entrée. Par exemple, bien que le code suivant puisse sembler un peu étrange au premier abord, le style d’enveloppement d’un flux est assez courant en pratique :

try (var ois = new ObjectInputStream(
        new BufferedInputStream(
            new FileInputStream("zoo-data.txt")))) {
    System.out.print(ois.readObject());
}

Dans cet exemple, le FileInputStream de bas niveau interagit directement avec le fichier, qui est enveloppé par un BufferedInputStream de haut niveau pour améliorer les performances. Enfin, l’ensemble de l’objet est enveloppé par un autre ObjectInputStream de haut niveau, qui nous permet d’interpréter les données comme un objet Java.

Pour l’étude, les seules classes de flux de bas niveau avec lesquelles vous devez être familier sont celles qui opèrent sur les fichiers. Le reste des classes de flux non abstraites sont toutes des flux de haut niveau.

Classes de Base des Flux

La bibliothèque java.io définit quatre classes abstraites qui sont les parents de toutes les classes de stream d’I/O définies dans l’API : InputStream, OutputStream, Reader, et Writer.

Les constructeurs des stream d’I/O de haut niveau prennent souvent une référence à la classe abstraite. Par exemple, BufferedWriter prend un objet Writer comme entrée, ce qui lui permet de prendre n’importe quelle sous-classe de Writer.

Un domaine commun où se joue souvent des tours est le mélange et l’appariement de classes de stream d’I/O qui ne sont pas compatibles entre elles. Par exemple, regardez chacun des exemples suivants et voyez si vous pouvez déterminer pourquoi ils ne compilent pas :

new BufferedInputStream(new FileReader("z.txt")); // NE COMPILE PAS
new BufferedWriter(new FileOutputStream("z.txt")); // NE COMPILE PAS
new ObjectInputStream(
    new FileOutputStream("z.txt")); // NE COMPILE PAS
new BufferedInputStream(new InputStream()); // NE COMPILE PAS

Les deux premiers exemples ne compilent pas parce qu’ils mélangent des classes Reader/Writer avec des classes InputStream/OutputStream, respectivement. Le troisième exemple ne compile pas parce que nous mélangeons un OutputStream avec un InputStream. Bien qu’il soit possible de lire des données d’un InputStream et de les écrire dans un OutputStream, envelopper le stream d’I/O n’est pas la façon de le faire. Comme vous le verrez plus tard dans ce chapitre, les données doivent être copiées. Enfin, le dernier exemple ne compile pas parce que InputStream est une classe abstraite, et donc vous ne pouvez pas en créer une instance.

Décodage des Noms de Classes d’I/O

Faites attention au nom de la classe d’I/O, car son décodage donne souvent des indices contextuels sur ce que fait la classe. Par exemple, sans avoir besoin de le chercher, il devrait être clair que FileReader est une classe qui lit des données d’un fichier sous forme de caractères ou de chaînes. De plus, ObjectOutputStream ressemble à une classe qui écrit des données d’objets dans un flux d’octets.

Nom de classeDescription
InputStreamClasse abstraite pour tous les flux d’entrée d’octets
OutputStreamClasse abstraite pour tous les flux de sortie d’octets
ReaderClasse abstraite pour tous les flux d’entrée de caractères
WriterClasse abstraite pour tous les flux de sortie de caractères
Nom de classeNiveau bas/hautDescription
FileInputStreamBasLit les données de fichier sous forme d’octets
FileOutputStreamBasÉcrit les données de fichier sous forme d’octets
FileReaderBasLit les données de fichier sous forme de caractères
FileWriterBasÉcrit les données de fichier sous forme de caractères
BufferedInputStreamHautLit les données d’octets à partir d’un InputStream existant de manière tamponnée, ce qui améliore l’efficacité et les performances
BufferedOutputStreamHautÉcrit les données d’octets vers un OutputStream existant de manière tamponnée, ce qui améliore l’efficacité et les performances
BufferedReaderHautLit les données de caractères à partir d’un Reader existant de manière tamponnée, ce qui améliore l’efficacité et les performances
BufferedWriterHautÉcrit les données de caractères vers un Writer existant de manière tamponnée, ce qui améliore l’efficacité et les performances
ObjectInputStreamHautDésérialise les types de données primitives Java et les graphes d’objets Java à partir d’un InputStream existant
ObjectOutputStreamHautSérialise les types de données primitives Java et les graphes d’objets Java vers un OutputStream existant
PrintStreamHautÉcrit des représentations formatées d’objets Java vers un flux binaire
PrintWriterHautÉcrit des représentations formatées d’objets Java vers un flux de caractères

Gardez les Tableaux 14.7 et 14.8 à portée de main lorsque vous en apprendrez davantage sur les stream d’I/O dans ce chapitre. Nous les discuterons plus en détail, avec des exemples de chacun.