Input/Output streams, Files
In Java, il tipo più comune di operazioni riguardanti l'input/output (I/O) su file e rete è basato su I/O streams, che sono oggetti che supportano i comandi di I/O. Infatti, standard input (System.in) e standard output (System.out) sono esempi di stream di I/O.
Lavorare con file e network richiede familiarità con le eccezioni. Molte delle funzioni che sono utilizzate possono lanciare checked exception, che richiedono la gestione delle eccezioni obbligatoria.
Questo significa in genere chiamare il metodo all'interno di try...catch per gestire l'eccezione qualora fosse sollevata.
I programmi leggono l'input da i data source, sorgenti di dati (tastiera, file, network, o un altro programma) e scrivono l'output su un output di dati, detto data sink (in inglese sink significa lavandino, pozzo), ad esempio il terminale video, un file, un buffer in memoria, o un altro programma. In Java le operazioni di input e output, sono gestite tramite gli stream. Lo stream è un flusso sequenziale e unidirezionale di dati (come l'acqua in un tubo). E' importante notare che Java non differenzia tra diversi tipi di data source, come file o network, keyboard o console. Java riceve i dati da una sorgente aprendo un input stream, e spedisce i dati in un data sink, aprendo un output stream. Tutti gli stream di I/O in Java sono unidirezionali (eccetto java.util.RandomAccessFile). Se il tuo programma ha bisogno di eseguire sia operazioni di input che di output, devi utilizzare due stream, un input stream e un output stream.
Le operazioni sugli stream di I/O si eseguono in tre passi:
Open l'input/output stream associato con il device fisico (es, il file, la console/testiera) costruendo l'istanza appropriata di I/O stream;
Read dall'input stream già aperto fino a incontrare la fine dello stream, o
writesull'output stream già aperto ( e eventualmente svuotare (flush) l'output del buffer);Close l'input/output stream;
Le operazioni di I/O in Java sono più complicate di quelle in C/C++ per supportare l'internazionalizzazione (i18n). Java internamente immagazzina i caratteri (tipi char) in 16 bit in UCS-2 (tipo di codifica). Ma il data source esterno può immagazzinare i caratteri in un altro tipo di codifica (es US-ASCII, ISO-8859-x, UTF-8, UTF-16 o altro tipo), di lunghezza fissa di 8 bit o 16 bit, o di lunghezza variabile di da 1 a 4 byte. Di conseguenza Java deve differenziare tra operazioni di I/O basate sui puri byte (raw bytes) o dati binari (binary data) e operazioni di I/O basate sui caratteri.

Prima di proseguire con gli stream parliamo di file e directory.
File e Directory
La classe java.io.File può rappresentare sia file che directory.
Una path string è usata per individuare un file o una directory. Sfortunatamente, i path string sono system dependent, es: "c:\myproject\java\Hello.java" in Windows o "/myproject/java/Hello.java" in Unix/Mac.
Windows usa il back-slash '\' come directory separator, mentre Unix/Mac utilizzano il forward-slash '/'.
Windows usa 'punto e virgola' (semicolon) ';' come path separator per separare liste di path; mentre Unix utilizza duepunti (colon) ':'.
Windows utilizza "\r\n" come separatore di linea nei file di testo; mentre Unix utilizza '\n' e Mac '\r'.
"c:\" o "\" è chimata la root. Windows supporta molte root, ognuna mappato su un drive (ad esempio "c:\" o "d:\"). Unix e Mac hanno una single root ("\").
Un path può essere assoluto (iniziando dalla root) o relativo (che è relativo a una directory base). le notazioni speciali "." e ".." denotano la directory corrente e la directory padre, rispettivamente.
La classe java.io.File mantiene queste proprietà system-dependent, così da poter scrivere programmi portabili:
Directory separator: i campi static
File.separator(come String) eFile.charSeparator.Path separator: i campi static
File.pathSeparator(come String) eFile.pathSeparatorChar.
Si può costruire una istanza di File con una path string o una URI. Da tener presente che il file/directory potrebbero esistere o non esistere fisicamente. L'URL di un file prende la forma di file://... ad esempio, file:///d:/docs/programming/java/test.html.
per esempio:
Verifica delle proprietà di un file o una directory
List directory
L'esempio sotto lista il contenuto di una certa directory (simile la comando UNIX "ls -r")
Ora riprendiamo la trattazione con gli stream.
Byte streams

