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 WriteObjects1{
    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.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.

Oltre al metodo writeObject(), la classe ObjectOutputStream fornisce metodi per scrivere elementi dei tipi di dati primitivi, come writeInt, writeFloat, writeChar eccetera.

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 di tipo String e Date che erano stati scritti dal programma WriteObject1:

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

public class ReadObjects1{
    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();
        System.out.println(today);
        System.out.println(date);
    }
}

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.

La classe ObjectInputStream ha naturalmente anche metodi per leggere elementi di tipi di dati elementari, come readInt, readFloat, e readChar.




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, compreso le variabili che contegono riferimenti ad altri oggetti.

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.

Il metodo writeObject controlla come l'oggetto deve essere salvato, ed è usato tipicamente per inserire informazioni aggiuntive allo stream. Il metodo readObject di solito o legge le informazioni scritte dal corrispondente metodo writeObject, oppure può essere usato per aggiornare lo stato dell'oggetto dopo che è stato ricostruito.

Il metodo writeObject deve essere scritto nel modo sequente: deve invocare come prima cosa il metodo defaultWriteObject dello stream per effettuare la serializzazione default:

private void writeObject(ObjectOutputStream s)
                         throws IOException {
    s.defaultWriteObject();
    // codice personalizzato per la serializzazione
}

Il metodo readObject deve leggere tutto ciò che è stato scritto dal metodo writeObject, nello stesso ordine in cui era stato scritto. Inoltre, il metodo readObject può effettuare ulteriori elaborazioni o aggiornare lo stato dell'oggetto in qualche modo. Questo è l'aspetto tipico di un metodo readObject che corrisponde al metodo writeObject visto sopra:

private void readObject(ObjectInputStream s)
                        throws IOException  {
    s.defaultReadObject();
    // codice personalizzato per la deserializzazione
    ...
    // seguito dal codice per aggiornare l'oggetto, se necessario
}

I metodi writeObject e readObject sono responsabili solo per la serializzazione della classe in cui sono definiti. Ogni eventuale serializzazione richiesta dalla superclasse è gestita automaticamente. Java fornisce comunque un meccanismo per coordinare la serializzazione di una classe con quella della superclasse, utilizzando l'interfaccia Externalizable (che non vedremo).

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 (o Externalizable);

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