Tutoriel - Au cœur du JDK : java.io expliqué simplement

Olivier Croisier

Ce tutoriel vise à expliquer de manière succincte les concepts de java.io. Il est destiné aux personnes débutantes en Java.

Il a été rédigé par Olivier Croisier dont la version originale est disponible sur son blog officiel : Au cœur du JDK : java.io expliqué simplement.

Vous pouvez réagir par rapport au contenu de ce tutoriel sur un forum qui lui est dédié Commentez . ♪

- Titre : Tutoriel - Au cœur du JDK : java.io expliqué simplement

- Auteur : Olivier Croisier

- Parution : 2 avril 2014

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Olivier Croisier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Les quatre quadrants

- II-A. Conversion texte / binaire

- II-B. Petit test

- III. Le pattern Decorator

- III-A. Principe théorique

- III-B. Le Décorateur au quotidien

- III-C. Application au package java.io

- IV. Quelques exemples

- V. Conclusion

- VI. Articles connexes

I - Introduction

Je suis surpris par le nombre de développeurs qui s'avouent intimidés par le package java.io. Il est vrai qu'il contient beaucoup de classes, dont la plupart portent des noms très similaires, mais nous allons voir que son architecture est en réalité très simple.

Il suffit de comprendre deux concepts pour y voir tout de suite plus clair : le schéma en 4 quadrants, et le pattern Decorator. Le premier permet de déterminer à quoi sert une classe ; le second indique son rôle dans la chaîne de lecture ou d'écriture des données.

Suivez le guide !

II - Les quatre quadrants

Le package java.io est composé de nombreuses classes, mais elles peuvent être réparties en quatre catégories, selon qu'elles réalisent des opérations :

- de lecture ou d'écriture ;

- sur des données textuelles ou binaires

On peut ainsi les placer sur un graphe à quatre quadrants, chacun étant gouverné par une classe abstraite :

Ces classes abstraites sont ensuite implémentées par différentes classes concrètes spécialisées. Leur quadrant d'appartenance se déduit de leur suffixe (-Reader, -Writer, -InputStream ou -OutputStream) :

Par exemple, pour écrire un flux binaire, on utilisera des classes appartenant au quadrant bas droit : FileOutputStream, ObjectOutputStream, etc.

II-A - Conversion texte / binaire

Il existe également deux classes qui permettent de faire le pont entre l'univers des données binaires et celui des données textuelles : InputStreamReader et OutputStreamWriter.

- La classe InputStreamReader propose, comme son nom l'indique, une interface de type Reader (texte) sur des données provenant d'un InputStream (binaire).
Elle est particulièrement pratique lorsqu'une classe (ex. : Socket) vous fournit un InputStream alors que vous savez que les données transmises seront de type texte.

- Inversement, un OutputStreamWriter permettra d'utiliser les API de type Writer pour produire des données binaires propres à transiter par un OutputStream.

Mais la conversion texte / binaire pose toujours quelques problèmes.
Prenez la représentation binaire ci-dessous. Combien de caractères comporte-t-elle ? Où commence un caractère, où finit-il ?(1)

0100100001100101011011000110110001101111001000000101011101101111011100100110110001100100

Il est généralement impossible de le déterminer sans connaître le type d'encodage utilisé : UTF-8, UTF-16, ISO-8859-1, voire même -gasp!- Win-Cp1252… Ces algorithmes d'encodage sont encapsulés par des implémentations de la classe java.nio.Charset.

Pour que nos classes InputStreamReader et OutputStreamWriter puissent convertir efficacement des données textuelles en binaire (et réciproquement), il est donc nécessaire de leur fournir un Charset en paramètre de constructeur :

InputStream in = System.in;
Charset charset = Charset.forName("UTF-8");
InputStreamReader reader = new InputStreamReader(in, charset);

II-B - Petit test

Vous voyez qu'il est facile, une fois ce schéma mémorisé, de déterminer à quoi sert une classe du package java.io. Inversement, cela permet de trouver rapidement une classe réalisant l'opération souhaitée, sans apprendre l'API par cœur (ouf !).

Allez, un petit quiz rapide pour voir si vous avez compris. Je relève les copies dans 5 minutes(2).
Quelle classe utiliser pour :

- lire du texte depuis une String ?

- lire un flux binaire depuis un fichier ?

- écrire un objet dans un flux binaire (c'est-à-dire le sérialiser) ?

- lire du texte de façon optimisée (en utilisant un buffer) ?

Vous voyez, ce n'est pas difficile !
Voyons maintenant le second principe fondateur du package java.io : le pattern Decorator, qui explique la façon dont toutes ces classes peuvent être assemblées.

III - Le pattern Decorator

III-A - Principe théorique

Le design pattern Decorator (« Décorateur » en français) fait partie des patterns structurels.
Il permet d'ajouter des comportements (méthodes) à un objet de base, par composition plutôt que par héritage, favorisant ainsi la cohésion et la réutilisabilité. Et comme nous sommes sur un blog sérieux, hop hop, vite un diagramme UML tiré de Wikipédia :

Le principe est simple : sur une brique de base exposant une certaine interface, on vient brancher des briques additionnelles proposant la même interface, mais fournissant des services supplémentaires.

Vous serez sans doute surpris d'apprendre que vous utilisez le pattern Decorator tous les jours : lorsque vous branchez un casque à votre lecteur MP3 ; lorsque vous jouez aux Lego ; lorsque vous utilisez un hub USB ou un switch réseau.

III-B - Le Décorateur au quotidien

Prenons l'exemple du lecteur MP3. Cet appareil produit de la musique, et l'expose suivant une interface (physique en l'occurrence) prédéfinie : la forme de la prise jack. Tout appareil (casque, enceintes…) se conformant à cette interface peut alors consommer ladite musique.

Mais il est également possible de brancher, entre le lecteur et le casque, différents accessoires comme une rallonge ou un contrôleur de volume. Chacun fournit une fonctionnalité différente, mais tous peuvent être combinés pour former une chaîne arbitraire s'intercalant entre le producteur de données et le consommateur. Cette « composabilité », très pratique, n'est possible que parce que tous les accessoires exposent la même interface que la brique initiale qu'ils « décorent ».

III-C - Application au package java.io

Le package java.io est entièrement construit sur ce principe de composition, permis par le pattern Décorateur.

Certaines classes lisent/écrivent réellement les données sur/depuis un certain médium (fichier, réseau, buffer mémoire…) : elles sont donc toujours en bout de chaîne. D'autres en revanche ne font que manipuler ou observer les données qui transitent sur la chaîne de lecture/écriture : ce sont les décorateurs.

Par exemple, FileWriter, ByteArrayInputStream, ou StringReader sont des classes de bout de chaîne ; en revanche, BufferedReader, LineNumberReader ou ObjectOutputStream sont des décorateurs.

Pour composer une chaîne de lecture ou d'écriture, il vous suffit de sélectionner une classe de bout de chaîne permettant de lire ou d'écrire sur le médium cible, puis de brancher dessus autant de décorateurs que nécessaire pour obtenir les fonctionnalités souhaitées. Le branchement d'un élément sur le suivant s'effectue habituellement en le passant le second comme paramètre du constructeur du premier.

Exemple :

FileReader fr = new FileReader("/path/to/file"); // classe terminale, pour lire un fichier texte
BufferedReader reader = new BufferedReader(fr); // Décorateur, pour utiliser un buffer de lecture

IV - Quelques exemples

Voyons quelques exemples classiques de chaînes (la gestion des exceptions est omise ici).

- Lire les lignes d'un fichier texte, en comptant les lignes (LineNumberReader → BufferedReader → FileReader) :

FileReader fr = new FileReader("/path/to/file");
BufferedReader reader = new BufferedReader(fr);
LineNumberReader counter = new LineNumberReader(reader);
String line = null;
while ((line = counter.readLine()) != null) {
int lineNum = counter.getLineNumber();
System.out.println(lineNum + " : " + line);
}
counter.close();

- Sérialiser un objet vers un tableau de bytes (ObjectOutputStream → ByteArrayOutputStream) :

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject("Hello World");
oos.close();

- Lire une ligne de texte saisie dans la console ; attention, System.in est de type InputStream, il faut donc le convertir (BufferedReader → InputStreamReader → InputStream fourni par System.in) :

InputStream in = System.in;
InputStreamReader isr = new InputStreamReader(in, Charset.forName("UTF-8"));
BufferedReader reader = new BufferedReader(isr);
String line = reader.readLine();
System.out.println(line);
reader.close();

Nous nous arrêterons là, mais vous voyez que les combinaisons sont infinies.

V - Conclusion

Le package java.io est conçu selon deux concepts très simples : un double découpage lecture/écriture et texte/binaire d'une part, et le design pattern Decorator d'autre part. Le premier indique la fonction des classes, et le second la façon dont elles peuvent être assemblées en une chaîne de lecture ou d'écriture.

La prochaine fois que vous parcourrez la javadoc à la recherche d'une classe correspondant à vos besoins, rappelez-vous le schéma en quatre quadrants !

Nos remerciements à l'endroit de Olivier Croisier pour avoir donné son accord pour la publication de cet article. L'article original, Au cœur du JDK : java.io expliqué simplement, est disponible sur son blog officiel The Codest Breakfast.

VI - Articles connexes

Dans la série « Au cœur du JDK », vous serez sans doute également intéressés par :

- Au cœur du JDK : performance des conversions

- Au cœur du JDK : conversion des nombres

- Au cœur du JDK : l'enum TimeUnit

- Au cœur du JDK : l'interface Iterable

- Au cœur du JDK : la classe Scanner

- Au cœur du JDK : présentation du Service Provider API

(1)C'est « Hello World » en binaire (8bits par caractère) !

(2)1-StringReader, 2-FileInputStream, 3-ObjectOutputStream, 4-BufferedReader