Lettura da InputStream
La classe astratta java.util.InputStream dichiara il metodo astratto read() per leggere byte di dati:
Il metodo read():
ritorna l'input byte letto come un intero di valore tra 0 e 255, o
ritorna -1 se si incontra il carattere di "fine stream", o
lancia un eccezione di
IOExceptionse incontrato un errore di I/O
Il metodo read() ritorna un intero al posto di un byte, perché utilizza -1 per indicare la fine dello stream.
Il metodo read() blocca finché il byte non è disponibile, o in eccezione di I/O non è sollevata, o la fine dello stream è raggiunta. Il termine blocca vuol dire che il metodo (e il programma) è sospeso. Il programma riprende solo quando il metodo read() ritorna qualcosa.
Due varianti di read() sono implementate in InputStream per leggere blocchi di byte in un array di byte. Esso ritorna il numero di byte letti o -1 se arrivati alla fine dello stream.
Scrittura su OutputStream
Simile alla controparte dell'input, la superclasse astratta OutputStream dichiara il metodo astratto write() per scrivere un byte sull'output.
Il byte meno significativo di in int è utilizzato, i 3 byte più significativi sono scartati. Esso lancia una IOException se un errore di I/O avviene (ad esempio lo stream era già stato chiuso).
Come per la read(), due varianti di write() per scrivere blocchi di byte da un array di byte sono disponibili:
Chiusura di uno stream
Si apre uno stream di I/O, instanziando la classe di stream. Entrambi InputStream e OutputStream sono dotate del metodo close() per chiudere lo stream, che esegue tutte le operazioni per liberare la risorsa a livello di sistema.
E' buona pratica esplicitamente chiudere uno stream di I/O, eseguendo la close() nella clausola finally del try-catch-finally per liberare immediatamente le risorse quando abbiamo finito con lo stream. Questo per evitare spreco di risorse. Sfortunatamente, il metodo close() può sollevare anche lui IOException, e dobbiamo racchiuderlo anche lui tra try-catch come possiamo vedere sotto:
Questo non è molto bello e da Java 1.7 è stato introdotto la sintassi try-with-resource, che automaticamente chiude tutte le risorse aperte dopo try-catch. Si vede così un codice molto più pulito:
Flushing dell'OutputStream
In aggiunta la classe OutputStream provvede il metodo flush() per scaricare i rimanenti byte dal buffer di output:
Implementazioni di abstact InputStream/OutputStream
Le classi InputStream e OutputStream sono classi astratte che non possono essere instanziate. Bisogna scegliere una sottoclasse concreta per stabilire una connessione a un device fisico. Per esempio puoi instanziare un FileInputStream o FileOutputStream per creare uno stream a un file su disco.
Stream concatenati

Buffered I/O byte stream
I metodi read()/write() in InputStream/OutputStream leggono/scrivono un singolo byte per ogni chiamata. Tutto questo è molto inefficiente, in quanto ogni operazione è eseguita dal sistema operativo (che accede al disco o esegue altre operazioni dispendiose). Il buffering, che legge/scrive un blocco di byte da un device esterno in/dal buffer di memoria in una singola operazione di I/O, è comunemente utilizzato per velocizzare le operazioni di I/O.
FileInputStream e FileOutputStream non sono bufferizzate. Sono spesso concatenate con BufferedInputStream e BufferedOutputStream che provvedono il buffering. Per concatenate gli stream insieme, semplicemente si passa l'istanza di uno stream nel costruttore dell'altro stream. Per esempio il codice seguente concatena un FileInputStream a un BufferedInputStream e finalmente a un DataInputStream:
Esempio copia di file:
La versione, dello stesso programma, con try-catch-with-resource (da JDK 1.7):
Formatted Data-Streams: DataInputStream & DataOutputStream
DataInputStream e DataOutputStream possono essere concatenate con InputStream e OutputStream per parsare byte grezzi e per eseguire operazioni di I/O nel formato desiderato su tipi primitivi come int, double.
Per usare DataInputStream per input formattato, puoi concetenare l'input stream in questo modo:
DataInputStream implementa l'interfaccia DataInput, che provvede metodi per leggere tipi primitivi e String:
Similmente, si può concatenare DataOutputStream in questo modo:
DataOutputStream implementa l'interfaccia DataOutput, che provvede metodi per scrivere tipi primitivi e String:
Character stream

