Appunti java pag. 133 10. File e loro attributi In generale, in qualsiasi sistema operativo, il file system consente di operare su archivi di dati (data files) che possono essere organizzati e memorizzati su supporto magnetico in due modi fondamentali: 1. Come sequenze di record di lunghezza variabile individuate da particolari marcatori di fine record; 2. Come sequenze di record di lunghezza fissa senza marcatori; Il file di testo è il “più duttile” nel senso che può assumere entrambe le vesti indicate in precedenza. Assume la prima configurazione, ovvero è un file di record (il record è una linea di testo) di lunghezza variabile terminata da uno o più marcatori di fine linea End Of Line (EOL). La seconda configurazione è sempre “disponibile” e consiste nel pensare il file di testo come file con record di lunghezza fissa (il record è un carattere pari a uno o due byte). In questo caso si deve pensare al marcatore di fine linea come a un particolare carattere che non ha “immagine” ma che “stampato” genera un ritorno carrello spostando il cursore all’inizio della linea successiva del dispositivo di stampa (video o stampante). In informatica quando si parla di file di testo ci si riferisce alla prima interpretazione avvero a quella di file con record pari a una linea di testo di lunghezza variabile. Il file deve, di conseguenza, essere generato o letto in sequenza dal primo all’ultimo record e può subire aggiornamenti solo nella forma di “addizione in coda” (append) di nuove righe. Si può concludere, quindi, che un file di record di lunghezza variabile consente la sola metodologia di ACCESSO SEQUENZIALE. Il secondo tipo di file che opera su record di dati di lunghezza fissa predefinita, oltre all’ACCESSO SEQUENZIALE consente un ACCESSO INDICIZZATO che permette “salti” al record di posizione relativa scelta dall’utente. La lunghezza fissa di ogni record consente anche la SOVRASCRITTURA di un record intermedio oltre alla “addizione in coda” di nuovi record specifica dei file sequenziali. Un file è quindi una struttura dati “permanente” (mentre il vettore è “volatile” perché memorizzato in memoria di lavoro) costituita da una sequenza di RECORD virtualmente illimitata come mostrato in figura: ATT INIZ ...... EOF Ogni file viene “scandito” (letto, sfogliato) leggendo un record e portandolo in memoria di lavoro. A tale fine deve quindi essere dotato di un puntatore INIZiale che indica dove si trova la sua TESTA e di un puntatore ATTuale che indica la posizione della successiva lettura. Infine deve possedere un particolare MARCATORE che indica la sua fine EOF=EndOfFile. Se il puntatore ATT è posizionato sul marcatore EOF la lettura è terminata e non è più possibile avanzare. Appunti java pag. 134 Le due tipologie di file precedentemente descritte hanno, nei linguaggi imperativi tradizionali (es. Pascal), definizioni e metodologie di accesso distinte che fanno riferimento alla loro struttura. La struttura fisica di memorizzazione sui dispositivi per i file di tipo TEXT può essere diversa a seconda del sistema operativo e/o del linguaggio che li genera. In particolare il marcatore di fine linea può essere costituito da due marcatori di fine riga (in Pascal/Windows ad esempio CARRIAGE RETURN CR=13, LINE FEED LF=10) o da un solo marcatore (New Line ‘\n’ in c/UNIX). La struttura fisica di memorizzazione dei FILE di RECORD è di norma costituita da registrazioni binarie di lunghezza fissa senza marcatori con l’esclusione dell’EOF. 10.1. File in linguaggio Java Il linguaggio Java, con l’uso degli stream e dei filtri, visti nel capitolo precedente, tenta di superare questa classica suddivisione tra le due tipologie di file. Si è visto che un flusso FileReader o InputStreamReader consente di accedere in input ai singoli caratteri con il metodo read(). Viceversa introducendo il filtro BufferedReader si può accedere ad una intera linea con il metodo readLine(). Per non dipendere dal sistema operativo o dal linguaggio che ha creato il file di testo java interpreta come EndOfLine corretto sia la presenza di uno dei marcatori CR=13 (‘\r’) oppure LF=10 (‘\n’) oppure la presenza contemporanea di entrambi i caratteri ‘\r’+’\n’. I file con record di lunghezza fissa sono invece memorizzati come byte binari dipendenti dalla dimensione del tipo di dato elementare che si intende registrare. Questi non hanno in Java un particolare trattamento separato dai file di Testo infatti la classe con nome “improprio” Random Access File consente di trattare entrambi i tipi di file. 10.2. I metodi che operano sui file In sostanza i file di tipo TEXT possono subire due trattamenti, per linee o per singolo carattere. Nel primo caso i record sono righe nel secondo li si tratta come file con record di lunghezza fissa di uno o due byte. La diversità di struttura fisica si traduce solo nella disponibilità di due modalità di accesso scelte dall’utente. Se un file di testo lo si “vede” come file di linee vi si accede solo con operazioni sequenziali di lettura o solo con l’addizione di record in coda in scrittura, se lo si vede invece come file di caratteri sono consentite le operazioni di SALTO al record (carattere) di posizione desiderata, sia per la scrittura (distruttiva) che per la lettura di un record. I record di un file (con record costante) hanno quindi una posizione (INDICE) relativa ben definita che parte dal 1° record, che ha posizione relativa o indice 0, fino all’n-esimo con posizione N-1. Struttura fisico-logica di un FILE di RECORD di lunghezza fissa: 0 1 ........... N-1 Indice di Posizione relativa I metodi che caratterizzano una classe che opera sui file si possono quindi raggruppare logicamente in quattro categorie: Appunti java ♦ ♦ ♦ ♦ pag. 135 Costruttori Metodi di accesso agli attributi del file Metodi di lettura Metodi di scrittura. 10.3. La classe RandomAccessFile di Java La tabella seguente elenca i metodi della classe RandomAccessFile. RandomAccessFile Costruttori RandomAccessFile(String, String) NOTE La prima stringa è un nome di file fisico, la seconda indica il modo con cui si accede. Il modo può essere di lettura modo read “r” o in lettura e scrittura modo “rw”. In modalità “rw” se non esiste lo crea, se esiste si predispone per le operazioni di lettura o scrittura ponedo il puntatore all’inizio. Invia una IOException se in modalità “r” non esiste il file o si tratta di una directory. Metodi long length() void setLength(long) long getFilePointer() void seek(long) Metodi di lettura Metodi di Scrittura void close() int readInt() long readLong() double readDouble() float byte boolean char String readLine() Attenzione: in scrittura è a carico del programmatore aggiungere un carattere di fine linea ‘\r’ o ‘\n’ o entrambi. void writeInt(int) void writeLong(long) void writeDouble(double) void writeFloat(float) void writeByte(int) void writeBoolean(boolean) void writeChar(int) void writeBytes(String) void writeChars(String) Restituisce il numero di byte del file. Setta la lunghezza del file. Se il file e più corto lo amplia, se più lungo lo tronca. In caso di riscrittura su un file esistente è necessario utilizzare questo metodo al termine della scrittura per inserire l’eof. Nella forma setLength( getFilePointer()). Restituisce la posizione in byte del puntatore attuale Porta il puntatore sul byte di posizione indicata Chiude il file e libera la memoria heap Legge un dato e trasla il puntatore di una quantità pari al numero di byte binari occupati dal tipo di dato. La lettura produce un EOFException quando incontra il termine del File Legge tutti i caratteri fino al marcatore di fine linea ‘\r’ o ‘\n’. Se il puntatore è a EOF la stringa acquisita è null. In ogni caso si produce un EOFException Scrive il dato indicato e trasla il puntatore. Scrive tutti i byte di una stringa in formato dipendente dalla macchina. Scrive tutti i byte di una stringa in formato Unicode. Appunti java pag. 136 ♦ Lettura di un file di testo Siccome anche un file sequenziale di testo può essere trattato con i metodi della classe Random Access File si affronterà per primo un esercizio di lettura e stampa di un file di testo. esempio 1. “Assegnato un file di testo memorizzato su disco il cui nome fisico sia <C:\mio.txt> leggerlo e stamparlo sul monitor.” Richiesta: Ricordando che un file di testo è organizzato in linee che terminano con un marcatore di fine linea, utilizzare i metodi opportuni di RAF per acquisire e stampare una linea completa per volta fino a fine file. Codifica: import java.io.*; public class cap10_es_01 { public static void main(String args[])throws IOException { RandomAccessFile in=new RandomAccessFile("c:/mio.txt","r"); System.out.println("\nfile di "+in.length()+" byte contiene:"); String dat; while ((dat=in.readLine())!=null) System.out.println(dat); System.out.println("\nFine esecuzione."); } } // (1) // (2) // (3) Esecuzione: Il file stampato è costituito da cinque linee come mostra l’output della finestra. Commento al codice: La nota (1) mostra il costruttore del file random che assume il nome logico in. La nota (2) evidenzia l’utilizzo del metodo length() per stampare la lunghezza in byte del file, la (3) il metodo che acquisisce una linea del file e testa il raggiungimento della fine del file. Se si contano i caratteri stampati si nota che le 5 righe contengono 70 caratteri e di conseguenza gli 80 byte indicati nell’autput sono inputabili alla coppia di marcatori di EOL \r \n presenti su ogni riga. ♦ Creazione di un file di testo esempio 2. “realizzare un programma che generi in un ciclo 100 numeri da 1000 a 1099 e dopo averli trasformati in stringa li scriva su una singola riga del file.” Richieste: Oltre a generare il file richiesto il main() deve anche invocare un metodo di stampa() separato per verificare sul monitor se il programma ha svolto quanto richiesto. Appunti java pag. 137 Codifica: import java.io.*; public class cap10_es_02 { public static void Stampa(String n_fil)throws IOException { RandomAccessFile in=new RandomAccessFile(n_fil,"r"); // (4) System.out.print("\nil file lungo "+in.length()+" byte"); // (5) System.out.println(" contiene:\n"); String dat; while ((dat=in.readLine())!=null) // (6) System.out.println(dat); } public static void main(String args[])throws IOException { String n_fil="c:/nuovo.dat"; RandomAccessFile out=new RandomAccessFile(n_fil,"rw"); // (1) for (int i=0; i<100; i++) out.writeBytes(""+(i+1000)+'\n'); // (2) out.setLength(out.getFilePointer()); // (3) out.close(); // (4) Stampa(n_fil); // stampa del file generato System.out.println("\nfine esecuzione.\n"); } } Esecuzione: La finestra di output mostra che il file contiene su ogni riga i numeri desiderati trasformati in testo e che la lunghezza totale del file è 500 byte. Infatti una riga contiene 5 caratteri; quattro numerici più il marcatore di fine linea ‘\r’. Commento al codice: La nota (1) indica il costruttore del file in modalità scrittura “rw”. La nota (2) indica il ciclo di costruzione delle righe che vengono scritte nel file. In particolare il metodo writeBytes(String) ha come parametro una String e quindi il doppio apice “”+(i+1000)+’\r’ che precede il numero generato nel ciclo è indispensabile per trasformare immediatamente il numero in un stringa. Il carattere ‘\r’ è il marcatore fine linea inserito nella stringa. In alternativa si poteva scrive un ‘\n’ oppure una sequenza ‘\r’+’\n’. La nota (3) indica la modalità con cui si deve inserire il Fine File EOF usando il metodo setLength( long) dove il parametro numerico è la lunghezza in byte del file che si intende marcare con EOF. Il close() (4) successivo non è indispensabile ha la sola funzione di liberare lo heap dagli oggetti File costruiti. Le note (5) e (6) indicano la necessità di usare nuovamente il costruttore in modalità “r” per leggere il file se prima è stato chiuso con close(). Appunti java pag. 138 ♦ Appendere righe a un file di testo esempio 3. “realizzare un programma che aggiunga al precedente file di testo altri 10 numeri da 1099 a 1108 e li scriva in coda al file esistente.” Richieste: Oltre ad appendere i dati al file il main() deve anche invocare un metodo di stampa() separato per verificare sul monitor se il programma ha svolto quanto richiesto. Codifica: import java.io.*; public class cap10_es_03 { public static void Stampa(String n_fil)throws IOException { RandomAccessFile in=new RandomAccessFile(n_fil,"r"); System.out.print("\nil file lungo "+in.length()+" byte"); System.out.println(" contiene:\n"); String dat; while ((dat=in.readLine())!=null) System.out.println(dat); } public static void main(String args[])throws IOException { String n_fil="c:/nuovo.dat"; RandomAccessFile out=new RandomAccessFile(n_fil,"rw"); Out.seek(out.length()); // posizionamento in coda del puntatore for (int i=1100; i<1110; i++) out.writeBytes(""+i+'\n'); out.setLength(out.getFilePointer()); out.close(); Stampa(n_fil); // stampa del file generato System.out.println("\nfine esecuzione.\n"); } } Esecuzione: l’output dovrebbe mostrare anche le nuove righe appese al procedente file. Commenti: La nota su out.seek(out.length()); mostra come posizionare il puntatore attuale a fine file per eseguire l’append dei nuovi dati. ♦ Generare un file di dati Per rendere evidente la diversità tra un file ti testo e uno di dati proponiamo il seguente: esempio 4. “realizzare un programma che generi in un ciclo 5 numeri da 10000 a 10004 e li scriva su un file di dati.” Richieste: Oltre a generare il file richiesto il main() deve anche stamparlo per verificare se il programma ha svolto quanto richiesto. Appunti java pag. 139 Codifica: import java.io.*; public class cap10_es_04 { public static void main(String args[])throws IOException { RandomAccessFile out=new RandomAccessFile("nuovo.dat","rw"); for (int i=0; i<5; i++) out.writeInt(i+10000); // (1) out.setLength(out.getFilePointer()); /* verifica del file costruito */ System.out.print("\nil file è di :"+out.length()+" byte"); System.out.println("e contiene:\n"); out.seek(0); int dat=0; // (2) try { while (true) { // (3) dat=out.readInt(); // (4) System.out.print(dat+" "); } } catch (EOFException e) {System.out.println("\nFINE FILE\n"); } System.out.println("Esecuzione terminata."); } } Esecuzione: La finestra di output mostra che il file contiene i cinque numeri richiesti ma a differenza del precedente programma non sono stampati su righe diverse. La lunghezza totale del file è 20 byte, infatti un record e costituito da un numero che pur essendo di 5 cifre non è memorizzato in modalità carattere ma in modalità binaria e quindi occupa lo spazio di un int che in Java è pari a 4 byte. Evidentemente non è più presente il marcatore di fine linea ‘\r’. Commento al codice: Nella prima parte nulla è cambiato rispetto all’esempio precedente solo la nota (1) indica il nuovo metodo usato per scrivere un record intero writeInt(). Rispetto al precedente esempio non si è operata la chiusura del file con il metodo close(), per evidenziare che in questo caso non è più necessario riusare in costruttore ma è sufficiente posizionare il puntatore all’inizio del file con il metodo seek(0) della nota (2). Le righe con note (3) e (4) e successive mostrano il metodo che si può usare per leggere un file di record. Il blocco try{ } catch (..) { } indica che il ciclo apparentemente infinito while (true) viene interrotto quando si raggiunge il fine file e il metodo readInt() genera un’eccezione che rimanda all’esecuzione del blocco cath. Si nota infatti che nella finestra di output al termine del file è stampato in messaggio di <FINE FILE> generato dall’eccezione EOFException. Appunti java pag. 140 ♦ Generare un file di dati con più campi Per rendere evidente come si opera su un file di dati formato da diversi campi si propone il seguente: esempio 5. “realizzare un programma che legga da tastiera una sequenza di nomi di città seguite dal numero intero che rappresenta la popolazione e scriva in un opportuno file di dati.” Richieste: Oltre a generare il file richiesto il main() deve anche invocare una procedura stampa() per verificare se il programma ha svolto quanto richiesto. Il record da memorizzare è costituito da una Stringa e da un numero intero e si decide di dedicare un massimo di 20 caratteri alla descrizione della città e in intero per la popolazione (in java int è di 4 byte, più che sufficiente a contenere la popolazione). Siccome si devono acquisire i dati da tastiera, si può organizzare il programma filtrando i dati con uno StreamTokenizer per separare con facilità le stringhe dai numeri e quindi comporre il record di 24 byte da scrivere nel file di output. Lo schema dei flussi e filtri potrebbe essere così rappresentato: Flusso di Record Programma. RAF (cons. di Token) Flusso di Char Flusso di Token ISR ST File rappresentazione dei flussi del programma di I/O richiesto dal problema. File Flusso di Record RAF Prog. Stampa. (cons. di Record) I flussi del metodo stampa(). Codifica: import java.io.*; public class cap10_es_05 { public static void stampa(String sfil) throws IOException{ RandomAccessFile out=new RandomAccessFile(sfil,"r"); // (20) System.out.print("\nil file contiene:\n"); out.seek(0); // (21) String cit;int dat=0; byte[] b=new byte[20]; // (22) try { while (true) { out.read(b,0,20); // (23) cit=new String(b,0,20); // (24) dat=out.readInt(); // (25) System.out.println(cit+" "+dat); } } catch (EOFException e) {System.out.println("\nFINE FILE\n"); } } Appunti java public static void main(String args[])throws IOException { String sfil_out="Citta.dat"; InputStreamReader in=new InputStreamReader(System.in); StreamTokenizer st=new StreamTokenizer(in); RandomAccessFile out=new RandomAccessFile(sfil_out,"rw"); out.seek(0); // (4) String s="";int pop=0; st.nextToken(); while ((st.ttype!=st.TT_EOF)) { if(st.ttype==st.TT_WORD){ s=st.sval; // (5) s=s+" "; // (6) s=s.substring(0,20); // (7) out.writeBytes(s); // (8) } else if (st.ttype==st.TT_NUMBER) { pop=(int)st.nval; // (9) out.writeInt(pop); // (10) } st.nextToken(); } out.setLength(out.getFilePointer()); // (11) out.close(); stampa(sfil_out); // (12) System.out.println("Esecuzione terminata."); } } pag. 141 // (1) // (2) // (3) Commento al codice: Le note da (1) a (3) mostrano i costruttori dei flussi indicati nello schema grafico. La (4) il posizionamento in testa al file (non indispensabile perché è appena stato aperto). Il ciclo separa i token e viene interrotto da un EOF immesso da tastiera (si deve premere Ctrl-z su una riga vuota). Le note da (5) a (8) mostrano come si acquisisce la stringa e la si satura di spazi bianchi dimensionandola a 20 caratteri e quindi la si scrive nel file di output. Le note (9) e (10) le analoghe operazioni per scrivere il campo popolazione nel file. Le (11) e (12) sono l’operazione di chiusura del file e quella di invocazione della stampa() di verifica. Le note da (20) a (25) mostrano come si acquisisce un record e lo si mostra. ♦ Sovrascrivere in un file di dati esempio 6. “realizzare un programma che legga da tastiera il nome di una città, presente nel file del precedente esempio 5, la faccia seguire dal numero intero che rappresenta la nuova popolazione, diversa da quella memorizzata precedentemente, quindi sovrascriva il record da aggiornare nel file di dati.” Richieste: Il main() deve anche invocare i metodi: leggi_cit() che acquisisce la stringa corretta lunga 20 caratteri, leggi_int() per acquisire la popolazione, ricerca() che restituisce la posizione del record da aggiornare, quindi il main sovrascrive il record stesso. Infine stampa() verificherà se il programma ha svolto quanto richiesto. Scomposizione: Appunti java pag. 142 Leggi int() main() Leggi cit() ricerca() stampa() Codifica: import java.io.*; public class cap10_es_06 { static void stampa(String sfil) throws IOException{ RandomAccessFile out=new RandomAccessFile(sfil,"r"); // (20) System.out.print("\nil file contiene:\n"); out.seek(0); // (21) String cit;int dat=0; byte[] b=new byte[20]; // (22) try { while (true) { out.read(b,0,20); // (23) cit=new String(b,0,20); // (24) dat=out.readInt(); // (25) System.out.println(cit+" "+dat); } } catch (EOFException e) {System.out.println("\nFINE FILE\n"); } } static String Leggi_cit() {return "Bologna static int Leggi_int() {return 1111; ";}//lettura simulata } // lettura simulata static long Ricerca(String cit, RandomAccessFile io) throws IOException{ String cit_f; byte[] b=new byte[20]; io.seek(0); io.read(b,0,20); // lettura 1° record cit_f=new String(b,0,20); // lettura 1° record io.seek(24); // punt sul 2° record while (!cit.equals(cit_f)) { io.read(b,0,20); // lettura record succ cit_f=new String(b,0,20); io.seek(io.getFilePointer()+4); // punt sul record succ } return (io.getFilePointer()-24); } public static void main(String args[])throws IOException { String sfil="Citta.dat"; RandomAccessFile io=new RandomAccessFile(sfil,"rw"); String cit=Leggi_cit(); int pop=Leggi_int(); long pos=Ricerca(cit, io); io.seek(pos); // (1) io.writeBytes(cit); // (2) io.writeInt(pop); // (3) io.close(); stampa(sfil); System.out.println("Esecuzione terminata."); } } Commento al codice: Le note da (1) a (3) mostrano le operazione di sovrascrittura del record trovato.