ICS3U

Accueil > Programmer avec Java >

🛠️ Lire et écrire des fichiers

Survol et attentes

Définitions

File : une classe Java qui permet de manipuler des fichiers et des répertoires. On importe cette classe avec import java.io.File;. File nous permet de créer, supprimer, renommer, etc. des fichiers et des répertoires. Au plus simple, on utilise un objet de type File comme source pour un Scanner.

  • Par exemple, Scanner fileReader = new Scanner(new File("nomDuFichier.txt")).

FileWriter : une classe Java qui permet d’écrire des données dans un fichier texte avec la méthode write(). On importe cette classe avec import java.io.*;. On a plusieurs options pour le créer un objet de ce type mais les deux les plus communs sont :

  • new FileWriter("nomDuFichier.txt") -> En créant ce FileWriter, le contenu existant du fichier sera remplacé par les appels de write.
  • new FileWriter("nomDuFichier.txt", true) -> En créant ce FileWriter et en assignant true au 2e paramètre, on ajoute du contenu à la fin du fichier avec les nouveaux appels de write.

objet anonyme : un objet qui n’a pas de nom, donc qui est utilisable seulement à l’endroit où il est déclaré. On peut créer un objet anonyme directement dans un appel de méthode.

  • Par exemple, new Scanner(new File("nomDuFichier.txt")) crée un objet Scanner qui lit le nouvel objet anonyme File.
  • Cette approche remplace la déclaration d’une variable pour l’objet, p. ex. File inputFile = new File("./data/input.txt"); suivie de Scanner fileReader = new Scanner(inputFile);.

chemin de fichier : séquence de dossiers à partir d’un point de référence, se terminant avec le nom complet du fichier, soit son nom et son extension.

  • Pour un projet Java, le point de référence est le dossier du projet, donc le chemin commence par ./.
  • Par exemple, ./data/input.txt pour un fichier texte input.txt qui se trouve dans un dossier data à la racine du projet.

références de dossier spéciales : On peut inclure certaines références spéciales au début d’un chemin comme :

  • .. pour le dossier parent,
  • . pour le dossier actuel,
  • ~ pour le dossier utilisateur et
  • / pour le dossier racine.

classe de données : une classe qui ne contient que des variables pour stocker des données. On l’utilise pour organiser des données lues d’un fichier texte, notamment en créent un tableau d’objets de cette classe.

Objectifs d’apprentissage

À la fin de cette leçon vous devrez être en mesure de :

Critères de succès

Gérer les exceptions

Parce qu’on essaie d’accéder à une ressource - un fichier - qui n’existe peut-être pas (si on l’a mal nommé ou si le chemin est mal formé ou si son dossier n’a pas été crée en premier), on utilise généralement un bloc try-catch avec ressources pour l’action de lire ou écrire dans un fichier. Le format général est :

1
2
3
4
5
6
7
8
try (déclarer et initialiser la ressource) {
  // code pour lire ou écrire dans le fichier
  /* retourner une valeur utile et/ou assigner les valeurs lues 
  à des variables déclarées à l'extérieur du bloc try-catch */
} catch (Exception e) {
  // message d'erreur
  // retourner/assigner une valeur par défaut
}

Un avantage majeur du bloc try-catch est que la ressource est automatiquement fermée à la fin du bloc, même si une exception est lancée. Cela évite les fuites de ressources et les erreurs de programmation.

Un autre avantage est qu’en cas de plantage, on a une valeur connue qu’on peut utiliser dans une condition pour éviter que le reste du programme ne plante. Par exemple, si on sait que la valeur d’un String lue revient null en cas d’erreur, on peut tester si la valeur est null comme pré-condition pour continuer le programme.

Il y a deux exemples de code dans la leçon sur les exceptions qui représentent des bons points de départ pour lire et écrire des fichiers :

D’autres exemples d’algorithmes incluant des exceptions sont fournis plus bas.

Connaître la structure du fichier

Pour bien lire un fichier, il faut avoir une idée de sa structure ou, mieux, connaître exactement sa structure.

Cela vous permet de développer un plan pour lire le fichier, notamment :

  • Est-ce que la première ligne est spéciale, avec un chiffre indiquant le nombre de lignes de données, par exemple ? -> Cela vous permet de déclarer un tableau de taille appropriée pour les données et de fixer une limite ferme pour la boucle de lecture.
  • Est-ce que les données sur une ligne sont séparées par des virgules, des espaces, des tabulations, etc. ? -> Cela vous permet de configurer le Scanner correctement avec useDelimiter()
  • Quels types de données se trouvent à chaque position sur une ligne ? -> Cela vous permet de configurer la suite d’instructions next* pour lire les données
  • Est-ce que les nombres décimaux utilisent un point ou une virgule pour le séparateur décimal ? -> Cela vous permet de configurer le Scanner correctement avec useLocale()

Un objet sur mesure pour nos données

Connaissant la structure des données sur chaque ligne d’un fichier, vous pouvez vous casser la tête à savoir comment organiser toute l’information lue afin de pouvoir l’utiliser dans votre programme et pas seulement l’afficher à l’écran.

La solution est de créer une petite classe qui ne contient aucune méthode mais juste des variables pour stocker les données. On appelle cette classe une classe de données.1 2

Par exemple, plus loin on voit un exemple où on veut lire le mois (String), la température moyenne (double) et la précipitation (int) pour chaque mois de l’année. On pourrait créer une classe WeatherData avec les variables month, averageTemp et precipitation pour stocker ces données.

1
2
3
4
5
6
7
8
9
10
11
12
/** Définit notre propre structure de données */
class WeatherData {
  String month;
  double averageTemp;
  int precipitation;
}

void main() {
  // déclare un tableau qui contient des éléments de type WeatherData
  WeatherData[] monthlyWeather = new WeatherData[12]; 
  //... lire les données dans le tableau
}

Sans notre propre type de données on aurait dû utiliser des tableaux séparés de String, double et int pour stocker les données. Cela aurait été plus difficile à lire et à maintenir. Et dans Java, on ne peut retourner qu’une seule valeur d’une méthode, alors on n’aurait pas pu retourner les trois tableaux ensemble. On serait obligé de déclarer ces tableaux globalement (dans la classe).

Dans la boucle de lecture, on peut assigner les valeurs lues aux 3 variables d’instance de chacune des 12 objets de la classe WeatherData et ensuite retourner le tableau pour l’utiliser dans le reste du programme. Par exemple :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.*;
import java.util.*;

void main() {
  Scanner fileReader = new Scanner(new File("weather_data2022.txt"));
  WeatherData[] monthlyWeather = getWeatherData(fileReader);
  filereader.close();

  // ...code pour utiliser monthlyWeather
}

class WeatherData {
  String month;
  double averageTemp;
  int precipitation;
}

WeatherData[] getWeatherData(Scanner fileReader) {
  WeatherData[] weather = new WeatherData[12]; // créer un tableau de 12 objets
  for (int i = 0; i < 12; i++) {
    weather[i] = new WeatherData(); // initialiser chaque objet
    // initialiser ses variables
    weather[i].month = fileReader.next(); 
    weather[i].averageTemp = fileReader.nextDouble();
    weather[i].precipitation = fileReader.nextInt();
  }
  return weather; // retourner le tableau
}

Une classe comme WeatherData est le début de la programmation orientée-objet, le thème du cours ICS4U.

Lire un fichier texte avec un Scanner

Il y a plusieurs façons de lire un fichier texte avec un Scanner.

  • Pour un fichier simple, on peut créer un seul objet Scanner (pour le fichier) et utiliser une séquence de next* pour lire les données.
  • Pour un fichier qui contient des données structurées qui se répètent, soit un tableau de valeurs comme, p. ex., un fichier CSV, en plus du Scanner pour le fichier, on doit généralement créer une boucle pour lire les données ligne-par-ligne.
  • Pour un fichier de texte non structurée (comme une histoire), on peut extraire le contenu du fichier au complet dans un String et utiliser un Scanner pour lire ce String mot-par-mot ou phrase-par-phrase ou utiliser d’autres méthodes String pour l’enquêter.

Il y a plusieurs autres approches possibles, alors ces trois-ci ne sont que des points de départ. Dans tous les cas, l’algorithme ressemble généralement à ceci :

  1. Déclarer des variables pour stocker les données lues.
  2. Extraire les données dans un bloc try-catch avec ressources (la déclaration du Scanner pour le fichier); définir des valeurs par défaut en cas d’erreur.
  3. Si la valeur lue est la valeur en cas d’erreur (clause de garde) :
    • quitter le programme (ou éviter autrement l’utilisation du code qui dépend des valeurs lues).
  4. Sinon :
    • utiliser les données lues dans le reste du programme.
Lire un fichier simple Prenons l'exemple d'un fichier qui conserve le meilleur pointage obtenu en jouant votre jeu. Le fichier `highscore.txt` pourrait ressembler à ceci : ```plaintext David 99 ``` On sait qu'il contient juste un nom (de type `String`) et un pointage (de type `int`). On peut lire ce fichier avec le code suivant : ```java void main() { String name; // variables déclarées à l'extérieur du bloc try-catch int score; try (Scanner fileReader = new Scanner(new File("./highscore.txt"))) { name = fileReader.next(); // valeurs en cas de succès score = fileReader.nextInt(); } catch (Exception e) { System.out.println("Erreur de lecture du fichier."); name = null; // valeurs en cas d'erreur score = 0; } // précondition ("clause de garde") if (name == null) { System.out.println("Aucun pointage n'a été enregistré."); // possiblement quitter le programme avec return; } // ...code pour utiliser name et score } ```
Lire un fichier structuré Prenons l'exemple d'un fichier qui conserve les températures moyennes pour chaque mois de l'année, incluant les valeurs maximum et minimum, et la quantité de précipitation en mm. Le fichier `weather.txt` pourrait ressembler à ceci : ```plaintext Ottawa Mois;Tmoy(C);Tmin(C);Tmax(C);Précip(mm) Janvier;-9,1;-18,2;11,4;114 Février;-11,3;-22,4;10,2;98 Mars;-8,2;-12,3;17,3;140 ... Décembre;-12,3;-16,4;12,3;87 ``` On voit : - la première ligne donne juste le nom de la ville - la deuxième ligne donne les noms des colonnes et les unités de mesure - il y 12 lignes de données par la suite (1 pour chaque mois) - les valeurs sont séparées par des points-virgules - les nombres décimaux utilisent une virgule comme séparateur décimal Si on s'intéresse juste aux valeurs des températures moyennes et de la précipation, on peut prévoir les étapes suivantes : - configurer le Scanner pour lire les données avec `useDelimiter(";")` et `useLocale(Locale.CANADA_FRENCH)` - des commandes spéciales pour lire la ville et ignorer la ligne avec les noms de colonne - une boucle qui fait 12 itérations pour lire les données mensuelles ligne-par-ligne : un `next` pour le mois, 1 `nextDouble` pour la température, 2 `next` qu'on ignore et un `nextInt` pour les précipitations Une implémentation possible utilisant la classe `WeatherData` décrite plus haut est la suivante : ```java void main() { WeatherData[] weather; // variable déclarée à l'extérieur du bloc try-catch String city; try (Scanner fileReader = new Scanner(new File("./weather.txt"))) { fileReader.useDelimiter(";"); fileReader.useLocale(Locale.CANADA_FRENCH); city = fileReader.nextLine(); // première ligne fileReader.nextLine(); // ignorer l'en-tête à la 2e ligne weather = new WeatherData[12]; // pour les 12 lignes de données for (int i = 0; i < 12; i++) { weather[i] = new WeatherData(); // initialiser chaque objet weather[i].month = fileReader.next(); // initialiser ses variables weather[i].averageTemp = fileReader.nextDouble(); fileReader.next(); // ignorer la température minimale fileReader.next(); // ignorer la température maximale weather[i].precipitation = fileReader.nextInt(); } } catch (Exception e) { System.out.println("Erreur de lecture du fichier."); weather = null; // valeurs en cas d'erreur } // précondition ("clause de garde") if (weather == null) { System.out.println("Aucune donnée météo n'a été enregistrée."); // possiblement quitter le programme avec return; } // ...code pour utiliser weather } ```
Lire un fichier non structuré Prenons l'exemple d'un fichier qui contient une histoire. Le fichier `story.txt` pourrait ressembler à ceci : ```plaintext Il était une fois, dans une galaxie lointaine, très lointaine... ``` Ici le but est juste d'extraire le texte au complet afin de l'analyser par la suite. Une technique efficace est d'utiliser le caractère spéciale `\\Z` qui désigne **la fin d'un fichier** comme délimiteur du Scanner. Lire le ficher revient alors à un seul appel de `next` : ```java void main(){ String story; // variable déclarée à l'extérieur du bloc try-catch try (Scanner fileReader = new Scanner(new File("./story.txt"))) { fileReader.useDelimiter("\\Z"); // configurer le Scanner story = fileReader.next(); // lire le fichier au complet } catch (Exception e) { System.out.println("Erreur de lecture du fichier."); story = null; // valeurs en cas d'erreur } // précondition ("clause de garde") if (story == null) { System.out.println("Aucune histoire n'a été enregistrée."); // possiblement quitter le programme avec return; } // ...code pour utiliser story, // incluant potentiellement d'autres Scanners } ```

Écrire dans un fichier texte avec un FileWriter

🛠️ Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices.

Lire un fichier simple

  1. Créez un fichier FileReading1.java et y ajouter sa méthode main.
  2. Créez un fichier texte moodJournal.txt qui contient la ligne suivante : 2024-04-27 heureux. Implémentez un algorithme dans main qui :
    1. Lit le contenu du fichier moodJournal.txt dans un bloc try-catch et affiche son contenu à la console.
    2. Lit le contenu du fichier moodJournal.txt dans un bloc try-catch et assigne les valeurs individuelles à des variables pour la date et l’humeur (deux String). Préparer un message à l’extérieur du bloc try-catch qui affiche la date et l’humeur.
    3. (DÉFI) Lit le contenu du fichier moodJournal.txt dans un bloc try-catch et assigne les valeurs individuelles à des variables pour l’année, le mois, le jour (3 int) et l’humeur (String).

      Trois pistes pour Scanner les parties de la date : (1) modifiez le délimiteur du Scanner (“-“ et “ “), (2) utilisez d’abord next pour la date entière et déclare un Scanner sur ce texte avec un délimiteur de “-“ pour saisir les int, (3) utiliser split("-") sur la date entière et Integer.parseInt sur chaque élément du tableau.

Lire un fichier structuré

  1. Créer un fichier FileReading2.java et y ajouter sa méthode main.

Écrire dans un fichier

  1. Créer un fichier FileWriting.java et y ajouter sa méthode main.
  1. Java offre aussi une structure de données appelée Record qui est plus spécifique qu’une classe mais exactement pour ce genre de situation, mais qui masque les données, ce qui est un concept plus avancé qu’on voit seulement en 12e année. 

  2. Les exemples de code ci-dessous appliquent le JEP 463 (classe abstraite, méthode maind’instance), alors seulement la classe interne pour la structure de données est déclarée dans le code.