Fichier et Stream d’Entrée-Sortie IFT1025, Programmation 2 Jian-Yun Nie
Concepts Notion de fichier et de stream Opérations: ouverture, lecture/écriture, fermeture Format texte vs. binaire Accès séquentiel vs. aléatoire Organisation des indexes
Fichier Unité de stockage des données, sur disque dur, stockage permanent (vs. en mémoire vive) Un fichier contient un ensemble d’enregistrements Traitement fichier CPU Mémoire vive tampon
Fichier en Java Stream: une suite de données (octets ou caractères)
Opérations typiques Lecture: –Ouvrir un stream –Lire tant qu’il y a des données –Fermer le stream Écriture –Ouvrir un stream (ou créer un fichier) –Écrire des données tant qu’il y en a –Fermer le stream Établir un canal de communication Relâcher les ressources allouées Écrire ce qu’il est dans le tampon, et Relâcher les ressources allouées
Exemple: TP1 public static void main(String[] args) { ouvrir_fichier("liste_mots"); traiter_fichier(); fermer_fichier(); } public static void ouvrir_fichier(String nom) { try { input = new BufferedReader( new FileReader(nom)); } catch (IOException e) { System.err.println("Impossible d'ouvrir le fichier d'entree.\n" + e.toString()); System.exit(1); } public static void traiter_fichier() { String ligne; try { // catch EOFException ligne = input.readLine(); while (ligne != null) { System.out.println(ligne); ligne = input.readLine(); } public static void fermer_fichier() { try { input.close(); System.exit(0); } catch (IOException e) { System.err.println("Impossible de fermer les fichiers.\n" + e.toString()); }
Un autre exemple public void readFile() { FileReader fileReader = null; try { fileReader = new FileReader("input.txt"); int c = fileReader.read(); while (c != -1) { char d = ((char)c); c = fileReader.read(); } } catch (FileNotFoundException e) { System.out.println("File was not found"); } catch (IOException e) { System.out.println("Error reading from file"); } if (fileReader != null) { try { fileReader.close(); } catch (IOException e) { /* ignore */ } }
Deux unité de base Caractère (2 octets=16 bits) ou octet (8 bits) Deux hiérarchies de classe similaires (mais en parallèle)
Hiérarchies En haut des hiérarchies pour stream de caractères: 2 classes abstraites Reader java.lang.Object java.io.Reader Writer java.lang.Object java.io.Writer Implantent une partie des méthodes pour lire et écrire des caractère de 16 bits (2 octets)
Hiérarchie de Stream de caractère Les sous-classe de Reader simplepré-traitement Chaque sous-classe ajoute des méthodes
Hiérarchie de Stream de caractère Les sous-classe de Writer
Hiérarchies Byte Stream System.in
Hiérarchie de Byte Stream System.out System.err
Différence: caractère vs byte Reader: –int read() –int read(char cbuf[]) –int read(char cbuf[], int offset, int length) InputStream: –int read() –int read(byte cbuf[]) –int read(byte cbuf[], int offset, int length)
Utilisation de différentes classes En mémoire: –CharArrayReader, CharArrayWriter –ByteArrayInputStream, ByteArrayOutputStream –Lire/écrire dans un tableau de bytes –StringReader, StringWriter, StringBufferInputStream –Lire/écrire dans un String Pipe: –PipedReader, PipedWriter –PipedInputStream, PipedOutputStream –Relier la sortie d’un Stream comme une entrée d’un autre stream (pipeline)
Utilisation Fichier (*) –FileReader, FileWriter –FileInputStream, FileOutputStream –Lire/écrire dans un fichier Sérialisation –ObjectInputStream, ObjectOutputStream Conversion de données (*) –DataInputStream, DataOutputStream –Reconnaître les types de données primitifs Impression (*) –PrintWriter, PrintStream –Méthodes conviviales pour l’impression
Utilisation Buffer (Tampon) –BufferedReader, BufferedWriter –BufferedInputStream, BufferedOutputStream –Créer un tampon: accès plus efficace Filtering –FilterReader, FilterWriter –FilterInputStream, FilterOutputStream –Accepte un Stream, le filtre et ensuite passer à un autre Stream Convertir entre byte et caractère –InputStreamReader, OutputStreamWriter –Lire des bytes en caractère, ou écrire des caractère en byte
Exemple Utiliser FileReader et FileWriter Méthodes simples disponible: –int read(), int read(CharBuffer []), write(int),.. –Exemple: copier un fichier caractère par caractère (comme un int) import java.io.*; public class Copy { public static void main(String[] args) throws IOException { File inputFile = new File("farrago.txt"); File outputFile = new File("outagain.txt"); FileReader in = new FileReader(inputFile); FileWriter out = new FileWriter(outputFile); int c; while ((c = in.read()) != -1) out.write(c); in.close(); out.close(); } –Méthodes limitées Fin de fichier: -1
Augmenter les possibilités: wrap Créer un stream en se basant sur un autre: FileReader in = new FileReader(new File("farrago.txt")); Avantage: –Obtenir plus de méthodes Dans File: les méthodes pour gérer les fichier (delete(), getPath(), …) mais pas de méthode pour la lecture Dans FileReader: les méthodes de base pour la lecture –Un autre exemple: DataOutputStream out = new DataOutputStream( new FileOutputStream("invoice1.txt")); FileOutptuStream: écrire des bytes DataOutputStream: les méthodes pour les types de données de base: write(int), writeBoolean(boolean), writeChar(int), writeDouble(double), writeFloat(float), …
Pourquoi faire du wrapping? Les streams enrichis ont besoin d’un stream plus primitif dans son constructeur: –E.g. DataInputStream(InputStream in) DataOutputStream(OutputStream out)DataInputStreamInputStream DataOutputStreamOutputStream –Impossible de créer un stream directement d’un nom de fichier comme new DataOutputStream(“sortie”); –Besoin de faire du wrapping: New DataOutputStream(new FileOutputStream("sortie")) Les streams de base s’occupe des E/S de base, et les streams enrichies ajoutent d’autres possibilités Wrapping: réutiliser les méthodes de base, et ajouter d’autre méthodes
Hiérarchies
Sink stream Sink TypeCharacter StreamsByte Streams Memory CharArrayReader, CharArrayWriter StringReader, StringWriter ByteArrayInputStream, ByteArrayOutputStream StringBufferInputStream PipePipedReader, PipedWriter PipedInputStream, PipedOutputStream FileFileReader, FileWriter FileInputStream, FileOutputStream
Wrap dans quelle classe ? Dépend des méthodes dont on a besoin Quelques exemples –Lire ligne par ligne: Besoin de String readLine() (null à la fin du fichier) BufferedReader (sous classe de Reader) Traitement: –Lire une ligne (String) –Traiter cette ligne avec un Parsing (comme TP1) Constructeur: BufferedReader(Reader) new BufferedReader(new FileReader("fichier")) (FileReader est sous-classe de Reader – classe abstraite) –Écrire ligne par ligne BufferedWriter (sous-classe de Writer) write(String): écrire un String newLine(): insérer un changement de ligne Traitement: –Organiser une ligne comme String –Écrire la ligne
Wrap dans quelle classe ? Besoin: utiliser les méthodes comme dans System.out –write(int), … –print(…), println(…) Stream à utiliser: PrintWriter Constructeurs –PrintWriter(OutputStream out)PrintWriterOutputStream –PrintWriter(Writer out)PrintWriterWriter –new PrintWriter(new FileOutputStream("fichier")) –new PrintWriter(new FileWriter("fichier"))
Wrap dans quelle classe ? Besoin: écrire les données des types primitifs –writeChar(Char), writeFloat(float), … Stream à utiliser: DataOutputStream Constructeur: –DataOutputStream(OutputStream out)DataOutputStreamOutputStream –new DataOutputStream(new FileOutputStream("fichier"))
Interface: JFileChooser JFileChooser chooser = new JFileChooser(); FileReader in = null; if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { File selectedFile = chooser.getSelectedFile(); reader = new FileReader(selectedFile);... }
Attention au format Stocker les données dans un fichier Mais aussi pouvoir les ressortir E.g. stocker le nom et la date de naissance: –GéraldTremblay SusanneRoy … Au moment de stocker, déterminer un format pour pouvoir les relire Façon simple: Gérald Tremblay Susanne Roy … –Lire prénom (jusqu’à l’espace), nom, date de naissance (il faut ensuite la décomposer)
Format Stocker en binaire ou en caractère? –Caractère: lisible (e.g. avec more, vi, etc.) –Binaire: transformer en code binaire: int: 4 octets Long: 8 octets char: 2 octet … –Valeur (entier) En caractère: (10 octets) En binaire: (4 octets) = 48* Binaire plus économique en espace Binaire = espace fixe (facilite la lecture)
Exemple en binaire Pour un compte bancaire: –No. de compte: entier –Montant: double Pour écrire un enregistrement (pour un compte) –writeInt(int) –writeDouble(double) –Classe: DataOutputStream Pour lire un enregistrement –readInt() –readDouble() –Classe: DataInputStream Penser aux écritures et aux lectures en même temps
DataInputStream et DataOutputStream Lire et écrire des données des types de base –readBoolean(), readInt(), readFloat, readChar(), readLine(); readUTF(), … –writeBoolean(boolean), writeInt(int), writeFloat(float), writeChar(char), writeChars(String), writeUTF(String), … –readUTF et writeUTF: Entier en binaire correspondant à la longueur du String + String Exemple: liste_mots prep sing sur pron_rel fem sing n masc sing outil non adv non n fem sing … Références:
Sérialiser Convertir un objet (avec une structure) en une suite de données dans un fichier Reconvertir du fichier en un objet Utilisation: avec ObjectOutputStream Employee[] staff = new Employee[3]; ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test2.dat")); out.writeObject(staff); out.close();
Sérialiser Utilité de sérialisation –Stocker un objet dans un fichier –Créer une copie d’objet en mémoire –Transmettre un objet à distance Devient une transmission de String
Sérialiser Pour pouvoir sérialiser un objet: –sa classe doit implanter l’interface Serializable –Interface Serializable: Pas de méthode exigée Mais on peut réimplanter readObject() et writeObject() si on ne se contente pas de la version par défaut: defaultReadObject(), defaultWriteObject() private void writeObject(java.io.ObjectOutputStream out) throws IOException private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException –Beaucoup de classes existantes sont sérialisables (e.g. ArrayList)
Sérialiser: une classe sérialisable import java.io.*; import java.util.*; import java.util.logging.*; public class ExerciseSerializable { public static void main(String[] aArguments) { //create a Serializable List List quarks = new ArrayList(); quarks.add("up"); quarks.add("down"); quarks.add("strange"); quarks.add("charm"); quarks.add("top"); quarks.add("bottom"); try{ OutputStream file = new FileOutputStream( "quarks.ser" ); OutputStream buffer = new BufferedOutputStream( file ); output = new ObjectOutputStream( buffer ); output.writeObject(quarks); } catch(IOException ex){ fLogger.log(Level.SEVERE, "Cannot perform output.", ex); } finally{ try { if (output != null) { //flush and close "output" and its underlying streams output.close(); } catch (IOException ex ){ fLogger.log(Level.SEVERE, "Cannot close output stream.", ex); } ArrayList est sérialisable
Définir une clase sérialisable class Employee implements Serializable { public Employee(String n, double s, Date d) { name = n; salary = s; hireDate = d; } public Employee() {} public void raiseSalary(double byPercent) { salary *= 1 + byPercent / 100; } public int hireYear() { return hireDate.getYear(); } public void print() { System.out.println(name + " " + salary + " " + hireYear()); } private double salary; private String name; private Date hireDate; } class Manager extends Employee { public Manager(String n, double s, Date d, Employee e) { super(n, s, d); secretary = e; } public Manager() {} public void raiseSalary(double byPercent) { // add 1/2% bonus for every year of service Date today = new Date(); double bonus = 0.5 * (today.getYear() - hireYear()); super.raiseSalary(byPercent + bonus); } public void print() { super.print(); System.out.print("Secretary: "); if (secretary != null) secretary.print(); } private Employee secretary; }
Utiliser une classe sérialisable import java.io.*; import java.util.*; class ObjectRefTest { public static void main(String[] args) { try { Employee[] staff = new Employee[3]; Employee harry = new Employee ("Harry Hacker", 35000, new Date(1989,10,1)); staff[0] = harry; staff[1] = new Manager("Carl Cracker", 75000, new Date(1987,12,15), harry); staff[2] = new Manager("Tony Tester", 38000, new Date(1990,3,15), harry); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test2.dat")); out.writeObject(staff); out.close(); ObjectInputStream in = new ObjectInputStream( new FileInputStream ("test2.dat")); Employee[] newStaff = (Employee[])in.readObject(); for (int i = 0; i < newStaff.length; i++) newStaff[i].raiseSalary(100); for (int i = 0; i < newStaff.length; i++) newStaff[i].print(); } catch(Exception e) { e.printStackTrace(); System.exit(1); }
Sortie de sérialisation vi test2.dat: ^HEmployee~BÅ ^V 4 þ ^E Lisible par désérialisation (readObject()): Harry Hacker Carl Cracker Secretary: Harry Hacker Tony Tester Secretary: Harry Hacker
Accès séquentiel vs. aléatoire Séquentiel: Première donnée, suivante, … –Approprié pour traiter toutes les données Aléatoire (random): positionner à un endroit, lire les données à partir de cette position –Approprié pour sélectionner certaines données à traiter –Question importante: Comment déterminer la position correcte ?
RandomAccessFile Un Stream en format binaire Écrire et lire (selon l’ouverture) Possibilité de positionner avec seek(long) Exemple: file = new RandomAccessFile(filename, "rw"); file.seek(100); int accountNumber = file.readInt(); double balance = file.readDouble(); Référence:
RandomAccessFile Ouverture: file = new RandomAccessFile(filename, "rw"); Modes: r: lecture seulement rw: lecture et ecriture Mode
RandomAccessFile Lecture ou écriture –Sans seek: Position au début du fichier Écrire et lire à partir de cette position comme un accès séquentiel –seek(long) Positionner à la position (no. de bytes à partir du début) Lire ou écrire à partir de cette position
Position Comment déterminer la bonne position? –Solution simple: Chaque enregistrement = taille fixe Pour un enregistrement: déterminer son numéro seek(taille*no) –Solution plus complexe: Organiser un indexe Utiliser une table de hashage
Taille fixe pour un enregistrement 008: public class BankData 009: { 024: public void open(String filename) 025: throws IOException 026: { 027: if (file != null) file.close(); 028: file = new RandomAccessFile(filename, "rw"); 029: } 035: public int size() 036: throws IOException 037: { 038: return (int) (file.length() / RECORD_SIZE); 039: } 071: public int find(int accountNumber) 072: throws IOException 073: { 074: for (int i = 0; i < size(); i++) 075: { 076: file.seek(i * RECORD_SIZE); 077: int a = file.readInt(); 078: if (a == accountNumber) // Found a match 079: return i; 080: } 081: return -1; // No match in the entire file 082: } 056: public BankAccount read(int n) 057: throws IOException 058: { 059: file.seek(n * RECORD_SIZE); 060: int accountNumber = file.readInt(); 061: double balance = file.readDouble(); 062: return new BankAccount(accountNumber, balance); 063: } 089: public void write(int n, BankAccount account) 090: throws IOException 091: { 092: file.seek(n * RECORD_SIZE); 093: file.writeInt(account.getAccountNumber()); 094: file.writeDouble(account.getBalance()); 095: } 096: 097: private RandomAccessFile file; 098: 099: public static final int INT_SIZE = 4; 100: public static final int DOUBLE_SIZE = 8; 101: public static final int RECORD_SIZE 102: = INT_SIZE + DOUBLE_SIZE; 103: }
Accès dans RandomAccessFile Exploiter seek pour déterminer la position pour lire ou écrire un enregistrement –Séquentiel: lire chaque enregistrement à partir du début –Direct: déterminer une position selon une clé, et lire/écrire l’enregistrement Position = no. de compte * taille: –clé = no. de compte Position est déterminer selon une conversion avec une clé (e.g. code permanent GILB ) –Non numérique –Non compact: valeur non continue
Cas 1: Recherche binaire Les enregistrements sont stockés dans l’ordre croissant des clés Accès binaire (O(log(n)): –Chercher(clé, début, fin): Lire le milieu Si clé_milieu == clé, trouvé Si clé_milieu < clé, –Si (milieu – début) < 2, introuvable; –Sinon cherche(clé, début, milieur-1) Si clé_milieu > clé, –Si (fin – milieu) < 2, introuvable; –Sinon cherche(clé, milieu+1, fin)
Recherche binaire Fichier
Cas 2: table de hashage Table de hashage: déterminer une position pour chaque clé Fonction de hashage: clé entier Contraintes: –Le plus compacte possible (pas beaucoup de positions vides) –Le moins de conflit possible (2 clés – même position)
Exemple simple Transformer un code permanent en un entier: –VALB code –4 premiers caractères: A-Z: (1-26) 4 –2 chiffres: 01-31:1-31 –2 chiffres: 01-12, 5-62: 1-24 –2 chiffres: ( ): 0-90 –2 chiffres: 01-99: 1-99 Fonction1(clé) = concatener les codes –Compacte ? 00*** et 27***, **00*** et **32*** non utilisés Fonction2(clé) = code(L1)*…*code(L4)*code(jour)* code(mois)*code(année)*code(derniers_ch) –Conflit? VALB = VALB
Approche générale Valeur non conflictuelle (mais relativement compact) Déterminer la taille approximative du fichier souhaitée (taille) Valeur % primaire –Primaire est un nombre proche de la taille –E.g. < enregistrements: 10007, 10009, 10037, …12007, … –Prévoir plus large –Prévoir un mécanisme pour résoudre le conflit Aller à la suivante Rehashage: appliquer une autre fonction de hashage …
Cas 3: indexe Maintenir une structure d’indexe (clé= lettre + nombre) a b c … z 0pos …… 20 21pos 90 Clé= a21