📚 Cours magistral  •  ☕ Java SE 8+

Les Entrées / Sorties en Java

✍️ François Bonneville 📦 Package java.io & java.nio.file

1Introduction aux Entrées / Sorties

Dans la plupart des langages de programmation, les notions d'entrées/sorties sont considérées comme une technique de base, car les manipulations de fichiers, notamment, sont très fréquentes.

En Java, et pour des raisons de sécurité, on distingue deux cas :

ℹ️
Deux packages principaux Le package historique java.io fournit les classes de flux et la classe File. Depuis Java 7, java.nio.file constitue l'API moderne, plus robuste et plus expressive. Les deux coexistent et se complètent.

2La gestion des fichiers — classe File

La gestion de fichiers proprement dite se fait par l'intermédiaire de la classe File. Cette classe possède des méthodes qui permettent d'interroger ou d'agir sur le système de gestion de fichiers du système d'exploitation.

Un objet de la classe File peut représenter un fichier ou un répertoire.

Principaux constructeurs et méthodes

Méthode / ConstructeurDescription
File(String name)Crée un objet File à partir d'un chemin
File(String path, String name)Crée un objet File à partir d'un répertoire et d'un nom
File(File dir, String name)Crée un objet File à partir d'un objet répertoire
boolean isFile()Indique si l'objet représente un fichier
boolean isDirectory()Indique si l'objet représente un répertoire
boolean mkdir()Crée le répertoire désigné
boolean exists()Teste l'existence du fichier ou répertoire
boolean delete()Supprime le fichier ou répertoire
boolean canWrite() / canRead()Teste les droits d'accès
File getParentFile()Retourne le répertoire parent
long lastModified()Date de dernière modification

Exemple : lister un répertoire

import java.io.*;

public class Listeur {
    public static void main(String[] args) {
        litrep(new File("."));  // répertoire courant
    }

    public static void litrep(File rep) {
        if (rep.isDirectory()) {
            String[] t = rep.list();
            for (String nom : t)
                System.out.println(nom);
        }
    }
}

Exemple : parcours récursif d'arborescence

public static void litrep(File rep) {
    if (rep.isDirectory()) {
        String[] t = rep.list();
        for (String nom : t) {
            File r2 = new File(rep.getAbsolutePath() + "\\" + nom);
            if (r2.isDirectory())
                litrep(r2);          // appel récursif
            else
                System.out.println(r2.getAbsolutePath());
        }
    }
}

3Notion de flux (stream)

Les E/S sont gérées de façon portable (selon les OS) grâce à la notion de flux (stream en anglais). Un flux est en quelque sorte un canal dans lequel l'information transite. L'ordre dans lequel l'information y est transmise est respecté.

Un flux peut être :

Certains flux de données peuvent être associés à des ressources : les fichiers, les tableaux de données en mémoire, les lignes de communication (connexion réseau), etc.

Avantage clé de la notion de flux Elle permet une gestion homogène, quelle que soit la ressource associée et quel que soit le flux (entrée ou sortie). Certains flux peuvent en outre être associés à des filtres qui, combinés à des flux d'entrée ou de sortie, permettent de traduire ou transformer les données.

Deux grandes familles de flux

InputStream (abstrait)
  
OutputStream (abstrait)

Flux d'octets
Reader (abstrait)
  
Writer (abstrait)

Flux de caractères (Unicode)

Tous les flux sont regroupés dans le paquetage java.io. Il existe de nombreuses classes représentant les flux — il n'est pas toujours aisé de s'y repérer. L'approche consiste à combiner différents types de flux pour réaliser la gestion souhaitée.

4Flux d'octets : InputStream & OutputStream

Hiérarchie InputStream

InputStream (abstraite)
├── ByteArrayInputStream
├── FileInputStream — lecture depuis un fichier
├── PipedInputStream
├── StringBufferInputStream
├── SequenceInputStream
└── FilterInputStream
     ├── BufferedInputStream — lecture bufferisée
     ├── DataInputStream — types Java de base
     ├── LineNumberInputStream
     └── InflaterInputStream
         ├── GZIPInputStream
         └── ZipInputStream
             └── JarInputStream

Méthodes principales de InputStream

