MokaByte 95 - Aprle 2005 
IO in Java
I parte: gli Stream e la classe File
di
Andrea Gini
L'Input/Output comprende tutta una serie di operazioni di dialogo da e verso i dispositivi, un qualcosa di cui ogni programma che si rispetti deve occuparsi. L'IO in Java è basato su quello usato nel sistema Unix, del quale vengono riproposti in maniera fedele il modello di comunicazione a stream e le system call. Il risultato è un sistema di comunicazione piuttosto semplice, ma poco "Object Oriented", come sostengono alcuni critici. Di fatto l'API java.io è la libreria di base su cui sono stati realizzati servizi di più alto livello, come il networking e RMI. Nell'articolo di questo mese verranno illustrate le basi dell'IO basato su stream; verrà quindi spiegata la classe File, che permette di creare programmi che maneggiano file in maniera indipendente dalla piattaforma. Gli esempi presentati sono stati realizzati con la versione 1.5 del JSDK, l'ultima disponibile, anche se in molti casi potranno funzionare con poche modifiche anche sulle versioni precedenti.

Input Output
l'IO, o Input/Output, descrive le modalità attraverso le quali il computer dialoga con il mondo esterno. l'IO permette di leggere un file su disco, inviare caratteri ad una stampante, aprire una connessione di rete con un sistema remoto e molte altre cose. Più precisamente per input si intende la modalità attraverso cui un programma legge i dati provenienti da un programma o da un dispositivo, mentre l'output riguarda l'invio di dati verso una destinazione.

Alcuni dispositivi, come gli hard disc, supportano sia operazioni di input che di output, altri solo una tipologia di comunicazione: la stampante, ad esempio, è generalmente un dispositivo di output, mentre la tastiera è un tipico dispositivo di input.

 

IO basato su Stream
Nel corso degli anni, l'IO è stato è stato trattato in maniere differenti tra loro, a seconda del linguaggio utilizzato o del sistema operativo sottostante. Java ricorre ad un approccio proveniente dal mondo Unix: la comunicazione attraverso stream. Gli stream sono canali che trasmettono dati attraverso una connessione da una sorgente a una fonte (o viceversa). La trasmissione avviene un elemento alla volta, in sequenza e in un'unica direzione. La prima di queste affermazioni vuole precisare che ogni stream prevede un elemento minimo di comunicazione (tipicamente un byte o un carattere); la seconda garantisce che in una trasmissione l'ordine dei dati viene mantenuto, e che i dati escono da un'estremità dello stream nello stesso ordine in cui sono stati inseriti dal lato sorgente; l'ultima sottolinea il fatto che gli Stream sono canali specializzati per effettuare operazioni di Input o Output in maniera mutuamente esclusiva, e che se si desidera una comunicazione bi-direzionale, è necessario aprire due canali distinti.


Figura 1
- Rappresentazione grafica di uno stream

 


IO Bloccante
L'IO basato su Stream è di tipo bloccante, una caratteristica che semplifica di gran lunga la vita al programmatore. L'IO viene definito bloccante quando le primitive di lettura e scrittura offerti dal canale bloccano il programma qualora il dispositivo non sia pronto ad inviare o ricevere dati. Il programma viene riavviato automaticamente e in modo trasparente non appena si ripristina la condizione necessaria alla trasmissione. In questo modo il programmatore non deve preoccuparsi del reale stato del dispositivo: i programmi si comportano come se le sorgenti e le fonti di dati fossero sempre pronte a trasmettere o a ricevere dati; tutta la complessità sottostante all'effettiva sincronizzazione con i dispositivi viene completamente nascosta all'utente.

Nota: a partire dalla distribuzione 1.4, è stato introdotto in Java una nuova API per l'IO che supporta anche operazioni asincrone e non bloccanti. Tale API è riservata ad usi specifici ed avanzati dell'IO, particolarmente nella programmazione Server Side, e per questo integra, ma non sostituisce, la tradizionale API che verrà trattata in questa serie di articoli. Chi fosse interessato alla nuova API per l'IO può consultare [1].

Esistono due tipi di stream: quelli orientati ai byte e quelli orientati ai caratteri. Per motivi storici verranno introdotti in questo ordine, anche se di fatto oggi si usano più i secondi che i primi.


Stream orientati ai byte
Gli stream orientati ai byte sono stati introdotti in Java fin dalla sua prima edizione. La loro interfaccia di programmazione ricalca le system call Unix per l'IO, una cosa che non deve sorprendere dal momento che Unix è il sistema operativo sul quale Java è stato sviluppato inizialmente. Il byte è l'unità minima di informazione usata dai computer, per cui è naturale che in un primo tempo la comunicazione con i dispositivi di IO venisse fatta con stream orientati ai byte; in seguito, a partire dalla release 1.1 del JSDK, sono stati introdotti gli stream orientati ai caratteri, più pratici per il programmatore. Tali stream verranno approfonditi nel prossimo articolo; prima di essi verranno introdotti gli stream orientati ai byte, a partire da InputStream e OutputStream, le superclassi astratte di qualunque altro stream.

InputStream API
Gli InputStream servono a collegare una sorgente di dati, tipo un file o la tastiera, ad un programma che ne deve manipolare i dati. I metodi caratteristici di questa API sono i seguenti:

int available()

Restituisce il numero di byte che possono essere letti da questo stream senza provocare un blocco in lettura.

abstract int read()

Regge il successivo byte dallo stream, e lo restituisce sotto forma di intero. Il valore è compreso tra 0 e 255, ed è possibile trasformarlo in byte o in char con un'operazione di casting. Nel caso lo stream non contenga più dati, viene restituito il valore -1.

int read(byte[] b)

Legge una serie di byte dallo stream in modo da riempire il buffer passato come parametro. Come valore di ritorno viene restituito il numero di byte effettivamente letti, o -1 se è stata raggiunta la fine dello stream.

long skip(long n)

Scarta n bytes dallo stream. Il metodo restituisce il numero di byte effettivamente scartati.

void close()

Chiude lo stream e rilascia le risorse ad esso associate.

Ognuno di questi metodi può generare una IOException qualora si verifichi un problema che impedisce la comunicazione.

OutputStream API
Gli OutputStream svolgono una funzione speculare a quella degli InputStream, e servono a collegare un programma ad un ricevitore di byte, come ad esempio un file in scrittura.

void write(byte[] b)

Scrive nello stream i byte contenuti nell'array passato come parametro.

abstract void write(int b)

Scrive nello stream il byte passato come parametro sotto forma di intero compreso tra 0 e 255.

void flush()

Forza lo stream ad inviare tutti i byte in esso contenuti. Alcuni Stream concreti, come i BufferedOutputStream, mantengono al loro interno un buffer di byte: questo comando ne forza lo svuotamento .

void close()

Chiude lo stream e rilascia le risorse ad esso associate. Anche in questo caso, i metodi presentati possono generare una IOException qualora si verifichi un problema che impedisce la comunicazione.

 

Stream di sistema: System.out, System.in e System.err
Molti programmatori, magari senza saperlo, hanno più volte utilizzato almeno uno Stream: lo stream di output di default. Ogni volta che si utilizza l'istruzione

System.out.println("Stampa qualcosa");

si utilizza il metodo println() dell'attributo statico out della classe java.lang.System, che è un'istanza di PrintOutputStream, uno stream molto simile al PrintWriter che verrà analizzato nel prossimo articolo. La classe java.lang.System contiene i campi pubblici e statici in, out ed err che permettono di accedere rispettivamente agli stream di default di input, output e error. Gli stream di output e di error sono dei PrintOutputStream, mentre lo stream di input è un normale InputStream. Grazie ad essi è possibile stampare messaggi di output o di errore sulla console di sistema o ricevere caratteri in input dall'utente. In epoca di interfacce grafiche, l'uso della console non è ancora passato completamente di moda, per cui non deve stupire che Java implementi questa soluzione, proveniente anch'essa dal mondo Unix.
Uso basilare degli stream
Il package java.io contiene una decina di stream concreti. In questa serie di articoli verranno trattati in modo approfondito gli stream orientati ai caratteri, per cui non verranno analizzati tali sottoclassi nei dettagli. D'altra parte un paio di esempi funzionanti sono necessari per illustrare i concetti analizzati fino ad ora.

 

Esempio Echo
Si osservi il seguente programma:

import java.io.*;

public class FileEcho {

public static void main(String argv[]) throws Exception {
InputStream in = new FileInputStream(argv[0]);
while(true) {
int i = in.read();
if(i==-1)
break;
else
System.out.print((char)i);
}
}
}

Nel programma di esempio, che per semplicità non contiene nessuna forma di controllo di errori o eccezioni, viene creato un FileInputStream, ossia uno stream che legge il contenuto di un file, e attraverso le istruzioni successive ne legge il contenuto, un byte alla volta fino al termine del file. Se viene incontrato la fine del file (il valore -1), il programma termina, altrimenti il byte viene stampato su schermo dopo averlo convertito in char attraverso un'operazione di casting. Per lanciare il programma è necessario specificare sulla riga di comando il nome di un file di testo tipo quello presente nella directory degli esempi:

