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:

  • Tipi di file
  • Lettura dei file
  • Scrittura dei file

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: