# Sistemes de fitxers. Accés a fitxers. #### **Introducció** Un **fitxer** (també arxiu) és un conjunt de bits emmagatzemats en un dispositiu d'emmagatzematge com pot ser un disc dur. Per tant, és un dels mecanismes que podem utilitzar per persistir dades. A grans trets podem diferenciar els fitxers en dos grups principals: * **Fitxers de text**: * Contenen sols caràcters de text (llegibles) i es poden obrir amb un editor de text (com el notepad). * Els .txt, .xml, .sql entre altres formats són fitxers de text. * **Fitxers binaris**: * Poden contenir caràcters de text però també altres tipus de caràcters. * Per poder-los manipular o veure el seu contingut sovint necessitarem un programari específic que els pugui interpretar. * Els .pdf, els .jpeg o un simple .docx són formats (entre altres) de fitxers binaris. Arribats a aquest punt cal diferenciar el que és la gestió dels fitxers com objectes del sistema de la gestió del seu contingut. Com veurem tot seguit, java ens proporciona una sèrie de classes per fer totes dues coses. #### **Classes associades a la gestió de fitxers.** El paquet java.io conté les classes necessàries per manejar l'entrada i sortida de fitxers amb java. Una de les primeres classes que estudiarem és la classe File, la qual s'encarregarà d'un seguit d'operacions amb fitxers com són la seva creació (i esborrament), el conèixer les seves propietats, entre altres. Per crear un objecte de la classe File disposem de tres constructors, els quals podem utilitzar indistintament: 1. File (String ruta_directori_i_fitxer). Exemple amb un fitxer a Windows: ``` File f1 = new File ("C:\\Users\\accesadades\\file1.txt"); ``` 2. File (String nom_directori, String nom_fitxer). Exemple amb un fitxer a Windows: ``` String dir_acc_d = "C:/Users/accesadades"; File f1 = new File (dir_acc_d, "file2.txt"); ``` 3. File (File directori, String nom_fitxer). Exemple amb un directori i fitxer windows: ``` File dir1 = new File ("C:\\Users\\accesadades"); File f1 = new File (dir1, "file3.txt"); ``` Fixeu-vos en la diferència entre el segon i el tercer constructor pel que fa al directori, ja que en el primer cas el declarem com un String i en el segon cas com una instància de la classe File. Un cop hem triat el constructor amb el qual procedir, donem una ullada a alguns dels mètodes que es poden fer servir amb els objectes file instanciats. 1. Crear un nou fitxer en un directori: Un cop instanciat l'objecte file fem servir el mètode booleà **createNewFile()**, el qual crearà un fitxer buit sols si no existeix cap altre amb el mateix nom: ``` import java.io.*; public class FileM { public static void main(String[] args) throws IOException { File dir1 = new File ("C:\\Users\\accesadades"); File f1 = new File (dir1, "file3.txt"); f1.createNewFile(); } } ``` Fixeu-vos que el fet d'utilitzar els mètodes de File ens condueix a utilitzar un **throws IOException**. Per què penseu què cal fer-ho? 2. Obtenir un array d'Strings amb el nom de fitxers i directoris associats a l'objecte File: utilitzem **String[] list()**. Aquest mètode no ens llistarà de forma directa el contingut d'un directori, sinó que fica aquesta informació dins un array, el qual caldrà accedir per poder veure les seves ocurrències. En el següent exemple utilitzem aquest mètode per saber el nombre d'elements continguts dins un directori: ``` import java.io.*; public class FileM { public static void main(String[] args) throws IOException { File dir1 = new File ("C:\\Users\\accesadades"); String[] arxius = dir1.list(); System.out.println("nombre de fitxers al directori actual: "+arxius.length); } } ``` 3. Semblant al mètode anterior tenim el File[] listFiles(), el qual retorna un array d'objectes File. Prenent l'anterior exemple, el canvi seria tant simple com fer això: ``` import java.io.*; public class FileM { public static void main(String[] args) throws IOException { File dir1 = new File ("C:\\Users\\accesadades"); File[] arxius = dir1.listFiles(); System.out.println("nombre de fitxers al directori actual: "+arxius.length); } } ``` A més d'aquests mètodes tenim: | Tipus | Mètode | Què fa? | | --- | --------------------------- | ---------------------------------------------------------------------------------------------- | | String | getName() | Donat un objecte file, getName() retorna el seu nom | | String | getPath() getAbsolutePath() | Donat un objecte file, getPath() retorna la ruta relativa i getAbsolutePath() la ruta absoluta | | boolean | isFile() isDirectory() | Donat un objecte file, isFile() ens diu si és un arxiu mentre isDirectory() si és un directori | | boolean | canRead() canWrite() | Donat un objecte file, cadascun d'aquests dos mètodes retornaran True si l'objecte té permisos de lectura o d'escriptura respectivament | | boolean | mkdir() | Semblant a la típica instrucció dels sistemes, mkdir crearà un directori d'acord a l'objecte file instanciat | | long | length() | Retornarà la mida en bytes de l'objecte file (independentment de si és arxiu o directori) | | boolean | exists() | Retornarà True si l'arxiu o directori referenciats a l'objecte file existeixen | | boolean | delete() renameTo(nom_nou_fitxer) | Donada un objecte file existent, delete() l'esborrarà i renameTo el renombrarà d'acord al nou nom que se li passi per paràmetre | | String | getParent() | Si el directori o l'arxiu pengen d'un directori pare, getParent() ens el retornarà o bé retornarà null si no n'hi ha | #### **Exercicis proposats amb File** 1. Creeu un directori qualsevol a la vostra màquina i afegiu almenys 3 fitxers de text i 2 subdirectoris fent ús dels mètodes anteriors. 2. A partir del directori anterior, de quina manera llistarieu els diferents elements que hi ha dins d'aquest. Feu que la sortida del petit codi sigui com aquesta: ``` Al directori: C:\Users\accesadades Hi ha l'arxiu amb nom: file1.txt Al directori: C:\Users\accesadades Hi ha l'arxiu amb nom: file2.txt Al directori: C:\Users\accesadades Hi ha l'arxiu amb nom: file3.txt Al directori: C:\Users\accesadades Hi ha el directori amb nom: subcarpeta ``` 3. Modifiqueu el codi anterior per tal que mostri a més la mida i si té permisos de lectura i escriptura. 4. Afegiu tres fitxers nous al directori anterior amb nom "1234.txt","125567.txt" i "5767.txt". De quina forma podem llistar sols aquells fitxers que comencin per "12". 5. Dins el següent codi observem "String dir = args[0]". Què penseu que fa? De quina manera el podem executar? ``` import java.io.*; public class FileM { public static void main(String[] args) throws IOException { String dir = args[0]; File dir1 = new File(dir); File[] llistaFiles = dir1.listFiles(); } } ``` #### **Streams i fluxos** Java utilitza els streams com a forma de comunicar dades entre un origen i un destí. Aquestes dades poden estar contingudes en un arxiu emmagatzemat en un disc, de tal forma que quan un programa java el necessiti accedir a les seves dades obrirà un stream. I si el programa requereix enviar dades anàlogament obrirà un stream i escriurà la informació en sèrie. Formalment **els streams són abstraccions dels fluxos (entrada i sortida) que java utilitza per comunicar informació entre un origen i un destí**. En altres paraules, java declara una sèrie de classes abstractes per tractar amb aquests fluxos. Així farem una classificació entre dos tipus de fluxos: * Fluxos de bytes (Byte Stream) (8 bits) * S'encarreguen de la lectura i escriptura de dades binàries (baix nivell). * Definirem dues classes abstractes, les quals deriven de la classe Object: * **InputStream** (pels fluxos d'entrada). Subclasses interessants d'aquesta són: * FileInputStream: els utilitzarem per la lectura de fitxers disposant doncs de mètodes com: * int read(): Llegeix un caràcter i el retorna. * int read(byte[] by): llegeix i emmagatzema els bytes en una matriu de bytes fins el final (by.length). * int read(byte[] by, int desplaçament, int n): Llegeix fins a un nombre *n* de bytes començant des de la posició específicada a *desplaçament*. * ObjectInputStream: els utilitzarem per llegir dades serialitzades (ho veurem més endavant). * **OutputStream** (pels fluxos de sortida). Subclasses interessants d'aquesta són: * FileOutputStream: els utilitzarem per l'escriptura de fitxers disposant doncs de mètodes com: * void write(int by): escriu un byte. * void write(byte[] by): escriu un array de bytes de mida by.length . * void write(byte[] by, int desplaçament, int n): escriu un array de *n* bytes començant des de la posició específicada a *desplaçament*. * ObjectOutputStream: els utilitzarem per serialitzar dades (ho veurem més endavant). Per poder utilitzar tant FileInputStream com FileOutputStream cal haver instanciat un objecte de la classe File prèviament. A l'apartat de [fitxers binaris](#Fitxers-binaris) podreu veure alguns casos d'aplicació. També en el moment d'abordar la serialització veurem que aquestes classes juguen un paper important. Exemple d'ús dels streams: **Volem fer una còpia d'un fitxer anomenat file1.txt cap un altre anomenat file1_old.txt.**. Com es pot fer utilitzant streams? Per fer la lectura del file1.txt emprarem read(), el qual farà la lectura byte a byte, mentre que per escriure cada byte llegit en el flux de sortida utilitzarem write(). El procés finalitza quan read() retorna -1, és a dir, quan arribem al final del fitxer. NOTA prèvia: cal que creeu el file1.txt al directori que esteu referenciant dins el programa abans de provar el codi (en el cas de l'exemple és C:/users/accesadades). ``` import java.io.*; public class InOutStr { public static void main(String [ ] args) { FileInputStream origen = null; FileOutputStream desti = null; File f1 = new File("C:\\Users\\accesadades\\file1.txt"); File f2 = new File("C:\\Users\\accesadades\\file1_old.txt"); try { origen = new FileInputStream(f1); desti = new FileOutputStream(f2); int c; while ((c = origen.read( )) != -1) { destino.write((byte)c); } } catch (IOException er) { System.out.println("Excepció de fluxos " + er.getMessage( )); } finally { try { origen.close( ); desti.close( ); } catch (IOException er) { er.printStackTrace( ); } } } } ``` * Fluxos de caràcters (16 bits): * S'encarreguen de la lectura i escriptura de caràcters unicode (major complexitat que bit a bit). * Les dues classes protagonistes d'aquests fluxos són Reader i Writer. * En els casos en els quals cal combinar fluxos de caràcters amb fluxos binaris es disposa de les classes InputStreamReader (per transformar un InputStream en un Reader) i OutputStreamWriter (per transformar un OutputStream en un Writer). * Les classes de fluxos de caràcters més importants són: * Lectura i escriptura de caràcters en fitxers: **FileReader** i **FileWriter**. Tingueu en compte que: * Es pot generar una excepció FileNotFoundException si el nom del fitxer no és vàlid o no existeix. * Es pot generar una excepció IOException si hi ha error de disc o no disposem de permisos. * Mètodes que proporciona la classe FileReader i que retornen el nombre de caràcters llegits (o -1 al final del fitxer): * int read(): Llegeix un caràcter i el retorna. * int read(char[] buf): llegeix i emmagatzema els caràcters en un buf (matriu de caràcters) fins el final. * int read(char[] buf, int desplaçament, int n): Llegeix fins a un nombre *n* de caràcters començant des de la posició específicada a *desplaçament*. * Mètodes que proporciona la classe FileWriter: * void write(int c): escriu un caràcter. * void write(char[] buf): escriu un array de caràcters. * void write(char[] buf, int desplaçament, int n): escriu un array de *n* caràcters començant des de la posició específicada a *desplaçament*. * void write(String str): escriu una cadena de caràcters. * void append (char ch): afegeix un caràcter a un fitxer. * Per buferitzar dades, és a dir, fer ús d'un buffer intermig entre la memòria i l'stream, **BufferedReader** i **BufferedWriter**. **Exemple 1: Ús de FileReader** Necessitarem ubicar un fitxer qualsevol dins un directori accessible pel programa. En aquest cas és TextFile dins el directori accesadades: ``` import java.io.*; public class LlegirFitxerText { public static void main ( String [] args) throws IOException { // 1. declaració fitxer amb File File fitxer = new File ("C:\\Users\\accesadades\\TextFile.txt"); //2. Creem flux d'entrada cap al fitxer FileReader flux = new FileReader (fitxer); //3. Recorrem amb while caràcter a caràcter fins al final int i; while ((i=flux.read())!=-1) // Fem un cast a char del caràcter llegit com enter System.out.println ((char) i); flux.close(); } } ``` El resultat d'aquest output és aquest, mostrant cada caràcter llegit a cada línia (el mostrem de forma abreujada): ``` E l v e l o z m u r c ``` **Exemple 2: Ús de FileWriter** En aquest cas volem crear un fitxer i afegir-li contingut. L'escriptura és caràcter a caràcter i s'afegeix un '*' després de cada caràcter: ``` import java.io.*; public class EscriureFitxerText { public static void main (String [] args) throws IOException { // 1. declaració fitxer amb File File fichero = new File("C:\\Users\\accesadades\\FitxerText2.txt"); //2. Creem flux de sortida cap al fitxer FileWriter fic = new FileWriter (fichero); //3. Fiquem la cadena a escriure String cadena = "Estem fent proves amb el FileWriter"; //4. El passem a una matriu de caràcters char [] cad = cadena.toCharArray () ; //5. Procedim amb la seva escriptura caràcter a caràcter for ( int i=0; i < cad.length ; i++) { fic.write (cad[i]); //Afegim un * darrera de cada caràcter fic.append ('*'); } //6. Tanquem fitxer fic.close (); } ``` La sortida que produeix aquest programa és: ``` E*s*t*e*m* *f*e*n*t* *p*r*o*v*e*s* *a*m*b* *e*l* *F*i*l*e*W*r*i*t*e*r* ``` #### **Exercicis proposats amb FileReader i FileWriter** 1. Modifica l'exemple anterior per, en comptes d'escriure els caràcters un a un, escrigui tot l'array fent ús del mètode write (char [] buf). *NOTA: si volem afegir caràcters al final d'un fitxer de text podem fer servir el següent constructor de FileWriter → FileWriter fic = new FileWriter (fichero , true).* 2. Crea el següent array d'Strings i inserta en el fitxer cada String un a un fent ús del mètode write (String str) String prov[] = {"Albacete", "Avila", "Badajoz", "Caceres", "Huelva", "Jaen", "Madrid", "Segovia", "Soria", "Toledo", "Valladolid", "Zamora"} #### **Buffers: BufferedReader/BufferedWriter** La motivació de tractar el contingut dels fitxers amb buffers està en el fet de poder fer tractaments per línies en comptes de caràcter a caràcter. Així, en el cas de FileReader trobem que no disposa de cap mètode que permeti llegir línies completes. BufferedReader disposa del mètode readLine(), el qual llegeix una línia del fitxer i la retorna (o bé retorna null si no hi ha res a llegir o està buit). Per poder fer ús de BufferedReader necessitem haver instanciat un objecte de FileReader: ``` import java.io.*; public class LlegirFitxerTextBuf { public static void main (String [] args) { //fixeu-vos que no passem un objecte File, fem ús d'un altre constructor de FileReader. try { BufferedReader fichero = new BufferedReader (new FileReader (“LeerFicheroTextoBuf.txt”)); String linea; //Llegim de línia a línia while ((linea = fichero.readLine()) != null){ System.out.println (linea); } fitxer.close(); } catch (FileNotFoundException fn) { System.out.println (“No es troba el fitxer”); } catch (IOException io) { System.out.println (“Error d'E/S”); } } } ``` D'altra banda BufferedWriter aporta funcionalitats d'escriptura a FileWriter. El seu mètode newLine() inserta un salt de línia. Per utilitzar-lo caldrà instanciar abans un objecte FileWriter. ``` import java.io.*; public class EscriureFitxerTextBuf { public static void main (String [] args) { //fixeu-vos que no passem un objecte File, fem ús d'un altre constructor de FileWriter. try { BufferedWriter fichero = new BufferedWriter (new FileWriter (“EscriureFicheroTextoBuf.txt”)); for (int i=1; i<12; i++ ) { fitxer.write("Fila num: " + i); fitxer.newLine(); } fitxer.close(); } catch (FileNotFoundException fn) { System.out.println (“No es troba el fitxer”); } catch (IOException io) { System.out.println (“Error d'E/S”); } } } ``` En resum: ![imatge](https://hackmd.io/_uploads/SkcPMUdR0.png) #### **Fitxers binaris** Com ja havíem comentat amb anterioritat, els fitxers binaris són aquells que emmagatzemen seqüències de dígits binaris, sovint no llegibles directament per qualsevol usuari (com si succeeix amb els fitxers de text). Havíem vist que les classes que permeten treballar amb aquests fitxers són la FileInputStream i FileOutputStream, les quals creen un enllaç entre el flux de bytes i el fitxer. Ara bé, per poder llegir i escriure dades de tipus primitius farem ús de les classes DataInputStream i DataOutputStream, les quals incorporen variants dels mètodes read() i write() com: | DataInputStream Mètodes de lectura | DataOutputStream Mètodes d'escriptura | | -------- | -------- | | boolean readBoolean(); | void writeBoolean(boolean v); | | byte readByte(); | void writeByte(int v); | | int readUnsignedByte(); | void writeBytes(String s); | int readUnsignedShort(); | void writeShort(int v); | short readShort(); | void writeChars(String s); | char readChar(); | void writeChar(int v); | int readInt(); | void writeInt (int v); | float readFloat(); | void writeFloat(float v); | String readUTF(); | void writeUTF(String str); | En el següent exemple utilitzarem el FileOutputStream i el DataOutputStream, a més dels mètodes que siguin adequats per escriure el següent array d'enters: {128,250,430,520,820} ``` import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; public class BinaryArray { static String f1 = "C:\\Users\\accesadades\\BinaryArray.dat" ; public static void main(String[] args) { int[] ints = {128,250,430,520,820}; //Cridem al mètode que escriu l'array d'enters writeStream(ints); //Cridem al mètode que llegeix l'array d'enters readStream(); } public static void writeStream(int[] ints) { try { FileOutputStream fileStr1 = new FileOutputStream(f1); DataOutputStream str1 = new DataOutputStream(fileStr1); for (int j:ints) { str1.writeInt(j); } str1.close(); fileStr1.close(); } catch (FileNotFoundException e) { System.out.println("EL FITXER NO EXISTEIX"); } catch (IOException e) { e.printStackTrace(); } } private static void readStream() { try { FileInputStream fileStr1_inp = new FileInputStream(f1); DataInputStream str2 = new DataInputStream(fileStr1_inp); int i = 0; while (str2.available()>0) { int k = str2.readInt(); System.out.println("el nombre contingut a la posició ["+ i + "] de l'array és: " +k); i++; } str2.close(); fileStr1_inp.close(); } catch (FileNotFoundException e) { System.out.println("EL FITXER NO EXISTEIX"); } catch (IOException ex) { ex.printStackTrace(); } } } ``` Quina diferència creieu que hi ha entre utilitzar read() i readInt()? **Exercici** Escriu un programa que insereixi dades a un arxiu anomenat “FitxerDades.dat”. Les dades les prendrà de dos arrays definits al propi programa. Un contindrà els noms d'una sèrie de persones i l'altre les seves edats. Recorrent els arrays fareu escriptura de cadascun d'aquests al fitxer (en ordre, primer el de noms i després el d'edats). Després feu la lectura d'aquest fitxer mostrant per pantalla una informació semblant a aquesta: Nom: Alba, edat: 20 anys Nom: Matusalem, edat: 199 anys ... NOTA: si volem afegir bytes al final del fitxer (FitxerDades.dat) es pot fer servir el següent constructor: ***FileOutputStream fileout = new FileOutputStream (fitxer, true)***