Input/Output su stream

Abbiamo visto che Java fornisce classi per accedere a dati memorizzati in un file in formato

  • testo (sequenza di caratteri, leggibile da esseri umani)

  • binario (sequenza di byte)

Classi per I/O su files Input da file Output su file
Formato testo FileReader FileWriter
Formato binario FileInputStream FileOutputStream

Più in generale, Java fornisce nel package java.io  [locale, Medialab, Sun] classi e metodi per scrivere e leggere dati che possono risiedere ovunque: in un file, su disco, da qualche parte nella rete, in memoria, o in un altro programma.

Per leggere dati da una sorgente, un programma "apre uno stream" (cioè una sequenza di dati) associata alla sorgente, e legge in modo sequenziale i dati:

Analogamente, un programma può inviare dati ad una destinazione esterna aprendo uno stream associato ad essa, e scrivendoci i dati in modo sequenziale:




Stream per accesso basico

Le seguenti classi definiscono stream di varia natura, fornendo le primitive read() e write() per leggere o scrivere caratteri o byte.

Sink Type Character Streams Byte Streams
Memory CharArrayReader,
CharArrayWriter
ByteArrayInputStream,
ByteArrayOutputStream
StringReader,
StringWriter
StringBufferInputStream
Pipe PipedReader,
PipedWriter
PipedInputStream,
PipedOutputStream
File FileReader,
FileWriter
FileInputStream,
FileOutputStream




Processing Streams

Java fornisce molte altre classi che consentono un accesso più ad alto livello a dati esterni. Ne abbiamo già viste alcune per scrivere e leggere dati strutturati da file.

Queste classi vengono usate come wrapper di stream basici, e forniscono le funzionalità aggiuntive in modo indipendente dal tipo di stream basico cui vengono associate. Tuttavia sono diverse per stream di caratteri o di byte.

ProcessCharacterStreamsByte Streams
Buffering BufferedReader,
BufferedWriter
BufferedInputStream,
BufferedOutputStream
Filtering FilterReader,
FilterWriter
FilterInputStream,
FilterOutputStream
Converting between
Bytes and Characters
InputStreamReader,
OutputStreamWriter
 
Concatenation   SequenceInputStream
Object Serialization   ObjectInputStream,
ObjectOutputStream
Data Conversion   DataInputStream,
DataOutputStream
Counting LineNumberReader LineNumberInputStream
Peeking Ahead PushbackReader PushbackInputStream
Printing PrintWriter PrintStream




La serializzazione di oggetti

Le classi ObjectInputStream e ObjectOutputStream definiscono streams (basati su streams di byte) su cui si possono leggere e scrivere oggetti.

La scrittura e la lettura di oggetti va sotto il nome di object serialization, poiché si basa sulla possibilità di scrivere lo stato di un oggetto in una forma sequenziale, sufficiente per ricostruire l'oggetto quando viene riletto.

La serializzazione di oggetti viene usata principalmente in due modi:

  • Nel contesto di invocazione remota di metodi (Remote Method Invocation -- RMI) in cui due oggetti, che possono essere su macchine diverse, comunicano attraverso sockets e possono scambiarsi oggetti durante la comunicazione.
    Questo sarà un argomento del Laboratorio di Programmazione Distribuita (I semestre, III anno).

  • Per fornire un meccanismo di persistenza ai programmi, consentendo l'archiviazione di un oggetto per poi riutilizzarlo in una successiva invocazione dello stesso programma. Si pensi ad esempio ad un programma che realizza una rubrica telefonica o un'agenda.

Per utilizzare correttamente questo meccanismo occorre sapere:

  • come si serializza un oggetto scrivendolo su di un ObjectOutputStream, e come lo si rilegge usando un ObjectInputStream;

  • come si scrive una classe in modo che le sue istanze possano essere serializzate.




Come si serializzano gli oggetti

Il seguente esempio mostra come si scrivono alcuni oggetti su di un ObjectOutputStream che incapsula il file (binario) theTime. L'oggetto new Date() rappresenta la data corrente (con precisione in millisecondi).

import java.io.*;
import java.util.Date;

public class WriteValues{
    public static void main (String [] args) throws IOException{   
        FileOutputStream out = new FileOutputStream("theTime");
        ObjectOutputStream s = new ObjectOutputStream(out);
        s.writeObject("Today");
        s.writeObject(new Date());
        s.writeObject(4);
        s.writeObject(3.14);
        s.writeObject(true);
        s.writeObject('k');
        s.flush();
    }
}

Se si scrive in un ObjectOutputStream con il metodo writeObject un oggetto che ha puntatori ad altri oggetti, allora tutti gli oggetti raggiungibili, sia direttamente che transitivamente, vengono scritti nello stream, in modo da mantenere le relazioni tra di essi.

Il metodo writeObject lancia un'eccezione NotSerializableException se cerca di serializzare un oggetto che non implementa l'interfaccia Serializable.


Per leggere degli oggetti da uno stream, si usa in modo simmetrico la classe ObjectInputStream. Il prossimo esempio legge dal file chiamato theTime gli oggetti che erano stati scritti dal programma WriteValues:

import java.io.*;
import java.util.Date;

public class ReadValues{
    public static void main (String [] args) 
                  throws IOExceptionClassNotFoundException{   
        FileInputStream in = new FileInputStream("theTime");
        ObjectInputStream s = new ObjectInputStream(in);
        String today = (String) s.readObject();
        Date date = (Date) s.readObject();
        int four = (Integer) s.readObject();
        double pi = (Double) s.readObject();
        boolean b = (Boolean) s.readObject();
        char c = (Character) s.readObject();
        System.out.println(today + " " + date);
        System.out.println(four + ", " + pi + ", " + b + ", " + c);
    }
}

Naturalmente gli oggetti devono essere riletti nell'ordine in cui erano stati scritti. Si noti che il tipo di readObject è Object e quindi serve un cast. Il metodo può lanciare l'eccezione controllata ClassNotFoundException.




Scrittura di classi che consentono la serializzazione

Un oggetto è serializzabile solo se la sua classe implementa l'interfaccia Serializable. Quindi se si vuole che le istanze di una classe che state scrivendo siano serializzabili, è sufficiente dichiarare che la classe implementa Serializable. Poiché questa intefaccia non ha metodi, non occorre fare altro.

La serializzazione delle istanze di una classe viene gestita dal metodo defaultWriteObject della classe ObjectOutputStream. Questo metodo scrive automaticamente tutto ciò che è richiesto per ricostruire le istanze di una classe, e cioè

  • la classe dell'oggetto;

  • la firma della classe;

  • I valori di tutte le variabili che non siano transient o static.

Per molte classi questo comportamento è soddisfacente. A volte, tuttavia, il programmatore può volere un maggior controllo su questo meccanismo. Si può personalizzare la serializzazione per proprie classi fornendo due metodi d'istanza: writeObject e readObject. Si rimanda alla documentazione di Java per ulteriori dettagli.

Per concludere, si osservi che la serializzazione di oggetti di una classe può causare problemi di sicurezza, poiché informazioni private o sensibili possono essere accedute da altri.

Per evitare ciò ci sono due modi semplici:

  • Non consentire la serializzazione degli oggetti di una classe, semplicemente non dichiarando che la classe implementa Serializable;

  • Dichiarare variabili d'istanza che non si desidera serializzare come transient.