Keyboard shortcuts

Touchez ← ou → pour naviguer les chapitres

Touchez S ou / pour chercher dans le livre

Touchez ? pour afficher ce message

Touchez Esc pour masquer ce message

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 :

  • ReconnaĂźtre les ressemblances entre lire et Ă©crire Ă  la console et lire et Ă©crire dans des fichiers texte.
  • Appliquer les techniques de Scanner pour lire des fichiers directement ou pour lire un String.
  • Utiliser les classes File et FileWriter pour manipuler des fichiers texte.

CritĂšres de succĂšs

  • Je peux intĂ©grer la lecture de fichiers texte ou l’écriture de fichiers texte dans mes programmes Java.

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 :

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.

/** 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 :

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 :

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 :

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 :

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 :

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 :

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 :

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
}

đŸ› ïž 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. ↩

© 2022-2025 David Crowley