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
|