MéthodeDescription
abstract int read()Retourne l'octet lu ou -1 en fin de flux. Méthode de base à définir dans les sous-classes.
int read(byte[] b)Remplit un tableau d'octets et retourne le nombre d'octets lus.
int read(byte[] b, int off, int len)Remplit une portion de tableau à partir d'un offset sur une longueur donnée.
int available()Retourne le nombre d'octets prêts à être lus sans blocage.
long skip(long n)Ignore n octets du flux ; retourne le nombre effectivement ignorés.
void close()Ferme le flux et libère les ressources système associées.
⚠️
Fermer les flux ! Un flux ouvert consomme des ressources du système d'exploitation qui sont en nombre limité. Il faut impérativement fermer les flux dès qu'on a fini de les utiliser. Utilisez de préférence un bloc try-with-resources (Java 7+) pour garantir la fermeture automatique.

Hiérarchie OutputStream

OutputStream (abstraite)
├── ByteArrayOutputStream
├── FileOutputStream — écriture dans un fichier
├── PipedOutputStream
├── ObjectOutputStream — sérialisation
└── FilterOutputStream
     ├── BufferedOutputStream
     ├── DataOutputStream
     ├── PrintStream — System.out, System.err
     └── DeflaterOutputStream
         ├── GZIPOutputStream
         └── ZipOutputStream

Méthodes principales de OutputStream

MéthodeDescription
abstract void write(int b)Écrit l'octet passé en paramètre.
void write(byte[] b)Écrit tous les octets du tableau.
void write(byte[] b, int off, int len)Écrit une portion du tableau à partir d'un offset.
void flush()Purge le tampon en cas d'écritures bufferisées.
void close()Ferme le flux après avoir vidé le tampon.

Flux manipulant des types de base

5Empilement de flux filtrés

En Java, chaque type de flux est destiné à réaliser une tâche précise. Lorsqu'on souhaite un flux au comportement plus complexe, on « empile », à la façon des poupées russes, plusieurs flux ayant des comportements plus élémentaires. On parle de flux filtrés.

Concrètement, il s'agit de passer, dans le constructeur d'un flux, un autre flux déjà existant pour combiner leurs caractéristiques.

Exemple 1 — Lecture de types Java depuis un fichier

// FileInputStream sait lire depuis un fichier mais uniquement octet par octet.
// DataInputStream sait lire des types Java de haut niveau mais pas depuis un fichier.
// On combine les deux :
FileInputStream  fic = new FileInputStream("fichier.bin");
DataInputStream  din = new DataInputStream(fic);
double d = din.readDouble();

Exemple 2 — Lecture bufferisée de nombres depuis un fichier

DataInputStream din = new DataInputStream(
    new BufferedInputStream(
        new FileInputStream("monfichier")
    )
);

Exemple 3 — Lecture depuis un fichier ZIP

ZipInputStream  zin = new ZipInputStream(
    new FileInputStream("monfichier.zip")
);
DataInputStream din = new DataInputStream(zin);
Performances : l'importance du buffering L'utilisation de flux bufferisés (BufferedInputStream, BufferedOutputStream) peut améliorer considérablement les performances en réduisant le nombre d'appels système. Sans buffer, chaque appel à read() effectue un accès disque ; avec buffer, les données sont chargées par blocs.

6Fichiers à accès direct — RandomAccessFile

La classe RandomAccessFile permet de lire ou d'écrire dans un fichier à n'importe quel emplacement (par opposition aux fichiers à accès séquentiel). Elle implémente les interfaces DataInput et DataOutput, permettant de lire ou d'écrire tous les types Java de base, les lignes, les chaînes ASCII ou Unicode, etc.

Modes d'ouverture

ModeDescription
"r"Lecture seule
"rw"Lecture et écriture

Ces fichiers possèdent un pointeur de fichier qui indique constamment la donnée suivante à lire ou écrire :

RandomAccessFile raf = new RandomAccessFile("data.bin", "rw");
raf.seek(100);                  // positionner à l'octet 100
double val = raf.readDouble(); // lire un double à cette position
raf.close();

7Flux de caractères — Reader & Writer

Les flux de caractères sont des sous-classes de Reader et Writer. Ils utilisent le codage de caractères Unicode et sont préférables dès qu'on manipule du texte, afin d'éviter les problèmes d'encodage.

Hiérarchie des flux de caractères

Reader (abstrait)
├── BufferedReader — readLine()
     └── LineNumberReader
├── CharArrayReader
├── FilterReader
     └── PushBackReader
├── InputStreamReader — conversion octets → chars
     └── FileReader — lecture d'un fichier texte