java FileEcho1 readme.txt

Si noti la necessità di effettuare una conversione esplicita tra int e char ricorrendo al casting. Questo genere di complicazioni, come si vedrà nei prossimi paragrafi, non esistono negli stream orientati ai caratteri.

 

Programma Copy
Il seguente programma è più complesso del precedente, non solo perchè effettua tanto operazioni di lettura quanto di scrittura, ma anche perché, a differenza del precedente, prevede un valido controllo degli errori.

import java.io.*;

public class Copy {

public static void main(String argv[]) {
if(argv.length!=2)
System.out.println("Usage: java copy <srcfile> <destfile>");
else
try {
FileInputStream in = new FileInputStream(argv[0]);
FileOutputStream out = new FileOutputStream(argv[1]);
while(true) {
int data = in.read();
if(data == -1)
break;
else
out.write(data);
}
}
catch(IOException ioe) {
ioe.printStackTrace();
}
}
}

In primo luogo viene effettuato un controllo sul numero dei parametri, che devono essere due, ossia il file di origine e quello di destinazione:

java copy readme.txt copyOfreadme.txt

Nelle successive istruzioni, contenute in un blocco try-catch, vengono aperti un FileInputStream e un FileOutputStream, che come si può intuire servono rispettivamente a leggere e a scrivere un file. Nel successivo ciclo while vengono letti uno ad uno i byte del file di origine, che vengono quindi scritti nel file di destinazione fino a quando non viene raggiunta la fine del file (il valore -1). Se durante la lettura si verifica un eccezione, questa viene stampata a console.

 

File
Nei paragrafi precedenti si è visto come leggere e scrivere un file a partire dagli stream FileInputStream e FileOutputStream; la posizione del file veniva specificata dall'utente attraverso parametri di riga di comando, ossia attraverso delle String. La rappresentazione del percorso di un file è un problema estremamente critico in un sistema come Java, basato sulla filosofia "Write Once, Run Everywere", dal momento che quest'ultima cambia in modo significativo a seconda della piattaforma ospite. Per questa ragione il package java.io contiene una classe File che fornisce una rappresentazione astratta dei percorsi di file e directory, e che permette di effettuare numerose operazioni sui file. Se si desidera realizzare programmi realmente portabili è importante imparare ad utilizzare questa classe nel migliore dei modi. Nei tre paragrafi seguenti verranno analizzati i campi, i costruttori e i principali metodi di questa importante classe Java.

Campi

static String pathSeparator
static char pathSeparatorChar