Java internamente immagazzina i caratteri (char type) in 16 bit codificati con UCS-2 character set. Ma le risorse esterne possono immagazzinare i caratteri anche in altre codifiche (es US-ASCII, , ISO-8859-x, UTF-8, UTF-16, e molte altre), in lunghezza fissa di 8 o 16 bit, o a lunghezza variabile da 1 a 4 byte.
Classi astratte Reader e Writer
A parte l'unità delle operazioni (il char al posto del byte) e la conversione dei character set (operazione complicata), I/O basato sui caratteri è piuttosto identico all'I/O basato sui byte. Al posto di InputStream e OutputStream noi utilizziamo Reader e Writer per stream di caratteri.
File I/O Character-Stream - FileReader & FileWriter
FileReader e FileWriter sono implementazioni concrete dell'abstract superclasse Reader e Writer, che supportato operazioni di I/O su disco. FileReader e FileWriter assumono l'encoding di default per le operazioni di I/O.
Buffered I/O Character Stream - BufferedReader & BufferedWriter
BufferedReader e BufferedWriter possono essere concatenati sopra FileReader/FileWriter o altri character stream per eseguire operazioni di I/O bufferizzate invece di operazioni carattere per carattere. BufferedReader ha il metodo readLine(), che legge una riga e ritorna una stringa, senza delimitatore. Le linee possono essere delimitare da '\n' in Unix, "\r\n" in Windows o "\r" su Mac.
Text File I/O - InputStreamReader and OutputStreamWriter
Si può selezionare il character set tramite il costruttore dell'InputStream.
java.io.PrintStream & java.io.PrintWriter

