Java File I/O
Introduction
Travailler avec des fichiers est une tâche essentielle. Les fichiers offrent aux applications un moyen de stocker et de récupérer des données de façon permanente sur un disque. En tant que développeur, tu devras tôt ou tard lire ou écrire des données depuis ou vers un fichier.

Java propose une approche basée sur les streams (flux : une suite continue d'octets qui circule d'un point à un autre) pour accéder aux fichiers. Un stream est essentiellement un flux d'octets du disque vers la mémoire ou l'inverse.
🤓 À la fin de cette quête, tu sauras :
✅ Connaître les classes Java pour l'entrée et la sortie de fichiers ✅ Comprendre le concept de stream et de wrappers (emballeurs : des classes qui en enveloppent une autre pour lui ajouter des fonctionnalités)
Une façon de voir un fichier, c'est comme une suite consécutive d'octets ou, dans le cas des fichiers texte, de caractères. En Java (comme dans tout langage de programmation raisonnable) les octets sont des octets, et les caractères sont généralement des suites d'octets qui suivent le standard d'encodage UTF-8.
File
La première classe que tu dois connaître quand tu travailles avec des fichiers, c'est la classe File. Cette classe représente un fichier de façon abstraite par son nom ou son chemin, et non le contenu du fichier. D'une certaine manière, c'est une référence vers un fichier réel sur le disque.
File file = new File("test.txt");
Cette instance de la classe File fait référence à un fichier portant le nom test.txt. Ce pourrait être un fichier qui existe ou que tu veux créer. La classe File fournit des méthodes que tu peux utiliser pour obtenir des informations plus détaillées sur le fichier réel.
Pour vérifier si le fichier existe sur le disque, utilise
if(file.exists()) {
System.out.println("File exists!");
} else {
System.out.println("There is no such file");
}
Pour connaître la taille du fichier en octets pour un fichier réel, utilise
int size = file.length();
Même si la classe File offre un moyen de référencer les fichiers sur le disque, la lecture et l'écriture de données depuis le fichier ne sont pas assurées par la classe File.
FileInputStream and FileOutputStream
Pour lire les octets d'un fichier, on utilise la classe FileInputStream, tandis qu'on utilise la classe FileOutputStream pour écrire des octets dans un fichier. Le préfixe File devant InputStream et OutputStream nous donne un indice clair sur la source et la destination de la suite d'octets.
Java propose d'autres implémentations des classes InputStream et OutputStream qui ne sont pas liées aux fichiers. C'est juste qu'on examine les streams de façon pragmatique en prenant les fichiers comme exemple. InputStream et OutputStream sont des sources ou des puits abstraits d'octets qui généralisent le concept d'entrée et de sortie depuis et vers n'importe où.
Tu peux utiliser les autres classes pour lire et écrire des streams d'octets depuis d'autres sources et vers d'autres destinations, ou tu peux même écrire tes propres classes et fournir une implémentation alternative. Une fois la source et la destination définies, peu importe d'où viennent les octets ou où ils sont écrits.
Pour lire tous les octets d'un fichier, on appelle la méthode read() pour chaque octet du fichier dans une boucle. La méthode read renverra chaque octet, ou la valeur spéciale -1 quand il n'y a plus d'octets à lire dans le fichier.
FileInputStream fileInputStream = new FileInputStream(new File("test.txt"));
int content;
while((content = fileInputStream.read()) != -1) {
// ...
}
fileInputStream.close();
L'utilisation du type de données int pour la variable qui reçoit l'octet est obligatoire, parce que la méthode read() fournit une valeur de retour mixte. Ce n'est pas simplement l'octet lu dans le fichier. Quand la valeur de retour est 0 ou plus grande, elle représente l'octet lu dans le fichier. La valeur du int ne dépassera jamais 255 car c'est la valeur maximale d'un octet. Quand la méthode read() renvoie -1, il n'y a plus d'octets à lire dans le fichier et tu as atteint la fin du fichier.
Utiliser la valeur de retour pour renvoyer à la fois les octets valides qui ont été lus et une valeur d'erreur spéciale quand il n'y a plus de données à lire, c'est source de confusion. Il est juste trop tard maintenant pour changer et améliorer ça, car ces méthodes sont utilisées depuis des années.
Quand tu as fini de lire depuis le FileInputStream, tu dois fermer le FileInputStream avec la méthode close().
Quand tu oublies de fermer un fichier avec close(), ça peut bloquer d'autres programmes qui veulent lire depuis ce fichier. Les choses empirent quand tu oublies de fermer un FileOutputStream. Sans l'appel à la méthode close(), le fichier peut être corrompu et des données peuvent être perdues.
Pour écrire des octets dans le fichier, on appelle la méthode write dans une boucle, en écrivant un octet après l'autre.
byte[] data = ... //
FileOutputStream fileOutputStream = new FileOutputStream(new File("test.txt"));
for(byte b : data) {
fileOutputStream.write(b));
}
fileOutputStream.close();
Lire ou écrire des octets un par un depuis ou vers un stream demande beaucoup d'appels de méthodes, ce qui n'est pas efficace et va ralentir ton application. Les deux classes, FileInputStream et FileOutputStream, fournissent des méthodes pour travailler avec plus d'un octet à la fois. Tu es libre de choisir le nombre d'octets à traiter à chaque appel de méthode. Note que, à moins d'avoir un nombre d'octets exactement multiple dans le fichier, le dernier appel de la méthode read fournira un nombre d'octets plus petit que ce que le buffer (tampon : une zone mémoire qui stocke temporairement les données) pourrait contenir.
Reader and Writer
Tu peux convertir une String en byte[] et un byte[] en String, mais comme les fichiers texte sont très utilisés, Java fournit des classes qui incluent déjà ça. Rappelle-toi que les String en Java sont encodées sur 16 bits, soit des valeurs de 2 octets avec l'encodage UTF-16. Quand on écrit des String dans un fichier, il est courant d'utiliser l'encodage UTF-8. Les String doivent être converties d'un encodage à l'autre.
Reader est utilisé pour lire depuis des fichiers texte, tandis que Writer est utilisé pour écrire dans des fichiers texte. Si la classe Reader peut lire des caractères depuis un fichier, elle ne peut pas lire une ligne complète depuis un fichier. Garde en tête que les String et les lignes ne sont pas la même chose, et qu'il est seulement courant de traiter les lignes d'un fichier une par une. Pour lire une ligne complète, on doit envelopper (wrap) le FileReader avec un BufferedReader.
File f = new File("test.txt");
String s = "Hello world";
FileReader fr = new FileReader(f);
BufferedReader br = new BufferedReader(fr);
while((line = br.readLine())!=null) {
System.out.println(line);
}
br.close();
Il n'est pas nécessaire d'ajouter de la mise en buffer pour écrire des String dans un fichier.
File f = new File("test.txt");
String s = "Hello world";
FileWriter fw = new FileWriter(f);
fw.write(s);
fw.close();
Avec la lecture de String depuis un fichier, on a introduit la nouvelle classe BufferedReader. L'instance de BufferedReader qu'on a créée a enveloppé le FileReader pour regrouper des caractères en String.
Have some fun with a BlackHoleOutputStream
Au lieu d'écrire des octets dans un fichier, on pourrait aussi les jeter avec un BlackHoleOutputStream. Réfléchis un instant à l'implémentation de cet OutputStream. Que dois-tu faire dans la méthode write ?
Un OutputStream qui ne persiste pas réellement les données peut sembler une idée stupide au premier abord. Mais beaucoup de systèmes d'exploitation, comme Linux, ont un périphérique spécial /dev/null à cette fin. Tu pourrais écrire tes octets vers ce périphérique pour implémenter un BlackHoleOutputStream. Avant Java 11, tu aurais dû écrire ta propre implémentation, mais avec Java 11 une implémentation est disponible dans Java.
OutputStream blackHoleOutputStream = OutputStream.nullOutputStream();
Roll your own chunk
Comme les objets ont des propriétés, on pourrait traiter chaque propriété comme un élément unique à écrire dans un fichier. Si on garde l'ordre, il sera facile de relire ces propriétés depuis un fichier plus tard.
On peut suivre le concept de wrapping (enveloppage : emballer une classe dans une autre pour lui ajouter un niveau de traitement) pour ajouter des wrappers de plus haut niveau, par exemple une classe qui consomme 3 lignes pour lire le prénom, le nom et l'âge d'une personne depuis trois lignes consécutives du fichier. Nos fichiers devraient toujours avoir un multiple de 3 lignes, 3 lignes formant un enregistrement ou une entité de type personne.
Une fois qu'on a implémenté PersonInputStream et PersonOutputStream, on pourrait obtenir le même résultat avec un groupe de personnes. Appelons ce groupe une équipe (team).
D'une certaine manière, notre classe Team est une composition de propriétés qui décrivent une équipe. D'abord, on a la taille de l'équipe et les membres de l'équipe. Chaque membre de l'équipe est une personne.
On peut continuer à construire des classes de stream qui permettent de lire et d'écrire n'importe quelle structure. Ça permettrait à notre application de sauvegarder et de restaurer des données depuis des fichiers avec de simples méthodes.
More file input and output
Java a ajouté beaucoup de classes pour améliorer l'entrée et la sortie de fichiers, et toutes ces classes sont utilisées pour de bonnes raisons.
Java I/O
Le premier groupe de classes pour l'entrée et la sortie en Java
Java NIO
De nouvelles classes pour améliorer les performances des I/O, en utilisant des channels (canaux : des conduits par lesquels les données transitent)
Java NIO2
NIO2 a introduit les I/O asynchrones.
Avec les I/O asynchrones, un appel à des méthodes de sortie ne bloque pas le programme. Quand tu écris un octet dans un fichier, l'appel de méthode se termine avant que le travail réel soit fait, alors qu'avec les I/O synchrones, l'appel de méthode ne se termine qu'une fois l'octet écrit.
NIO
Avec NIO, lire tous les octets d'un fichier ne nécessite plus de boucle. La classe Files fournit une méthode statique pour lire tous les octets d'un fichier.
File f = new File("test.txt");
byte[] bytes = Files.readAllBytes( f.toPath());
On a appris à lire tous les octets d'un fichier avec les streams d'entrée et de sortie. Comme c'est une tâche courante, beaucoup de programmeurs ont écrit leur propre implémentation de readAllBytes sous des noms différents.
Écrire tous les octets d'un tableau dans un fichier est aussi plus simple avec NIO. Tu n'as plus besoin de boucle pour cette tâche.
File f = new File("test.txt");
byte[] data = // ...;
Files.write(f.toPath(), data);
Dans les exemples pour Java NIO, on utilise la méthode toPath pour convertir des fichiers (File) en chemins (Path). NIO a introduit une nouvelle classe, la classe Path, qui remplit le même rôle que la classe File.
Path résout certains problèmes de la classe File, par exemple avec les liens symboliques. Ne t'inquiète pas, utiliser la classe File ne posera problème que dans des cas particuliers : le code n'est pas cassé.
Reference
L'ensemble complet des classes Java IO peut être intimidant. On a présenté les classes de base dans cette quête, ce qui te donnera assez pour comprendre le concept et explorer ce que Java a à offrir.
☝️ Summary
Les I/O de fichiers en Java, ou les I/O en Java de façon générale, sont basées sur les streams. Les streams sont des suites d'octets qui sont lues depuis une source ou écrites vers une destination. La source et la destination n'ont pas vraiment d'importance après l'initialisation. La lecture et l'écriture sont des tâches séparées et sont généralement implémentées dans des classes différentes. L'enveloppage (wrapping) des classes de stream est courant en Java, et quand tu auras fini cette quête, tu auras non seulement utilisé une des classes de stream existantes, mais tu auras aussi écrit ta propre classe de stream.
💪Challenge
Crée les classes PersonInputStream et PersonOutputStream pour lire et écrire des instances de la classe Person. Fournis un exemple d'application qui utilise ces classes pour lire et écrire une personne dans un fichier.
Une fois PersonInputStream et PersonOutputStream prêts, crée les classes TeamInputStream et TeamOutputStream pour lire et écrire des instances d'une List de Person. Fournis un exemple d'application qui utilise ces classes pour lire et écrire une List de personnes.
🧐 Acceptance criteria
- ✔ Tu as écrit les classes PersonInputStream et PersonOutputStream
- ✔ Tu as écrit un exemple qui utilise les deux classes pour lire et écrire des personnes dans un fichier.
- ✔ Tu as écrit les classes TeamInputStream et TeamOutputStream
- ✔ Tu as écrit un exemple qui utilise les deux classes pour lire et écrire une liste de personnes dans un fichier
- ✔️Ton code est poussé dans le dépôt git
Introduction
Working with files is an essential task. Files provide a way for applications to permanently store and retrieve data on a disk. As a developer you will, sooner or later, have to read or write data from or to some file.

