Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 43

Lecture et écriture de fichiers avec Java NIO et Streams

Recommended resources

Bonjour ! Dans nos leçons précédentes, nous avons exploré en profondeur la puissance de l'API Stream pour traiter des collections de données en mémoire. Vous avez appris à enchaîner des opérations pour filtrer, transformer et agréger des données de manière déclarative et élégante.

Aujourd'hui, nous allons appliquer ces compétences à un domaine essentiel pour tout développeur backend : la manipulation de fichiers. Nous allons abandonner l'ancienne API java.io pour nous concentrer sur la nouvelle API java.nio (NIO.2), introduite en Java 7. L'objectif de cette leçon est de vous apprendre à lire et écrire des fichiers texte de manière efficace en combinant l'API java.nio (notamment Files et Path) avec les Streams. Cette compétence est cruciale pour des tâches courantes comme l'analyse de logs, le traitement de fichiers de configuration ou la manipulation de fichiers de données comme les CSV.

1. La "Nouvelle" API de Fichiers : Path et Files

Avant Java 7, la manipulation de fichiers se faisait principalement avec la classe java.io.File. Cette classe présentait plusieurs inconvénients, notamment une gestion des erreurs peu pratique (retours de boolean ou null) et un manque de méthodes pour des opérations courantes comme la copie.

Java 7 a introduit une refonte complète avec le package java.nio.file. Les deux piliers de cette nouvelle API sont :

  • Path : Une interface qui représente le chemin d'un fichier ou d'un répertoire d'une manière abstraite, indépendante du système d'exploitation.
  • Paths : Une classe utilitaire avec une méthode statique get() pour créer des objets Path à partir d'une chaîne de caractères.
  • Files : Une classe utilitaire qui contient des dizaines de méthodes statiques pour effectuer des opérations sur les fichiers et répertoires (créer, supprimer, copier, lire, écrire...).

Cette nouvelle approche est plus robuste, plus complète et s'intègre parfaitement avec les fonctionnalités modernes de Java.

Pour vous familiariser avec ces nouvelles briques de base, je vous propose de lire les sections suivantes d'un article qui explique très bien cette transition.

Fichiers Java, Chemin

Cet article de Codegym.cc introduit de manière claire et concise les nouvelles classes Paths, Path et Files et les compare à l'ancienne classe File.

Lisez l'introduction et les sections sur "Paths", "Path" et "Files". L'objectif est de comprendre le rôle de chaque composant et de voir des exemples simples de création de chemin (Paths.get()), de manipulation de chemin (getFileName(), normalize()) et d'opérations sur les fichiers (createFile(), copy(), delete()).


2. Lire des Fichiers Texte Efficacement

La classe Files offre plusieurs manières de lire un fichier. Le choix dépend principalement de la taille du fichier.

Pour les petits fichiers : Files.readAllLines()

Si vous travaillez avec un fichier de petite taille (quelques centaines ou milliers de lignes), la méthode la plus simple est Files.readAllLines(Path path). Elle lit l'intégralité du fichier et retourne une List<String>, où chaque élément est une ligne du fichier.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class SmallFileReader {
    public static void main(String[] args) {
        Path filePath = Paths.get("data/salutations.txt");
        
        try {
            List<String> lines = Files.readAllLines(filePath);
            lines.stream()
                 .filter(line -> line.startsWith("B"))
                 .forEach(System.out::println);
        } catch (IOException e) {
            System.err.println("Erreur lors de la lecture du fichier : " + e.getMessage());
        }
    }
}

Créez un dossier data à la racine de votre projet et un fichier salutations.txt dedans avec le contenu : "Bonjour", "Hello", "Buenas dias".

Cette approche est simple, mais elle a un inconvénient majeur : elle charge tout le fichier en mémoire. Si le fichier fait plusieurs gigaoctets, votre application risque une OutOfMemoryError.

Pour les fichiers de toute taille : Files.lines()

C'est ici que la magie opère. La méthode Files.lines(Path path) est la solution la plus puissante et la plus idiomatique en Java moderne. Elle ne lit pas tout le fichier en mémoire. À la place, elle retourne un Stream<String> qui lit le fichier ligne par ligne de manière paresseuse (lazy).

Cela signifie que chaque ligne n'est lue du disque qu'au moment où le Stream en a besoin. C'est extrêmement efficace en termes de mémoire et cela nous permet de traiter des fichiers de taille quasi illimitée.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

