Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Stefano Russo » 5.Operazioni di Input-Output


 Sommario

  • Il concetto di stream
  • Uso del pattern Decorator
  • Classi gestione IO in Java 1.0 e in Java 1.1
  • Tokenizzatori di flusso
  • Esempi pratici

Gestione IO

  • Il concetto di Stream
  • IO in Java 1.0
  • IO Filtrato
  • IO su File
  • IO in Java 1.1
  • StreamTokenizer

Il concetto di Stream

Java gestisce le operazioni di IO servendosi dell’astrazione di flusso o stream e di due classi fondamentali: OutputStream e InputStream.

Stream: sequenza di byte che viaggiano da una origine a una destinazione lungo un canale monodirezionale, di cui

  • OutputStream: punto di accesso;
  • InputStream: punto di arrivo.

I dati scritti nell’output stream può essere letto dall’input stream.


Il concetto di Stream

Java offre molteplici classi per la gestione dell’IO (che compongono la libreria java.io), tutte basate sul concetto fondamentale di flusso o stream.

OutputStream, classe astratta.

  • Rappresenta la sorgente di un flusso con metodi write() per scrivere sul flusso.

InputStream, classe astratta.

  • Rappresenta la destinazione di un flusso con metodi read() per leggere dal flusso.

IO in Java 1.0

Java offre molteplici classi per la gestione dell’IO (che compongono la libreria java.io), tutte basate sul concetto fondamentale di flusso o stream.

OutputStream rappresenta la classe base della gerarchia di classi che caratterizzano la destinazione dei dati nel flusso.


IO in Java 1.0

InputStream è la classe base della gerarchia di classi che decidono da dove acquisire i dati attraverso il flusso.


IO in Java 1.0

Consideriamo come esempio di gestione dell’IO in Java un’applicazione che:

  1. scrive su un ByteArrayOutputStream una stringa di caratteri;
  2. successivamente acquisisce la stringa attraverso un ByteArrayInputStream.

IO in Java 1.0

Mostra codice