Java provides a stream based approach for files access. A stream is essentially a flow of bytes to or from disk to memory.
🤓 By the end of this quest, you will:
✅ Know about Java classes for file input and output ✅ Understand the concept of stream and wrappers
One way to look at a file is as a consecutive sequence of bytes or, in case of text files, characters. Bytes are bytes in Java (as in any reasonable programming language) and characters are usually byte sequences that follow the UTF-8 encoding standard.
File
The first class you should know about when working with files is the File class. This class represents an abstract file by name or path and not the file content. In a way, it is a reference to an actual file on the disk.
File file = new File("test.txt");
This instance of the file class references a file with the name test.txt. This could be a file that exists or that you want create. The file class provides methods that you can use to gather more detailed information about the actual file.
To check if the file exists on the disk, use
if(file.exists()) {
System.out.println("File exists!");
} else {
System.out.println("There is no such file");
}
To learn about the file size in bytes for an actual file, use
int size = file.length();
While the File class provides a way to reference files on the disk, reading and writing data from the file is not provided by the File class.
FileInputStream and FileOutputStream
To read the bytes from a file we use FileInputStream class while we use the FileOutputStream class to write bytes to a file. The prefix File for the InputStream and OutputStream gives us a clear hint about the source and destination of the byte sequence.
Java provides other implementations of the InputStream and OutputStream class that are not related to files. It's just that we are looking at streams in a pragmatic way using files as an example. InputStream and OutputStream are abstract sources or sinks for bytes that generalize the concept of input and output from and to anywhere.
You can use the other classes to read and write streams of bytes from other sources and to other destinations or you could even write your own classes and provide an alternative implementation. Once the source and destination are set, it does not matter where the byte come from or where they are written to.
Too read all bytes from a file, we call the read() method for each byte of the file in a loop. The read method will deliver each byte or the special value -1 when there are no more bytes left to read from the file.
FileInputStream fileInputStream = new FileInputStream(new File("test.txt"));
int content;
while((content = fileInputStream.read()) != -1) {
// ...
}
fileInputStream.close();
Use of the data type int for the variable that receives the byte is required, because the read() method provides a mixed return value. It's not just the byte read from the file. When the return value is 0 or larger, it represents the byte read from the file. The value of the int will never exceed the value 255 because that is the maximum value for a byte. When the read() method returns -1, there are no more bytes to read from the file and you reached the end of the file.
Using the return value to return both, valid bytes that have been read and a special error value when there is no more data to read is confusing. It's just to late now to change and improve this since the methods are in use for years.
When you are finished reading from the FileInputStream, you have to close the FileInputStream with the close() method.
When you forget to close() a file, this can block other programs that want to read from the file. Things get worse when you forget to close a FileOutputStream. Without the call to the close() method, the file can be corrupted and data may get lost.
To write bytes to the file, we call the write method in a loop, writing one byte after the other.
byte[] data = ... //
FileOutputStream fileOutputStream = new FileOutputStream(new File("test.txt"));
for(byte b : data) {
fileOutputStream.write(b));
}
fileOutputStream.close();
Reading or writing single bytes from or to a stream requires a lot of method calls which is not efficient and will make your application slow. Both classes, FileInputStream and FileOutputStream provide methods to work with more than one byte at a time. You are free to choose the number of byte to handle with each method call. Note that, unless you have an exact multiple number of bytes in the file, the last call of the read method will provide a smaller number of bytes than what the buffer could hold.
Reader and Writer
You can convert a string to a byte[] and a byte[] to a string, but because text files are used a lot, Java provides classes that already include this. Remember that strings in Java are encoded as 16 bit or 2 byte values with the UTF-16 encoding. When writing strings to a file, it is common to use the UTF-8 encoding. Strings have to be converted from one encoding to the other.
Reader is used to read from text files while Writer is used to write to text files. While the Reader class can read characters from a file, it can not read a complete line from a file. Keep in mind that strings and lines are not the same thing and it is only common to process the lines in a file one by one. To read a complete line, we have to wrap the FileReader with a BufferedReader.
File f = new File("test.txt");
String s = "Hello world";
FileReader fr = new FileReader(f);
BufferedReader br = new BufferedReader(fr);
while((line = br.readLine())!=null) {
System.out.println(line);
}
br.close();
It is not necessary to add buffering to write strings to a file.
File f = new File("test.txt");
String s = "Hello world";
FileWriter fw = new FileWriter(f);
fw.write(s);
fw.close();
With reading strings from a file, we introduced the new class BufferedReader. The instance of BufferedReader we created wrapped the FileReader to build groups of characters into strings.
Have some fun with a BlackHoleOutputStream
Instead of writing bytes to a file, we could also throw them away with a BlackHoleOutputStream. Think about the implementation of this OutputStream for a moment. What do you have to do in the write method?
A OutputStream that does not actually persist data may look like a stupid idea at first sight. But many operating systems, like Linux, do have a special device /dev/null for this purpose. You could write your bytes to this device to implement a BlackHoleOutputStream. Before Java 11 you would have to write your own implementation but with Java 11, there is an implementation available in Java.
OutputStream blackHoleOutputStream = OutputStream.nullOutputStream();
Roll your own chunk
As objects have properties, we could treat each property as a single item to write to a file. If we keep the order, it will be easy to read these properties from a file later.
We can follow the wrapping concept to add higher level wrappers, for example a class that consumes 3 lines to read the firstname, lastname and age of a person from three consecutive lines in the file. Our files should always have a multiple of 3 lines with 3 lines building a record or an entity of a person.
Once we implemented PersonInputStream and PersonOutputStream, we could achieve the same result with a group of people. Let's call this group a team.
In a way, our Team class is a composition of properties that describe a team. First of all, we have the team size and the team members. Each Team member is a person.
We can continue to build stream classes that enable reading and writing any structure. This would enable our application to save and restore data from files with simple methods.
More file input and output
Java added many classes to improve file input and output and all these classes are in use for good reason.
Java I/O
The first group of classes for input and output in Java
Java NIO
New classes to improve performance on I/O, using channels
Java NIO2
NIO2 introduced asynchronous I/O.
With asynchronous I/O, a call to output methods is not blocking the program. When you write a byte to a file, the method call will return before the actual work is done while with synchronous I/O, the method call will only return after the byte is written.
NIO
With NIO, reading all bytes from a file does not require a loop any longer. The Files class provides a static method to read all bytes from a file.
File f = new File("test.txt");
byte[] bytes = Files.readAllBytes( f.toPath());
We learned how to read all bytes from a file with input and output streams. Since this is a common task, many programmers wrote their own implementation of readAllBytes with different names.
Writing all bytes of an array to a file is simpler with NIO too. You do not need a loop for this task anymore.
File f = new File("test.txt");
byte[] data = // ...;
Files.write(f.toPath(), data);
In the examples for Java NIO, we use the toPath method to convert from files (File) to pathes (Path). NIO introduced a new class, the Path class that provides the same purpose as the File class.
Path resolves some issues of the File class, for example with symbolic links. Don't worry, using the File class will only be a problem for special cases - the code is not broken.
Reference
The complete set of Java IO classes can be overwhelming. We outlined the basic classes in this quest which will give you enough to understand the concept and explore what Java has to offer.
☝️ Summary
The Java File I/O, or Java I/O in general is stream based. Streams are sequences of bytes that are read from a source or written to a destination. Source and destination do not really matter after initialization. Reading and writing are separate tasks and are usually implemented in different classes. Wrapping of stream class is common in Java and when you finished this quest, you have not only used one of the existing stream classes, but you have written your own stream class.
💪Challenge
Create the PersonInputStream and PersonOutputStream classes to read and write instances of the class Person. Provide an example application that uses these classes to read and write a person to a file.
Once the PersonInputStream and PersonOutputStream are ready, create the TeamInputStream and TeamOutputStream to read and write instances of a List of Persons. Provide an example applicaiton that uses these classes to read and write a List of persons.
🧐 Acceptance criteria
- ✔ You wrote the PersonInputStream and PersonOutputStream classes
- ✔ You wrote an example that uses both classes to read and write persons to a file.
- ✔ You wrote the TeamInputStream and TeamOutputStream classes
- ✔ You wrote an example that uses both classes to read and write a list of persons to a file
- ✔️Your code is checked in to the git repository