Une delle cose importanti riguardo il package di I/O è che permette di aggiungere caratteristiche a un oggetto di tipo stream wrappandolo a un altro stream con queste capacità in più. L'oggetto wrapper è a sua volta uno stream, così che si possa scrivere o leggere da esso, ma con in più delle funzionalità aggiuntive rispetto a quelle dello stream di base.
Per esempio, PrintWriter è una sottoclasse di Writer, che provvede metodi utili per scrivere in modo capibile da un essere umano una rappresentazione di tutti i tipi base di Java. Se abbiamo un oggetto di tipo Writer, a qualsiasi sua sottoclasse, e vorremmo usare i metodi di PrintWriter per scrivere l'output su quello stream Writer, tutto quello che dobbiamo fare e wrappare l'oggetto Writer nell'oggetto PrintWriter. Si fa questo costrueno un nuovo oggetto di tipo PrintWriter, utilizzando l'oggetto Writer come parametro del costruttore. Per esempio, se charSink è di tipo Writer, si può fare:
In effetti, il parametro nel costruttore può essere anche un oggetto di tipo OutputStrem o un File, e il costruttore creerà un PrintWriter che scrive sull'output. Quando scriviamo dati sul PrintWriter printableCharSink, utilizzando i metodi di altro livello della classe, i dati andranno direttamente nello stesso posto come i dati scritti su charSink. Abbiamo creato un'interfaccia migliore alla stessa destinazione. Questo permette di utilizzare i metodi di PrintWriter per inviare dati a un file o alla rete.
Se out è una variabile di tipo PrintWriter, allora i seguenti metodi sono definiti:
out.print(x)- stampa il valore di x, rappresentato nella forma di una stringa di caratteri, sull'ouput stream; x può essere un'espressione di qualsiasi tipo, inclusi sia tipi primitivi che tipi oggetti. Un oggetto è convertito in stringa utilizzando il metodotoString()dell'oggetto. Un valore null è rappresentato con la stringa "null";out.println()- scrive il fine linea sull'output stream;out.println(x)out.printf(formatString, x1, x2, ....)out.flush()
Da notare che nessuno di questi metodi lancia IOException. Invece la classe PrintWriter, include il metodo:
che ritorna true se c'è stato un errore scrivendo i dati sullo stream.
Object Serialization e Object Stream
ObjectInputStream & ObjectOutputStream
ObjectInputStream e ObjectOutputStream possono essere utilizzati per serializzare un oggetto in un byte stream, attraverso questi metodi:
ObjectInputStream e ObjectOutputStream possono essere aggiunti sopra una implementazione concreta di InputStream e OutputStream, come FileInputStream e FileOutputStream.
Per esempio, il seguente codice, scrive oggetti su file su disco. L' estensione ".ser" è una convenzione per i file di oggetti serializzati:
Per leggere e ricostruire gli oggetti all'interno del programma, utilizza il metodo readObject(), che ritorna java.lang.Object. Bisogna poi fare il downcast esplicito all'oggetto desiderato:
I tipi primitivi e gli array sono per default serializzabili.
Le classi ObjectInputStream e ObjectOutputStream implementano rispettivamente le interfacce DataInput e DataOutput . Si hanno quindi ad esempio i metodi readInt(), readDouble(), writeInt() e writeDouble() per leggere e scrivere i tipi primitivi.
transient & static
i campi static non sono serializzati, poiché appartengono alla classe e non all'istanza che deve essere serializzata;
per prevenire che certi campi siano serializzati, si possono segnare con la parola chiave transient. Questo può ridurre la dimensione di traffico dati;
il metodo
writeObject()scrive anche l'informazione sulla classe dell'oggetto, i valori degli attributi non-static e non-transient;
java.io.Serializable & Externalizable Interfaces
Se vogliamo avere una classe che può essere serializzata, la classe deve implementare l'interfaccia java.io.Serializable. L'interfaccia Serializable non dichiara nessun metodo. Interfacce vuote come Serializable sono dette tagging interface (altro esempio è l'interfaccia java.lang.Clonable). Esse identificano le classi che le implementano, il fatto di acquisire certe proprietà, senza richiedere alla classe di implementare alcun metodo.
Molte delle classi standard di Java implementano Serializable, ad esempio tutte la classi wrapper dei tipi primitivi, le classi contenitore, le classi della GUI. In effetti, le sole classi tra quelle standard che non implementano Serializable, sono solo quelle che non avrebbe senso che fossero serializzabili (ad esempio java.io.FileDescriptor, in quanto l'oggetto deserializzato non avrebbe significato in un processo rispetto a quello che l'ha creato).
Array di tipi primitivi o di oggetti serializzabili sono a loro volta serializzabili.
Esempio, l'applicazione ObjectSerializationTest, in cui vengono serializzati e deserializzati oggetti e array del tipo MySerializedObject, da noi definito:
Una classe potrebbe voler definire un comportamento custom per la serializzazione e la deserializzazione per i propri oggetti, implementando i metodi writeObject(ObjectOutputStream out) e readObject(ObjectInputStreamObject in). Sorprendentemente questi metodi non sono specificati in nessuna interfaccia. Questi metodi devono essere dichiarati private, che è ancora sorprendente, in quanto sono chiamati dall'esterno della classe durante il processo di serializzazione e deserializzazione. Se la classe definisce questi metodi, quello appropriato è invocato da ObjectOutputStream e ObjectInputStream quando un oggetto è serializzato o deserializzato.
Ad esempio, il nostro componente custom potrebbe definire un metodo readObject() per dare l'opportunità di ricalcolare una propria proprietà in fase di deserializzazione (una proprietà dell'oggetto che ad esempio non ha senso che sia serializzata in quanto il valore magari varia e deve essere calcolato in base alla piattaforma su cui è eseguito il programma, ad esempio la dimensione dei font per un programma di grafica). Il metodo potrebbe essere di questo tipo:
Esempio di utilizzo di transient e implementazione di metodi writeObject() e readObject(): esso mostra una classe che implementa un array di numeri non a dimensione fissa. Questa classe definisce un metodo writeObject() per fare qualche operazione di pre-processing prima di essere serializzata e il metodo readObject() per fare operazioni di post-processing dopo la deserializazione.
java.io.Externalizable Interface
L'interfaccia Serializable ha una sotto-interfaccia chiamata Externalizable, che può essere utile se si vuole customizzare il modo in cui una classe è serializzata. Poichè Externalizable estende Serializable, è anche Serializable e si può invocare readObject() e writeObject() di ObjectInputStream e ObjectOutputStream rispettivamente.
Externalizable dichiara due metodi abstract:
ObjectOutput e ObjectInput sono interfacce che sono implementate da ObjectOutputStream e ObjectInputStream, che definiscono i metodi writeObject() e readObject() rispettivamente. Quando un'istanza di Externizable è passata a un ObjectOutputStream, la procedura standard di serializzazione è bypassata; invece lo stream chiama il metodo writeExternal() dell'oggetto. Allo stesso modo, quando un ObjectInputStream legge un Externizable instanza, utilizza readExternal() per ricostruire l'oggetto.
Externizable è utile se si vuole un completo controllo su come un oggetto è serializzato/deserializzato. Ad esempio si potrebbe voler criptare dei dati serializzabili prima di serializzare un oggetto.
Esempio definiamo la classe CompactIntList che implementa Externalizable, e ridefinisce la logica per serializzare e deserializzare per ottenere un formato più compatto:
Random access file
Tutti gli stream di I/O che abbiamo finora trattato sono unidirezionali (one-way). Cioè essi sono o read-only input stream o write-only output stream. Inoltre, essi sono tutti stream solo di accesso sequenziale, utili per leggere o scrivere dati sequenzialmente. Tuttavia, è delle volte necessario leggere il record del file direttamente così come modificare un record esistente come inserire un nuovo record. La classe RandomAccessFile provvede le funzionalità per l'accesso non sequenziale, diretto accesso al file su disco. RandomAccessFile è uno stream bidirezionale (two-way), in quanto supporta entrambe le operazioni, di input e di output, sullo stesso stream.
RandomAccessFile può essere trattato come un grosso array. Possiamo usare un puntatore a file (di tipo long), come all'indice di un array, per accedere a byte individuali o gruppi di byte di tipo primitivi (come int o double). Il puntatore a file è in posizione 0 quando il file è aperto. Esso avanza automaticamente per ogni operazione di read e write per il numero di byte processati.

Nella creazione di un RandomAccessFile, si possono utilizzare i flag 'r' o 'rw' per indicare se l'accesso è in sola lettura o anche in scrittura.
i seguenti metodi sono disponibili:
RandomAccessFile non è sottoclasse di InputStream o OutputStream. Tuttavia implementa le interfacce DataInput e DataOutput (simile a DataInputStream e DataOutputStream). Quindi, sono disponibili diversi metodi per leggere e scrivere tipi primitivi su un file, ad esempio:
Esempio:
L'output di entrambe le println è:
Mentre, se apriamo un file anche in scrittura possiamo modificarne il contenuto come vediamo in questo esempio:
Esempi I/O
Esempio - Compressione di file e directory
L'esempio vuole dimostrare un'applicazione interessante delle classi di stream: comprimere file e directory. Le classi utilizzate non sono parte del package java.io ma di java.io.zip. La classe Compress definisce due metodi statici, gzipFile(), che comprime un file utilizzando il formato di compressione GZIP, e zipDirectory() che comprime i file (ma non le directory) in una directory utilizzando il ZIP archive e formato di compressione. gzipFile() usa la classe GZIPInputStream, mentre zipDirectory() usa le classi, ZipOutputStream e ZipEntry, tutte da java.util.zip.
La classe AppCompress per l'esecuzione dell'applicazione:
Utilizzo: java AppCompress <from> [<to>]
Per la lettura invece di contenuti compressi ci sono le corrispettiva classi per l'input: java.util.zip.ZipInputStream e java.util.zip.GZIPInputStream.
Esempio - Filtraggio di Character Stream
La classe java.io.FilterReader è una classe astratta che definisce un "fltro nullo" - legge i carattere da in Reader specificato e li ritorna senza modifiche. In altre parole, FilterReader definisce delle implementazioni che non fanno operazioni di tutti i metodi di Reader. Una sottoclasse deve sovrascrivere almeno i due metodi read() per eseguire il tipo di filtering che si desidera. Alcune sottoclassi possono anche sovrascrivere altri metodi se necessario. Nell'esempio RemoveHTMLReader, è una sottoclasse di FilterReader che legge testo HTML dallo stream a filtra tutti i tag HTML dal testo che ritorna.
Si poteva definire anche una classe RemoveHTMLWriter, eseguendo la stessa azione di filtraggio in una solttoclasse di java.io.FilterWriter. Per filtrare byte streams invece di character streams, avremmo utilizzato sottoclassi di java.io.FilterInputStream e java.io.FilterOutputStream.
La classe, AppRemoveHTMLReader, per l'esecuzione dell'applicazione:
Utilizzo: java AppRemoveHTMLReader <filename>
Esempio - Filtraggio linee di testo
La classe GrepReader definisce un altro esempio di input stream custom. Questo stream legge le linee di testo da un Reader specificato e ritorna solo le linee che contengono una specificata sottostringa. In questo modo lavora come il comando fgrep di UNIX - eseguen una ricerca sull'input. GrepReader esegue il filtering, ma filtra il testo una linea alla volta, piuttosto che un carattere alla volta, per questo estende BufferedReader e non FilterReader.
La classe AppGrepReader, per l'esecuzione dell'applicazione:
Utilizzo: java AppGrepReader <pattern> <file>
Esempio - PhoneDirectoryFileDemo
Vediamo in particolare esempio PhoneDirectoryFileDemo , programma per gestire una rudimentale rubrica telefonica, che associa a un nome il numero di telefono. Utilizza una TreeMap<String, String>, per associare il nome al numero di telefono, inoltre ha funzionalità di ricerca, cancellazione, lista oltre che l'aggiunta dei record (nome, numero telefono).
Per far si che i dati immessi siano reperibili al prossimo utilizzo del programma, i dati dei record immessi sono salvati su file (in fase di startup dell'applicazione vengono letti e caricati nella TreeMap che contiene l'associazione nome e numero di telefono).
Codice esempi
RISORSE
Per approfondire I/O vedi: http://www3.ntu.edu.sg/home/ehchua/programming/java/J5b_IO.html
Last updated