public class LargeFileReader {
    public static void main(String[] args) {
        Path filePath = Paths.get("data/mon_gros_fichier_log.txt");

        // Le try-with-resources garantit que le fichier est bien fermé à la fin
        try (Stream<String> linesStream = Files.lines(filePath)) {
            long errorCount = linesStream
                .filter(line -> line.contains("ERROR"))
                .count();
            
            System.out.println("Nombre de lignes d'erreur trouvées : " + errorCount);
        } catch (IOException e) {
            System.err.println("Erreur lors de la lecture du fichier : " + e.getMessage());
        }
    }
}

Point important : Le Stream retourné par Files.lines() est lié à une ressource système (le fichier ouvert). Il est impératif de le fermer après utilisation pour éviter les fuites de ressources. La meilleure pratique est d'utiliser un bloc try-with-resources, comme dans l'exemple ci-dessus.


3. Écrire dans des Fichiers Texte

Pour l'écriture, la classe Files propose également des méthodes très pratiques.

La méthode Files.write(Path path, Iterable<? extends CharSequence> lines, OpenOption... options) est particulièrement utile. Elle permet d'écrire une collection de chaînes de caractères (comme une List<String>) dans un fichier.

Les OpenOption permettent de spécifier le comportement de l'écriture :

  • StandardOpenOption.CREATE : Crée le fichier s'il n'existe pas.
  • StandardOpenOption.TRUNCATE_EXISTING : S'il existe, vide le fichier avant d'écrire (comportement par défaut si vous écrivez).
  • StandardOpenOption.APPEND : Ajoute le contenu à la fin du fichier s'il existe.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;

public class FileWriterExample {
    public static void main(String[] args) {
        Path outputPath = Paths.get("data/rapport.txt");
        List<String> reportLines = List.of(
            "Rapport du jour",
            "----------------",
            "Ventes: 150 unités",
            "Statut: OK"
        );

        try {
            // Écrase le fichier s'il existe, sinon le crée
            Files.write(outputPath, reportLines); 
            System.out.println("Rapport initial écrit.");

            // Ajoute une ligne à la fin du fichier
            Files.write(outputPath, List.of("Fin du rapport."), StandardOpenOption.APPEND);
            System.out.println("Ligne ajoutée au rapport.");

        } catch (IOException e) {
            System.err.println("Erreur lors de l'écriture du fichier : " + e.getMessage());
        }
    }
}

La ressource suivante est très complète et détaille de nombreuses autres méthodes pour lire et écrire des fichiers. Je vous invite à la parcourir pour consolider vos connaissances.

Développons en Java - NIO 2 - Jean-Michel Doudoux

Ce chapitre du tutoriel "Développons en Java" de J.-M. Doudoux est une référence technique très détaillée sur NIO.2.

Parcourez la section 16.8. La lecture et l'écriture dans un fichier. Concentrez-vous sur les sous-sections : 16.8.1. Les options d'ouverture d'un fichier : Comprenez le rôle des StandardOpenOption. 16.8.2. La lecture et l'écriture de l'intégralité d'un fichier : Retrouvez les méthodes readAllLines() et write(). 16.8.3. La lecture et l'écriture bufférisées d'un fichier : Notez l'existence de newBufferedReader() / newBufferedWriter() qui offrent une alternative. Vous pouvez survoler les sections sur les flux d'octets et les channels, qui sont plus avancées.

L'image ci-dessous, tirée de cet article, résume bien le spectre des options disponibles, des plus simples aux plus avancées.

Ce schéma positionne les différentes méthodes de lecture/écriture de fichiers en Java sur un axe allant des besoins simples (lire tout un petit fichier) aux besoins avancés (contrôle fin sur les buffers et les accès). Pour nos cas d'usage avec les Streams, `Files.lines()` et `Files.write()` sont souvent le meilleur compromis.

4. Cas Pratique : Traitement d'un Fichier CSV avec des Streams

Mettons tout cela en pratique. C'est un scénario typique pour un développeur backend : recevoir un fichier de données, le transformer, et générer un nouveau fichier en sortie.

Objectif : Lire un fichier products.csv contenant des produits, appliquer une augmentation de 10% sur le prix des produits de la catégorie "Électronique", et écrire le résultat dans un nouveau fichier updated_products.csv.

1. Créez le fichier d'entrée data/products.csv :

ID,Name,Category,Price
1,Souris Gamer,Électronique,49.99
2,T-shirt Coton,Vêtements,19.99
3,Écran 27 pouces,Électronique,349.00
4.Livre Java,Livres,39.95

2. Créez une classe Product pour représenter les données :

public class Product {
    private int id;
    private String name;
    private String category;
    private double price;

    // Constructeur, getters, setters, et une méthode toString()
    // ...

    public Product(int id, String name, String category, double price) {
        this.id = id;
        this.name = name;
        this.category = category;
        this.price = price;
    }
    
    // Getters
    public int getId() { return id; }
    public String getName() { return name; }
    public String getCategory() { return category; }
    public double getPrice() { return price; }

    // Setter pour le prix
    public void setPrice(double price) { this.price = price; }

    @Override
    public String toString() {
        return "Product{" + "id=" + id + ", name='" + name + '\'' + ", category='" + category + '\'' + ", price=" + price + '}';
    }
    
    // Méthode pour formater en ligne CSV
    public String toCsvLine() {
        return String.join(",", String.valueOf(id), name, category, String.valueOf(price));
    }
}

3. Écrivez le code de traitement :

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CsvProcessor {

    public static void main(String[] args) {
        Path inputPath = Paths.get("data/products.csv");
        Path outputPath = Paths.get("data/updated_products.csv");

        try (Stream<String> lines = Files.lines(inputPath)) {
            // 1. Lire, sauter l'en-tête, et parser les lignes en objets Product
            List<Product> products = lines
                .skip(1) // On saute la ligne d'en-tête CSV
                .map(line -> {
                    String[] fields = line.split(",");
                    return new Product(
                        Integer.parseInt(fields[0]),
                        fields[1],
                        fields[2],
                        Double.parseDouble(fields[3])
                    );
                })
                .collect(Collectors.toList());

            // 2. Appliquer la logique métier avec un autre Stream
            List<String> updatedCsvLines = products.stream()
                .peek(product -> { // peek est utile pour appliquer une action sans modifier le stream
                    if ("Électronique".equals(product.getCategory())) {
                        product.setPrice(product.getPrice() * 1.10); // Augmentation de 10%
                    }
                })
                .map(Product::toCsvLine) // Reconvertir les objets en lignes CSV
                .collect(Collectors.toList());

            // 3. Ajouter l'en-tête et écrire dans le fichier de sortie
            String header = "ID,Name,Category,Price";
            updatedCsvLines.add(0, header); // Ajoute l'en-tête au début de la liste

            Files.write(outputPath, updatedCsvLines);

            System.out.println("Le fichier " + outputPath + " a été généré avec succès.");

        } catch (IOException e) {
            System.err.println("Une erreur est survenue : " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Après exécution, votre fichier updated_products.csv devrait contenir :

ID,Name,Category,Price
1,Souris Gamer,Électronique,54.989
2,T-shirt Coton,Vêtements,19.99
3,Écran 27 pouces,Électronique,383.9
4,Livre Java,Livres,39.95

Cet exemple illustre parfaitement la puissance de cette combinaison : Files.lines() pour lire efficacement, l'API Stream pour une logique de transformation claire et concise, et Files.write() pour sauvegarder le résultat.


Conclusion

Vous maîtrisez maintenant une technique fondamentale du développement Java moderne : le traitement de fichiers texte en utilisant java.nio et les Streams.

Points clés à retenir :

  • Privilégiez l'API java.nio (Path, Files) à l'ancienne java.io.File.
  • Pour les petits fichiers, Files.readAllLines() est une option simple qui retourne une List<String>.
  • Pour les fichiers de toute taille, Files.lines() est la méthode la plus performante et idiomatique. Elle crée un Stream<String> paresseux, évitant de charger tout le fichier en mémoire.
  • Pensez toujours à fermer le Stream obtenu via Files.lines() en utilisant un bloc try-with-resources.
  • Utilisez Files.write() avec une Iterable<String> et des StandardOpenOption pour écrire des données de manière contrôlée.
  • La combinaison Files.lines() -> Stream processing -> collect -> Files.write() est un pattern extrêmement puissant pour les tâches de transformation de données.

Cette leçon conclut notre module sur la programmation fonctionnelle. Dans la prochaine leçon, nous franchirons une étape majeure pour devenir développeur backend : nous abandonnerons les fichiers plats pour nous connecter à une véritable base de données. Nous commencerons le module sur JDBC (Java Database Connectivity) et apprendrons à établir une connexion avec une base de données MySQL et à y exécuter nos premières requêtes SQL depuis une application Java.