Dove throws IOException { rappresenta l'invocazione di metodi che realizzano operazioni di IO e possono sollevare eccezioni di  tipo IOException, che vanno gestite.

Con

ByteArrayOutputStream outStream = new ByteArrayOutputStream();
String s = "Questo è un test ...";
for(int i = 0; i < s.length(); i++)
outStream.write(s.charAt(i));

si istanzia un oggetto di tipo ByteArrayOutputStream e la stringa da utilizzare. Successivamente si realizza l'operazione di scrittura carattere per carattere.

IO in Java 1.0

Mostra codice

ByteArrayInputStream inStream;
inStream = new ByteArrayInputStream(outStream.toByteArray());

Si istanzia un oggetto di tipo ByteArrayInputStream, trasmettendo come parametro di ingresso al costruttore l'array di outStream, che rappresenta il buffer da cui estrarre i byte.

int inBytes = inStream.available()
byte inBuf[] = new byte[inBytes];
int bytesRead = inStream.read(inBuf, 0, inBytes);

Si utilizza available() per conoscere quanti caratteri si devono leggere e dimensionare opportunamente un array di byte, che verrà usato nell'operazione di read.

IO filtrato: generalità

Tra le classi derivate da OutputStream e InputStream troviamo due classi parti-colari che consentono di implementare la tecnica dell’”IO filtrato“: è possibile aggiungere dei filtri ai flussi, per adattarli a specifiche esigenze di programmazione.

IO filtrato: generalità

FilterOutputStream: classe astratta base di tutte le classi di flussi di output filtrati, che offre la possibilità di creare un flusso a partire da un altro.

È possibile definire filtri multipli concatenati usando più livelli di annidamento.

FilterInputStream è complementare a FilterOutputStream per i flussi di Input filtrati.


Pattern Decorator

I filtri di flussi sono realizzati secondo un pattern che prende il nome di decorator: uso di oggetti annidati per aggiungere in modo dinamico e trasparente responsabilità a singoli oggetti.

Il pattern decorator è molto simile alla composizione: la classe di livello superiore ha come attributo un oggetto della classe di livello inferiore.

La differenza è che la classe di livello superiore deve avere la stessa interfaccia della classe dell’oggetto contenuto, così da poter far uso delle due classi in maniere trasparente al programmatore.

Decoratore vs. ereditarietà

Usando differenti combinazioni di pochi tipi di wrapper è possibile ottenere molti comportamenti diversi. Utilizzando l’ereditarietà è possibile ottenere lo stesso risultato solo estendendo una classe per ogni comportamento

Rispetto all’ereditarietà utilizzare il pattern Decorator implica:

  • usare meno classi (progettazione e implementazione semplificata);
  • usare più oggetti (debugging più difficoltoso).

Decoratore vs. ereditarietà

Decoratore

  • Dinamico (run time)
  • Senza vincoli per il cliente (può inventarsi combinazioni non previste)
  • Aggiunge responsabilità a un singolo oggetto
  • Evita esplosione combinatoria

Ereditarietà

  • Statico (compile time)
  • Con vincoli per il cliente (non può inventarsi combinazioni non previste)
  • Aggiunge responsabilità a (tutte le istanze di) una classe
  • Può causare esplosione combinatoria

Decorator per l’IO filtrato


IO filtrato

DataOutputStream consente di inserire istanze di tipi primitivi (ad es. int) e String in uno stream di Output.

PrintStream produce un output formattato.

BufferedOutputStream viene usato per prevenire una scrittura fisica ogni volta che si scrive su un flusso, i dati in scrittura sono posti su un buffer e l’utente sollecita una scrittura sul flusso con flush().


IO filtrato

DataInputStream e BufferedInputStream sono i complementari di DataOutputStream e BufferedOutputStream per i flussi di Input.

LineNumberInputStream registra i numeri di riga nello stream di Input.

PushBackInputStream dispone di un buffer Push-Back della dimensione di un byte, cosicché l’utente può operare un inserimento nel buffer dell’ultimo byte letto.


IO filtrato

Mostra codice

Dove con

DataInputStream(fis);
Il costruttore del filtro richiede in ingresso il riferimento di un altro flusso o filtro a cui connettersi. Questo mette in luce la natura dei filtri: sebbene derivate da InputStream o OutputStream, le classi-filtro non sono dei veri e propri stream, infatti hanno come end-point degli stream anziché dei device.
Con le istruzioni
int i= dis.readInt();
String s= dis.readUTF();

Posso fare operazioni di lettura senza l'obbligo di usare array di tipo byte come nel caso precedente.


IO filtrato

Mostra codice

Ma il filtro consente di ottenere un accesso tipizzato al canale del flusso:
byte inBuf[] = new byte[inBytes];
int bytesRead = inStream.read(inBuf, 0, inBytes);

Infine attraverso
dis.close();
fis.close();

l'ordine di invocazioen dei metodi close() segue all'inverso l'ordine della catena filtri/flussi: ocorre chiudere prima il filtro (o la sua successione) e poi il flusso.

IO filtrato

Analogamente, possiamo usare nel seguente modo la combinazione del filtro DataOutputStream e del flusso FileOutputStream.

Mostra codice

Notiamo come in scrittura non siamo limitati solo a scrivere un byte o un array di byte come nel caso precedente outStream.write(s.chartAt(i)); ma possiamo scrivere valori dei tipi primitivi e String:

dis.writeDouble(1.3456);
dis.writeInt(44);
dis.writeUTF("ciao mondo");

IO filtrato

Mostra codice

IO su File

L’IO su file è realizzabile per mezzo della combinazione di FileOutputStream e FileInputStream. Java dispone di un’altra classe che prende il nome di RandomAccessFile:

  • consente di effettuare operazioni di input/output direttamente in specifiche porzioni di un file;
  • non è derivata dalle classi basi InputStream/OutputStream;
  • il nome della classe deriva dal fatto che i dati possono essere letti e/o scritti su porzioni casuali all’interno di un file, invece di costituire un flusso continuo di informazioni.

IO su File

Per facilitare la gestione dell’IO su file, Java offre anche un’ulteriore classe:File.

Questa classe consente di accedere a uno o più file o directory, e utilizza le convenzioni di assegnazione dei nomi del sistema operativo host.

Esempio di IO su File

Mostra codice

Dove
File file = new File("myFile.txt");
try {
file.createNewFile();
} catch (IOException x) {
System.err.format("createFile error: %s%n", x.getMessage());}

Crea un file di nome "myFile.txt"

I seguenti comandi invece realizzano operazioni di lettura:

  1. in = new FileInputStream(file); istanziazione InputStream sul file;
  2. BufferedReader reader = new BufferedReader(new InputStreamReader(in)); istanziazione di un BufferReader;
  3. while ((line = reader.readLine()) != null) lettura riga per riga del file;
  4. System.err.println(x); gestione eventuali eccezioni;
  5. if (in != null) in.close();}  chiusura flusso.

Esempio di IO su File: scrittura

Mostra codice

