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 :