Molto spesso mi trovo a dover spiegare a qualcuno come si gestisce l’I/O (Input/Output) su file con Java. Così, ho deciso di scrivere questo breve articolo, che introduce la gestione dei file con tale linguaggio.
I punti che intendo trattare con questo articolo sono i seguenti:
Non intendo trattare in modo troppo minuzioso la parte di gestione del File System, che verrà approfondita in un altro articolo. In questo articolo, quindi, non verranno spiegati i concetti di base sui file; nomi dei file, differenze tra file e directory, utilizzo del separatore per i nomi, file temporanei e metodi per il loro trattamento saranno trattati più avanti con un articolo a loro dedicato.
Tipi di file
Veniamo, quindi, al primo punto: i tipi di file. Prima di cominciare a costruire qualcosa in merito alla gestione dell’I/O su file in Java bisogna avere ben presente il tipo di file su cui si andrà a lavorare. Sembra banale, ma non è così: molto spesso capita che, a lavoro iniziato, ci si accorga di aver preso la strada sbagliata, di aver scelto il tipo di file sbagliato per la gestione che ci interessa. E non sempre è così semplice tornare indietro e convertire il tutto.
Il core Java mette a disposizione del programmatore un’ampia scelta di strumenti per la gestione di diversi tipi di file. Questo significa che la libreria standard di Java contiene già molte classi preconfezionate per agevolare il lavoro del programmatore. Il package di riferimento per queste classi è java.io. Ma vediamo qual è la classificazione più comune per i tipi di file:
I file di tipo binario (raw) includono bene o male tutti i tipi di file: qualunque tipo di file può essere classificato come tale. In questo contesto, però, si classifica come file binario un qualsiasi file che non faccia parte delle altre 3 categorie. Rientrano in questa classificazione, quindi, i file eseguibili, le immagini, i file di dati crudi, ecc. Per questo tipo di file, la gestione I/O di Java mette a disposizione delle classi per l’accesso byte per byte.
I file di testo sono i più comuni: sono file che contengono un limitato set di caratteri e sono, di norma, leggibili all’occhio umano (human-readable). Questi file possono essere organizzati per righe di testo, oppure semplicemente tramite dei marcatori (il punto e virgola, il trattino, ecc.). Per questa categoria di file esistono diverse classi nel core standard di Java, visto che sono anche i più comuni file utilizzati.
I file ad accesso casuale sono generalmente dei file binari, organizzati secondo una struttura di tipo record. In pratica sono delle lunghe sequenze di byte, che si possono “spezzettare” in sottosequenze di lunghezza fissa. Ognuna di queste sottosequenze rappresenta un record del file ed è organizzata secondo una struttura ben precisa. La caratteristica più interessante di questo tipo di file è che, nonostante sia un file binario, ciascun record è indirizzabile direttamente. Questo significa che per raggiungere un determinato record non è necessario leggere tutto il file fino al record prescelto, ma si può saltare direttamente al record desiderato.
Lettura dei file
E’ tempo, ora, di provare a lavorare con i file. Cominciamo, quindi, con la lettura di un semplice file di testo, che presumiamo esistere già. Il file che andremo a leggere si chiamerà “pippo.txt”, sarà collocato nella directory home del disco in cui andremo ad eseguire l’applicazione (C:\home\ per utenti Windows, /home/ per utenti Unix/Linux) e avrà il seguente contenuto:
Prima riga del file di testoSeconda riga del file di testo
Vediamo subito il codice necessario a leggere il seguente file: faremo un output a video del contenuto:
import java.io.*;
public class LeggiFile {
public static void main(String[] args) {
String linea = "";
try {
BufferedReader br = new BufferedReader(
new FileReader("/home/pippo.txt")
);
while((linea = br.readLine()) != null) {
System.out.println( linea );
}
br.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Analizzando l’esempio sopra riportato possiamo notare alcune cose interessanti: la sezione import include tutta la collezione di classi presenti nel package java.io, che rappresenta il package di base per le operazioni di I/O. Le classi di questo package che vengono utilizzate dal programma d’esempio sono 2: BufferedReader, che rappresenta un’astrazione per la lettura bufferizzata da un supporto qualsiasi e FileReader, che in serve ad indicare che il dispositivo da cui intendiamo leggere è un file aperto in lettura. La lettura avviene attraverso il metodo readLine(), che permette, come dice il nome, di leggere un’intera riga di testo. Tale metodo fa avanzare automaticamente il puntatore al file alla prossima riga da leggere e ritorna una stringa (la stringa letta) oppure null nel caso lo stream di input (il file, nel nostro caso) sia terminato.
Ovviamente, per ciascuno dei tipi di file presentati nel paragrafo precedente esiste l’apposita classe; per i file binari si usa semplicemente la classe FileInputStream, che fornisce dei metodi grezzi per il trattamento dei dati, consentendo la lettura di un singolo byte per volta, oppure di una porzione del file, che verrà restituita sottoforma di array di byte; per i file di testo, come si è già visto, si utilizza la classe BufferedReader, costruita sempre a partire da un FileReader (che è una implementazione standardizzata di un InputStreamReader, che fa da wrapper ad un FileInputStream); per il trattamento dei file Random, c’è l’apposita classe RandomAccessFile, che consente, attraverso dei meccanismi preposti, di scorrere tutti i record del file e mette a disposizione una serie di metodi per il posizionamento (seek); infine, per i file in Java Byte Code, viene utilizzata la classe ObjectInputStream, che permette la lettura degli oggetti serializzati su file (la serializzazione è argomento trattato in un prossimo articolo).
Scrittura dei file
Veniamo, quindi, a parlare della scrittura su file. Anche qui, ci serviamo di un semplice esempio che permette la scrittura di banali stringhe di testo all’interno di un file. Come per l’esempio precedente, suppongo che il file da scrivere si chiami “pippo.txt” e che risieda nella medesima directory. Vediamo il codice e cerchiamo di capire cosa produce:
import java.io.*;
public class ScriviFile {
public static void main(String[] args) {
String stringa1 = "Questa è la prima riga di testo";
String stringa2 = "Questa, invece, è la seconda riga";
try {
PrintStream ps = new PrintStream(
new FileOutputStream("/home/pippo.txt")
);
ps.println( stringa1 );
ps.println( stringa2 );
ps.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Questo esempio permette la creazione di un file con il seguente contenuto:
Questa è la prima riga di testo Questa, invece, è la seconda riga
Analizzando anche quest’ultimo esempio, si vedono subito quali sono le classi preposte alla scrittura dei file di testo: PrintStream, costruito su un FileOutputStream. Non è un caso che il metodo utilizzato per la scrittura si chiami println(): anche l’oggetto out della classe System, infatti, è un PrintStream. Il puntatore al file avamza automaticamente dopo ciascuna istruzione println() (oppure print(), se non si desidera inserire anche un ‘a capo’). Aggiungo, brevemente, che sarebbe opportuno sempre inserire anche una chiamata al metodo flush() dopo ciascuna scrittura, per prevenire problemi dovuti a terminazioni improvvise del programma: i dati inviati verso il file, infatti, vengono prima posti in un buffer, per ottimizzare gli accessi al disco e velocizzare, quindi, le operazioni di scrittura; se il programma termina prima che le scritture siano effettivamente state eseguite, i dati che pensiamo di aver salvato sul file verranno persi (la tecnica di buffering viene utilizzata da quasi tutti i linguaggi e dal sistema operativo stesso). In questo semplice caso non ho ritenuto doveroso farlo, poichè ci sono solo due righe da scrivere, prima della chiusura del file. La chiusura del file impone sempre un flush() dei dati.
Come per le letture, anche per le scritture vi sono classi apposite per ciascun tipo di file: FileOutputStream è la classe base per l’accesso in scrittura a tutti i file (quindi preposta, a maggior ragione, per i file binari). PrintStream è più adatta per le operazioni di scrittura sui file di testo (assieme a FileWriter, un po’ più generica). Per i file di dati in Java Byte Code, invece, la controparte di scrittura si chiama, ovviamente, ObjectOutputStream, mentre per i file ad accesso casuale, la stessa classe RandomAccessFile è preposta al trattamento sia della lettura, che della scrittura dei blocchi di dati.
E con questo si esaurisce l’articolo di tratamento del I/O su file per quanto riguarda la gestione in Java. L’articolo, ovviamente, non è esaustivo dell’argomento, ma questo è un blog e gli articoli devono essere limitati. Gli approfondimenti si potranno trattare in articoli futuri oppure aggiungendo dei commenti allo stesso, qualora si desidessero maggiori informazioni. Il mondo Java è molto ampio e trattare qui tutte le sfaccettature sarebbe impossibile. Da ricordare, comunque, che la documentazione Java è una fra le più complete, ben fatte e semplici da utilizzare e quindi è lo strumento principe da tenere sotto mano quando si programma. Auguro a tutti una Buona Programmazione!
Tags:classi file di testo java bytecode raw file
4 Risposte
Danilo
September 29th, 2008 at 09:33
1Ho 2 domande,
1] ma per la lettura e scrittura su file nn posso anche usare
DataInputStream input = new DataInputStream(new BufferedInputStream(new FileInputStream(”testo.txt”)));
DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(”testo.txt”)));
???
Perchè a me mi funziona, però nn so se è da preferire la procedura spiegata da te.
2] Utilizzando la RandomAccessFile vorrei poter accedere ad una particolare linea scelta dall’utente. “Tipo voglio la linea 5 del testo pippo.txt ” e il programma gli restituisce la linea. Ho visto che seek ti posizione però a secondo dei byte e nn è adatto alla mia esigenza.
Aspettando tue notizie
Ti saluto
Danilo
LeleFT
September 29th, 2008 at 12:45
21) Per la lettura e scrittura puoi anche utilizzare i DataInputStream e DataOutputStream. Queste due classi sono state concepite, però, più per la lettura di dati “grezzi” che arrivano da dei sottosistemi di tipo diverso (vengono molto spesso utilizzati per la lettura da Socket, dove i dati che arrivano sono spesso originati da applicazioni non-Java).
2) RandomAccessFile non può essere utilizzato in quel modo. La classe è prettamente rivolta all’utilizzo con file “di dati” organizzati a record. Tali tipi di file utilizzano una struttura (chiamata Record) in cui memorizzano diversi tipi di dati in modo organizzato. Il record ha sempre una linghezza fissa, quindi l’indicizzazione di un particolare record risulta semplice e veloce: si indirizza il byte che ha indirizzo [offset * (lunghezza_record)]. Così, se abbiamo un record di lunghezza 15, il record numero 3 (offset = 2, l’offset è sempre zero-based) si trova al byte numero: [2 * 15] = 30. Infatti, il primo record utilizza i byte da 0 a 14, il secondo i byte da 15 a 29 ed il terzo comincia al byte 30.
I file di testo, non essendo organizzati a record (non tutti, almeno), non possono essere aperti utilizzando RandomAccessFile (è possibile, ovviamente, aprirli utilizzando tale classe, ma il risultato non sarebbe apprezzabile… un file di testo può essere visto come un file organizzato a record in cui ciascun record ha lunghezza 1: il singolo carattere!!).
Ciao.
Suhel
December 16th, 2008 at 12:56
3Una Domanda :
Non esiste un ottimizzazione per la lettura dei file ? un file che contiene tante righe ci mette molto tempo a leggerlo. Volevo sapere se esiste un mondo per espandere il buffer di lettura oppure tipo caricare il file in memoria per velocizzare la lettura.
Help Plz
LeleFT
December 16th, 2008 at 15:28
4La velocità/lentezza nel leggere un file dipende, quasi sempre, dalle caratteristiche hardware della macchina. Le operazioni di I/O sono, generalmente, più lente di altre operazioni proprio perchè è necessario un accesso verso dispositivi hardware che sono notoriamente più lenti della memoria.
Le uniche “ottimizzazioni” possibili sono l’utilizzo diretto della classe FileInputStream (senza wrapper aggiuntivi), ma i miglioramenti non sono certo apprezzabili.
I memory mapped files sono una caratteristica fortemente dipendente dal sistema operativo (Unix/Linux, solitamente), che, di conseguenza, non possono essere adottati da Java.
Ad ogni modo bisognerebbe vedere la struttura del programma che legge il file. Io, personalmente, non ho mai avuto problemi di “lentezza” con la lettura di file di testo di grosse dimensioni: ho personalmente realizzato per l’azienda in cui lavoro un programma di analisi dei log di IIS, che analizzava file di log di circa 600 MB in poco più di 5-6 secondi.
RSS Iscriviti ai Feed dei commenti di questo articolo · TrackBack URI
Lascia un commento... please!!!
Autori
Categories
Calendario
Tag Cloud
Sondaggio
Blog Amici
Gente "Avanti"
Siti Interessanti
Benvenuto!
Qualche statistica
Blog segnalato su
Recent Posts
Recent Comments
Meta