Questa coppia di campi statici permettono di leggere il carattere usato dal sistema come separatore di percorso ("\" sotto windows e "/" sotto Unix/Linux) sotto forma di char o di String.

static String separator
static char separatorChar

Questa coppia di attributi statici contengono, sotto forma di char o String, il separatore di file usato dal sistema (";"sotto windows e ":" sotto Unix/Linux)

Costruttori

File(String pathname)

Crea un file il cui nome e percorso vengono specificati sotto forma di String. Per convenzione Java segue le convenzioni Unix, ed utilizzaa il carattere "/" come separatore: sotto la piattaforma Windows è possibile usare sia il formato nativo che quello Unix, ma solo in quest'ultimo caso il programma funzionerà in entrambe le piattaforme.

File(File parent, String child)
File(String parent, String child)

Questa coppia di costruttori permettono di creare un file il cui nome viene specificato dalla String che compare come secondo parametro, mentre il primo parametro, che può essere sia File che String, denota la directory.

Metodi

boolean exists()

Verifica l'esistenza del file.

boolean isAbsolute()

Verifica se il percorso del file è assoluto

boolean isDirectory()
boolean isFile()
boolean isHidden()

I tre metodi precedenti permettono di testare se il presente oggetto File denota un File vero e proprio, una directory e se è nascosto o meno.

boolean canRead()
boolean canWrite()

Questa coppia di metodi verifica se il file può essere letto o scritto

boolean setReadOnly()

Imposta l'oggetto corrente come File a sola lettura

boolean createNewFile()

Crea un file vuoto col nome e la posizione specificate dal costruttore, se quest'ultimo non esiste già.

static File createTempFile(String prefix, String suffix)
static File createTempFile(String prefix, String suffix, File directory)

Questa coppia di metodi statici creano un file temporaneo nella cartella temporanea di sistema o in una cartella specificata dall'apposito parametro. I parametri prefix e suffix permettono di specificare il prefisso e il suffisso del nome del file, i caratteri in mezzo vengono definiti in modo pseudo casuale.

boolean delete()

Cancella il file denotato dal percorso specificato nel costruttore.

void deleteOnExit()

Cancella il file quando la Virtual Machine termina la sua esecuzione. Questo metodo è particolarmente utile per cancellare i file temporanei in modo automatico quando il programma termina l'esecuzione.

File getAbsoluteFile()
String getAbsolutePath()

Questi metodi restituiscono il percorso completo di un file o la sua directory sotto forma di File o di String. Il percorso assoluto è dipendente dal sistema.

String getName()

Restituisce sotto forma di string ail nome del file o della directory denotata dal presente oggetto File

String getParent()
File getParentFile()

Questa coppia di metodi restituiscono la directory parent del presente oggetto File, sotto forma di File o di String, o null nel caso il File parent non sia stato dichiarato al costruttore.

String getPath()

Converte sotto forma di String il valore del percorso

long lastModified()

Restituisce la data dell'ultima modifica sotto forma di intero long. Per convertirlo in una data in forma canonica bisogna ricorrere all'apposita classe java.util.Date.

long length()

Restituisce la lunghezza del file corrente, ossia il numero di byte che esso contiene.

String[] list()
String[] list(FilenameFilter filter)
File[] listFiles()
File[] listFiles(FileFilter filter)


I precedenti metodi restituiscono un array di String o di Fils che contiene l'elenco dei files e delle directory contenuti nel presente oggetto File, a patto che questo rappresenti a sua volta una directory. L'interfaccia di appoggio FileFilter dichiara il metodo accept(File pathname), che permette di creare filtri ad hoc per le situazioni che lo richiedano (ad esempio un filtro che accetta solo files che terminano con l'estensione .java)

static File[] listRoots()

Restituisce l'elenco delle radici presenti nel file system ospite. In un sistema di tipo Unix l'unica radice è "/"; nei sistemi windows le radici sono tante quante sono le unità montate sul sistema (A:, C:, D: e così via)


boolean mkdir()
boolean mkdirs()

Crea la directory specificata dal corrente oggetto File. Il metodo mkdirs() crea anche le directory intermedie, qualora non fossero presenti. I metodi restituiscono true se la directory è stata effettivamente creata.
boolean renameTo(File dest)
Rinomina il file come specificato dal parametro

String toString()
URI toURI()
URL toURL()

Restituiscono una rappresentazione del corrente File sotto forma di String, URL o URI.

 

Conclusioni
Questo mese sono state introdotte le problematiche generali dell'Input Output, quindi è stato presentato il modello di IO a stream usato in Java, a sua volta mutuato da Unix. Per concretizzare i concetti illustrati, sono stati mostrati due programmi di esempio. Per finire è stata descritta l'API File, che permette di realizzare programmi indipendenti dalla piattaforma ospite. Il mese prossimo verranno analizzati gli stream orientati ai caratteri, che permettono una gestione più pratica dell'IO.

 

Bibliografia
[1] Java 2 SDK: New I/O: Documentation
http://java.sun.com/j2se/1.4/nio/index.html

[2] Corso di Java Base
http://www.mokabyte.it/2002/02/jbase_1.htm
http://www.mokabyte.it/2002/03/jbase_2.htm
http://www.mokabyte.it/2002/04/javabase-3.htm
http://www.mokabyte.it/2002/06/javabase-4.htm
http://www.mokabyte.it/2002/07/javabase-5.htm
http://www.mokabyte.it/2002/09/javabase-6.htm
http://www.mokabyte.it/2002/10/javabase-7.htm
http://www.mokabyte.it/2002/11/javabase-8.htm
http://www.mokabyte.it/2002/12/javabase-9.htm
http://www.mokabyte.it/2003/02/corsojava-10.htm
http://www.mokabyte.it/2003/03/corsojava-11.htm
http://www.mokabyte.it/2003/04/corsojava-12.htm
http://www.mokabyte.it/2003/05/corsojava-13.htm
http://www.mokabyte.it/2003/06/corsojava-14.htm
http://www.mokabyte.it/2003/09/corsojava-15.htm
http://www.mokabyte.it/2003/10/corsojava-16.htm

[3] La programmazione grafica in Java
http://www.mokabyte.it/2003/11/corsojava-17.htm

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it