├── PipedReader
└── StringReader
Writer (abstrait)
├── BufferedWriter
├── CharArrayWriter
├── OutputStreamWriter
     └── FileWriter — écriture dans un fichier texte
├── PrintWriter — print(), println()
├── PipedWriter
└── StringWriter

Lecture d'un fichier texte ligne par ligne

import java.io.*;

try (BufferedReader br = new BufferedReader(
                              new FileReader("monfichier.txt"))) {
    String ligne;
    while ((ligne = br.readLine()) != null) {
        System.out.println(ligne);
    }
} catch (IOException e) {
    System.err.println("Erreur : " + e.getMessage());
}

Écriture dans un fichier texte

try (BufferedWriter bw = new BufferedWriter(
                              new FileWriter("sortie.txt"))) {
    bw.write("Ceci est mon fichier");
    bw.newLine();
    bw.write("Il est à moi...");
    // Le bloc try-with-resources ferme automatiquement le flux
}

Lecture/écriture avec encodage explicite

// Lire un fichier encodé en ISO-2022-CN (chinois)
InputStreamReader in = new InputStreamReader(
    new FileInputStream("chinois.txt"), "ISO2022CN"
);

Écriture avec PrintWriter

Pour écrire des chaînes et des nombres sous forme de texte, on utilise la classe PrintWriter qui possède les méthodes print(...) et println(...). Pour lire des nombres sous forme de texte, il n'existe pas de solution toute faite : il faut passer par des chaînes de caractères et les convertir ensuite.

⚠️
Ne pas oublier de fermer ! Lorsqu'on écrit dans un fichier, il ne faut pas oublier de fermer le flux. Sans fermeture, une partie des données peut rester dans le tampon et ne jamais être écrite sur le disque. Avec un bloc try-with-resources, la fermeture est garantie même en cas d'exception.

8Flux prédéfinis & classe Scanner

Les trois flux prédéfinis

FluxTypeDescription
System.inInputStreamEntrée standard (clavier)
System.outPrintStreamSortie standard (console)
System.errPrintStreamSortie d'erreurs standard

La classe InputStream ne propose que des méthodes élémentaires. Pour une utilisation confortable du clavier, préférez BufferedReader couplé à InputStreamReader :

Reader         reader   = new InputStreamReader(System.in);
BufferedReader keyboard = new BufferedReader(reader);

System.out.print("Entrez une ligne de texte : ");
String line = keyboard.readLine();
System.out.println("Vous avez saisi : " + line);

La classe Scanner (Java 5+)

La classe Scanner est une classe injustement méconnue du JDK qui offre des fonctionnalités très intéressantes pour parser des chaînes de caractères, en extraire et convertir les composants. Un Scanner peut se brancher sur à peu près n'importe quelle source : InputStream, Reader, File, ou encore une simple String.

// Lire un entier depuis le clavier
Scanner sc = new Scanner(System.in);
int i = sc.nextInt();

Les méthodes hasNext...() / next...() découpent la chaîne en tokens selon un délimiteur (espace blanc par défaut, configurable via useDelimiter(expression)). Elles fonctionnent sur le même principe qu'un Iterator.

Exemple : parser un CSV

String s = "Dalton;Joe;1.4\nDalton;Jack;1.6\nDalton;William;1.8\nDalton;Averell;2.0";

Scanner scan = new Scanner(s);
scan.useDelimiter(";|\n");
scan.useLocale(Locale.US);  // pour les floats

while (scan.hasNextLine()) {
    System.out.printf("%2$s %1$s : %3$.1f m%n",
        scan.next(), scan.next(), scan.nextFloat());
}
// Joe Dalton : 1.4 m
// Jack Dalton : 1.6 m
// William Dalton : 1.8 m
// Averell Dalton : 2.0 m

9Le package java.nio.file (Java 7+)

Introduit avec Java 7, java.nio.file constitue l'API moderne de Java pour la gestion des fichiers, des répertoires et des systèmes de fichiers. Il remplace progressivement les classes historiques de java.io en proposant une approche plus robuste, plus lisible et mieux adaptée aux systèmes actuels.

Ce package est centré autour de trois notions fondamentales :

L'interface Path

Path représente le chemin vers un fichier ou un répertoire. Elle remplace avantageusement java.io.File : elle est indépendante du système d'exploitation et peut être relative ou absolue.

Path p1 = Path.of("data", "fichier.txt");   // Java 11+
Path p2 = Paths.get("/home/user/data");         // Java 7+

p1.getFileName();   // → fichier.txt
p1.getParent();     // → data
p1.getRoot();       // → null (chemin relatif)
p2.normalize();     // simplifie les . et ..

La classe Files

Files est une classe utilitaire fournissant des méthodes statiques pour manipuler les fichiers : création, suppression, copie, déplacement, lecture, écriture, accès aux attributs et parcours de répertoires.

Lecture

// Lecture complète du fichier en une String
String contenu = Files.readString(Path.of("fichier.txt"));

// Lecture ligne par ligne
List<String> lignes = Files.readAllLines(Path.of("fichier.txt"));

// Lecture en streaming (économique en mémoire)
Files.lines(Path.of("fichier.txt")).forEach(System.out::println);

Écriture

// Écriture simple (écrase le fichier)
Files.writeString(Path.of("test.txt"), "Bonjour\n");

// Écriture en mode ajout (append)
Files.writeString(Path.of("test.txt"), "Suite\n",
    StandardOpenOption.CREATE,
    StandardOpenOption.APPEND);

Gestion des fichiers et répertoires

// Création
Files.createFile(Path.of("nouveau.txt"));
Files.createDirectories(Path.of("data/logs"));

// Copie et déplacement
Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
Files.move(src, dest);

// Suppression
Files.delete(path);
Files.deleteIfExists(path);

// Informations
Files.size(path);
Files.getLastModifiedTime(path);
Files.isDirectory(path);
Files.isRegularFile(path);

Parcours d'arborescences

// Contenu immédiat d'un répertoire
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Path.of("data"))) {
    for (Path p : stream)
        System.out.println(p);
}

// Parcours récursif (Java 8+, style fonctionnel)
Files.walk(Path.of("data"))
     .filter(Files::isRegularFile)
     .forEach(System.out::println);
java.nio.file : à adopter sans hésitation Ce package constitue aujourd'hui la référence pour la gestion des fichiers en Java. Il offre une API moderne, cohérente et performante, adaptée aussi bien à l'enseignement qu'au développement professionnel. Sa maîtrise est indispensable pour tout développeur Java.

10La sérialisation

La sérialisation consiste à prendre un objet en mémoire et à en sauvegarder l'état sur un flux de données (vers un fichier, par exemple). Ce concept permet aussi de reconstruire, ultérieurement, l'objet en mémoire à l'identique de ce qu'il était initialement. La sérialisation peut donc être considérée comme une forme de persistance des données.

Conditions

Classes utilisées

ClasseRôleMéthode clé
ObjectOutputStreamSérialisation (écriture)writeObject(obj)
ObjectInputStreamDésérialisation (lecture)readObject()

Exemple complet

// ── Sauvegarde ──────────────────────────────────────────────
void sauvegarde(String chemin) {
    try (ObjectOutputStream oos = new ObjectOutputStream(
                                        new FileOutputStream(chemin))) {
        oos.writeObject(this);
    } catch (Exception e) {
        System.err.println("Erreur : " + e);
    }
}

// ── Relecture ───────────────────────────────────────────────
static Object relecture(String chemin) {
    try (ObjectInputStream ois = new ObjectInputStream(
                                       new FileInputStream(chemin))) {
        return ois.readObject();
    } catch (Exception e) {
        System.err.println("Erreur : " + e);
        return null;
    }
}
🔐
Attention à la sécurité La désérialisation d'objets provenant de sources non fiables présente des risques de sécurité importants (attaques par désérialisation). Pour les nouvelles applications, préférez des formats texte comme JSON ou XML pour la persistance des données.

Récapitulatif des classes principales

BesoinClasse(s) recommandée(s)Package
Lire/écrire des octets depuis/vers un fichierFileInputStream / FileOutputStreamjava.io
Lire/écrire des types Java (int, double…)DataInputStream / DataOutputStreamjava.io
Lire un fichier texte ligne par ligneBufferedReader + FileReaderjava.io
Écrire du texte dans un fichierBufferedWriter + FileWriterjava.io
Lire depuis la consoleScanner ou BufferedReaderjava.util / java.io
Gestion moderne des fichiers (Java 7+)Files + Pathjava.nio.file
Accès direct à n'importe quelle positionRandomAccessFilejava.io
Persister un objet JavaObjectOutputStream / ObjectInputStreamjava.io