I seguenti comandi  realizzano operazioni di scrittura:

  1. (newFileOutputStream(file)); istanziazione OutputStream sul file;
  2. out = new BufferedOutputStream istanziazione di un BufferedOutputStream;
  3. out.write(data, 0, data.length); scrittura dell'intero array di byte;
  4. System.err.println(x); gestione di eventuali eccezioni;
  5. out.close(); chiusura flusso.

IO su File con RandomAccessFile

Mostra codice

Java 1.1 IO streams

La libreria java.io in Java 1.1 estende la libreria di con nuove classi, organizzate in due gerarchie (una per l’input e una per l’output) con a capo rispettivamente le classi astratte Reader e Writer.

Non si deve erroneamente pensare che Reader e Writer abbiano soppiantato InputStream e OutputStream. Esistono casi, infatti, dove si possono usare congiuntamente le vecchie classi con le nuove, usando classi “ponte”:

  • InputStreamReader converte un InputStream in un Reader;
  • OutputStreamWriter converte un OutputStream in un Writer.

Reader e Writer sono state introdotte per la gestione di caratteri Unicode a 16 bit, mentre le vecchie classi gestivano solo caratteri a 8 bit. Inoltre, le nuove classi sono più performanti.

StreamTokenizer

Java offre una classe di supporto per i flussi chiamata StreamTokenizer (esiste anche un corrispettivo per le stringhe StringTokenizer):

  • non è una derivata di InputStream o OutputStream, e lavora solo con oggetti InputStream.
  • Viene utilizzata per spezzare il flusso di Input in segmenti lessicali chiamati “token“, usando metodi speciali per identificare i parametri di tokenizzazione, come caratteri normali, spazi bianchi etc.

StreamTokenizer

StreamTokenizer definisce quattro metodi costanti:

  • TT_EOF: ovvero fine flusso;
  • TT_EOL: ovvero fine linea;
  • TT_NUMBER: il token è un numero;
  • TT_WORD: il token è una parola.

L’utente può modificare i parametri del tokenizzatore secondo apposite funzioni, si rimanda alla documentazione online della classe per maggiori dettagli.

Esempio di StreamTokenizer

Mostra codice

Istanzio un oggetto BufferedReader, ovvero un filtro che bufferizza un flusso di Input. In ingresso pongo la conversione in Reader di un InputStream, che è quello canonico dalla tastiera. System.out e System.in sono due flussi, rispettivamente di input e di output. Il flusso così ottenuto è passato al costruttore di StreamTokenizer:

BufferedReader inData = new BufferedReader(
new InputStreamReader(System.in));
StreamTokenizer st = new StreamTokenizer(inData);

Segnalo al Tokenizzatore che i caratteri ‘.' e ‘-' non devono essere presi in esame durante l'analisi del flusso:

st.ordinaryChar('.');
st.ordinaryChar('-');

Entro in un ciclo while in cui: determino il token corrente; valuto che tale token è il carattere di fine flusso e se sono al fine flusso esco dal ciclo.

while (st.nextToken() != StreamTokenizer.TT_EOF) {

Esempio di StreamTokenizer

Mostra codice

Valuto all'interno del ciclo switch il tipo di token: carattere di fine riga, numero, parola o ignoto:
String s;
switch(st.ttype) {
case StreamTokenizer.TT_EOL:
s = new String("EOL");
break;
case StreamTokenizer.TT_NUMBER:
s = Double.toString(st.nval);
break;
case StreamTokenizer.TT_WORD:
s = st.sval;
break;
default:
s = String.valueOf((char)st.ttype); }

Prendo dal Tokenizer una rappresentazione numerica del token e lo converto da String a double:

s = Double.toString(st.nval);

Esempio di StreamTokenizer

Mostra codice

Prendo dal Tokenizer una rappresentazione a stringa del token e assegno a s:

s = st.sval;

Prendo dal Tokenizer il valore di tipo di token, opero il casting in char, e lo assegno a un oggetto di tipo String:

s = String.valueOf((char)st.ttype); }

Chiudo i flussi

inStream.close();
inData.close();}}

Riepilogo

Gestione delle operazioni di IO in Java. Sono stati trattati i seguenti argomenti:

  • astrazione di flusso;
  • gerarchia di classi di InputStream e OutputStream in Java 1.0 e corrispettivi Reader e Writer in Java 1.1;
  • operazioni avanzate di IO per mezzo di filtri;
  • realizzazione IO su File;
  • tokenizzazione di un flusso.

I materiali di supporto della lezione

Bruce Eckel, “Thinking in Java” capitolo 12

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion