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 write sull'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) e File.charSeparator.
Path separator: i campi static File.pathSeparator (come String) e File.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.
public File(String pathString)
public File(String parent, String child)
public File(File parent, String child)
// Constructs a File instance based on the given path string.
public File(URI uri)
// Constructs a File instance by converting from the given file-URI "file://...."
per esempio:
File file = new File("in.txt"); // A file relative to the current working directory
File file = new File("d:\\myproject\\java\\Hello.java"); // A file with absolute path
File dir = new File("c:\\temp"); // A director
Verifica delle proprietà di un file o una directory
public boolean exists() // Tests if this file/directory exists.
public long length() // Returns the length of this file.
public boolean isDirectory() // Tests if this instance is a directory.
public boolean isFile() // Tests if this instance is a file.
public boolean canRead() // Tests if this file is readable.
public boolean canWrite() // Tests if this file is writable.
public boolean delete() // Deletes this file/directory.
public void deleteOnExit() // Deletes this file/directory when the program terminates.
public boolean renameTo(File dest) // Renames this file.
public boolean mkdir() // Makes (Creates) this directory.
List directory
public String[] list() // List the contents of this directory in a String-array
public File[] listFiles() // List the contents of this directory in a File-array
L'esempio sotto lista il contenuto di una certa directory (simile la comando UNIX "ls -r")
// Recursively list the contents of a directory (Unix's "ls -r" command).
import java.io.File;
public class ListDirectoryRecusive {
public static void main(String[] args) {
File dir = new File("d:\\myproject\\test"); // Escape sequence needed for '\'
listRecursive(dir);
}
public static void listRecursive(File dir) {
if (dir.isDirectory()) {
File[] items = dir.listFiles();
for (File item : items) {
System.out.println(item.getAbsoluteFile());
if (item.isDirectory()) listRecursive(item); // Recursive call
}
}
}
}
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:
public abstract int read() throws IOException
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 IOException se 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.
public int read(byte[] bytes, int offset, int length) throws IOException
// Read "length" number of bytes, store in bytes array starting from offset of index.
public int read(byte[] bytes) throws IOException
// Same as read(bytes, 0, bytes.length)
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).
public void abstract void write(int unsignedByte) throws IOException
Come per la read(), due varianti di write() per scrivere blocchi di byte da un array di byte sono disponibili:
public void write(byte[] bytes, int offset, int length) throws IOException
// Write "length" number of bytes, from the bytes array starting from offset of index.
public void write(byte[] bytes) throws IOException
// Same as write(bytes, 0, bytes.length)
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.
public void close() throws IOException // close this Stream
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:
FileInputStream in = null;
......
try {
in = new FileInputStream(...); // Open stream
......
......
} catch (IOException ex) {
ex.printStackTrace();
} finally { // always close the I/O streams
try {
if (in != null) in.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
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:
try (FileInputStream in = new FileInputStream(...)) {
......
......
} catch (IOException ex) {
ex.printStackTrace();
} // Automatically closes all opened resource in try (...).
Flushing dell'OutputStream
In aggiunta la classe OutputStream provvede il metodo flush() per scaricare i rimanenti byte dal buffer di output:
public void flush() throws IOException // Flush the 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:
FileInputStream fileIn = new FileInputStream("in.dat");
BufferedInputStream bufferIn = new BufferedInputStream(fileIn);
DataInputStream dataIn = new DataInputStream(bufferIn);
// or
DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("in.dat")));
Esempio copia di file:
import java.io.*;
public class FileCopyBufferedStream { // Pre-JDK 7
public static void main(String[] args) {
String inFileStr = "test-in.jpg";
String outFileStr = "test-out.jpg";
BufferedInputStream in = null;
BufferedOutputStream out = null;
long startTime, elapsedTime; // for speed benchmarking
// Check file length
File fileIn = new File(inFileStr);
System.out.println("File size is " + fileIn.length() + " bytes");
try {
in = new BufferedInputStream(new FileInputStream(inFileStr));
out = new BufferedOutputStream(new FileOutputStream(outFileStr));
startTime = System.nanoTime();
int byteRead;
while ((byteRead = in.read()) != -1) { // Read byte-by-byte from buffer
out.write(byteRead);
}
elapsedTime = System.nanoTime() - startTime;
System.out.println("Elapsed Time is " + (elapsedTime / 1000000.0) + " msec");
} catch (IOException ex) {
ex.printStackTrace();
} finally { // always close the streams
try {
if (in != null) in.close();
if (out != null) out.close();
} catch (IOException ex) { ex.printStackTrace(); }
}
}
}
La versione, dello stesso programma, con try-catch-with-resource (da JDK 1.7):
import java.io.*;
public class FileCopyBufferedStreamJDK7 {
public static void main(String[] args) {
String inFileStr = "test-in.jpg";
String outFileStr = "test-out.jpg";
long startTime, elapsedTime; // for speed benchmarking
// Check file length
File fileIn = new File(inFileStr);
System.out.println("File size is " + fileIn.length() + " bytes");
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(inFileStr));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(outFileStr))) {
startTime = System.nanoTime();
int byteRead;
while ((byteRead = in.read()) != -1) {
out.write(byteRead);
}
elapsedTime = System.nanoTime() - startTime;
System.out.println("Elapsed Time is " + (elapsedTime / 1000000.0) + " msec");
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
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 in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("in.dat")));
DataInputStream implementa l'interfaccia DataInput, che provvede metodi per leggere tipi primitivi e String:
// 8 Primitives
public final int readInt() throws IOExcpetion; // Read 4 bytes and convert into int
public final double readDoube() throws IOExcpetion; // Read 8 bytes and convert into double
public final byte readByte() throws IOExcpetion;
public final char readChar() throws IOExcpetion;
public final short readShort() throws IOExcpetion;
public final long readLong() throws IOExcpetion;
public final boolean readBoolean() throws IOExcpetion; // Read 1 byte. Convert to false if zero
public final float readFloat() throws IOExcpetion;
public final int readUnsignedByte() throws IOExcpetion; // Read 1 byte in [0, 255] upcast to int
public final int readUnsignedShort() throws IOExcpetion; // Read 2 bytes in [0, 65535], same as char, upcast to int
public final void readFully(byte[] b, int off, int len) throws IOException;
public final void readFully(byte[] b) throws IOException;
// Strings
public final String readLine() throws IOException;
// Read a line (until newline), convert each byte into a char - no unicode support.
public final String readUTF() throws IOException;
// read a UTF-encoded string with first two bytes indicating its UTF bytes length
public final int skipBytes(int n) // Skip a number of bytes
Similmente, si può concatenare DataOutputStream in questo modo:
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("out.dat")));
DataOutputStream implementa l'interfaccia DataOutput, che provvede metodi per scrivere tipi primitivi e String:
// 8 primitive types
public final void writeInt(int i) throws IOExcpetion; // Write the int as 4 bytes
public final void writeFloat(float f) throws IOExcpetion;
public final void writeDoube(double d) throws IOExcpetion; // Write the double as 8 bytes
public final void writeByte(int b) throws IOExcpetion; // least-significant byte
public final void writeShort(int s) throws IOExcpetion; // two lower bytes
public final void writeLong(long l) throws IOExcpetion;
public final void writeBoolean(boolean b) throws IOExcpetion;
public final void writeChar(int i) throws IOExcpetion;
// String
public final void writeBytes(String str) throws IOExcpetion;
// least-significant byte of each char
public final void writeChars(String str) throws IOExcpetion;
// Write String as UCS-2 16-bit char, Big-endian (big byte first)
public final void writeUTF(String str) throws IOException;
// Write String as UTF, with first two bytes indicating UTF bytes length
public final void write(byte[] b, int off, int len) throws IOException
public final void write(byte[] b) throws IOException
public final void write(int b) throws IOException // Write the least-significant byte
import java.io.*;
public class TestDataIOStream {
public static void main(String[] args) {
String filename = "data-out.dat";
String message = "Hi,您好!";
// Write primitives to an output file
try (DataOutputStream out =
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(filename)))) {
out.writeByte(127);
out.writeShort(0xFFFF); // -1
out.writeInt(0xABCD);
out.writeLong(0x1234_5678); // JDK 7 syntax
out.writeFloat(11.22f);
out.writeDouble(55.66);
out.writeBoolean(true);
out.writeBoolean(false);
for (int i = 0; i < message.length(); ++i) {
out.writeChar(message.charAt(i));
}
out.writeChars(message);
out.writeBytes(message);
out.flush();
} catch (IOException ex) {
ex.printStackTrace();
}
// Read raw bytes and print in Hex
try (BufferedInputStream in =
new BufferedInputStream(
new FileInputStream(filename))) {
int inByte;
while ((inByte = in.read()) != -1) {
System.out.printf("%02X ", inByte); // Print Hex codes
}
System.out.printf("%n%n");
} catch (IOException ex) {
ex.printStackTrace();
}
// Read primitives
try (DataInputStream in =
new DataInputStream(
new BufferedInputStream(
new FileInputStream(filename)))) {
System.out.println("byte: " + in.readByte());
System.out.println("short: " + in.readShort());
System.out.println("int: " + in.readInt());
System.out.println("long: " + in.readLong());
System.out.println("float: " + in.readFloat());
System.out.println("double: " + in.readDouble());
System.out.println("boolean: " + in.readBoolean());
System.out.println("boolean: " + in.readBoolean());
System.out.print("char: ");
for (int i = 0; i < message.length(); ++i) {
System.out.print(in.readChar());
}
System.out.println();
System.out.print("chars: ");
for (int i = 0; i < message.length(); ++i) {
System.out.print(in.readChar());
}
System.out.println();
System.out.print("bytes: ");
for (int i = 0; i < message.length(); ++i) {
System.out.print((char)in.readByte());
}
System.out.println();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
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.
public abstract int read() throws IOException
public int read(char[] chars, int offset, int length) throws IOException
public int read(char[] chars) throws IOException
public void abstract void write(int aChar) throws IOException
public void write(char[] chars, int offset, int length) throws IOException
public void write(char[] chars) throws IOException
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.
import java.io.*;
// Write a text message to an output file, then read it back.
// FileReader/FileWriter uses the default charset for file encoding.
public class BufferedFileReaderWriterJDK7 {
public static void main(String[] args) {
String strFilename = "out.txt";
String message = "Hello, world!\nHello, world again!\n"; // 2 lines of texts
// Print the default charset
System.out.println(java.nio.charset.Charset.defaultCharset());
try (BufferedWriter out = new BufferedWriter(new FileWriter(strFilename))) {
out.write(message);
out.flush();
} catch (IOException ex) {
ex.printStackTrace();
}
try (BufferedReader in = new BufferedReader(new FileReader(strFilename))) {
String inLine;
while ((inLine = in.readLine()) != null) { // exclude newline
System.out.println(inLine);
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
Text File I/O - InputStreamReader and OutputStreamWriter
Si può selezionare il character set tramite il costruttore dell'InputStream.
public InputStreamReader(InputStream in) // Use default charset
public InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException
public InputStreamReader(InputStream in, Charset cs)
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:
PrintWriter printableCharSink = new PrintWriter(charSink);
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 PrintWriterprintableCharSink, 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 metodo toString() 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:
public boolean checkError()
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:
public final Object readObject() throws IOException, ClassNotFoundException;
public final void writeObject(Object obj) throws IOException;
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:
ObjectOutputStream out =
new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream("object.ser")));
out.writeObject("The current Date and Time is "); // write a String object
out.writeObject(new Date()); // write a Date object
out.flush();
out.close();
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:
ObjectInputStream in =
new ObjectInputStream(
new BufferedInputStream(
new FileInputStream("object.ser")));
String str = (String)in.readObject();
Date d = (Date)in.readObject(); // downcast
in.close();
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:
import java.io.*;
public class ObjectSerializationTest {
public static void main(String[] args) {
String filename = "object.ser";
int numObjs = 5;
// Write objects
try (ObjectOutputStream out =
new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream(filename)))) {
// Create an array of 10 MySerializedObjects with ascending numbers
MySerializedObject[] objs = new MySerializedObject[numObjs];
for (int i = 0; i < numObjs; ++i) {
objs[i] = new MySerializedObject(0xAA + i); // Starting at AA
}
// Write the objects to file, one by one.
for (int i = 0; i < numObjs; ++i) {
out.writeObject(objs[i]);
}
// Write the entire array in one go.
out.writeObject(objs);
out.flush();
} catch (IOException ex) {
ex.printStackTrace();
}
// Read raws bytes and print in Hex
try (BufferedInputStream in =
new BufferedInputStream(
new FileInputStream(filename))) {
int inByte;
while ((inByte = in.read()) != -1) {
System.out.printf("%02X ", inByte); // Print Hex codes
}
System.out.printf("%n%n");
} catch (IOException ex) {
ex.printStackTrace();
}
// Read objects
try (ObjectInputStream in =
new ObjectInputStream(
new BufferedInputStream(
new FileInputStream(filename)))) {
// Read back the objects, cast back to its original type.
MySerializedObject objIn;
for (int i = 0; i < numObjs; ++i) {
objIn = (MySerializedObject)in.readObject();
System.out.println(objIn.getNumber());
}
MySerializedObject[] objArrayIn;
objArrayIn = (MySerializedObject[])in.readObject();
for (MySerializedObject o : objArrayIn) {
System.out.println(o.getNumber());
}
} catch (ClassNotFoundException|IOException ex) { // JDK 7
ex.printStackTrace();
}
}
}
class MySerializedObject implements Serializable {
private int number;
public MySerializedObject(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
}
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:
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Deserialize the component in the usual way.
this.computePreferredSize(); // But then go recompute its size.
}
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.
/*
* Copyright (c) 2000 David Flanagan. All rights reserved.
* This code is from the book Java Examples in a Nutshell, 2nd Edition.
* It is provided AS-IS, WITHOUT ANY WARRANTY either expressed or implied.
* You may study, use, and modify it for any non-commercial purpose.
* You may distribute it non-commercially as long as you retain this notice.
* For a commercial use license, or to purchase the book (recommended),
* visit http://www.davidflanagan.com/javaexamples2.
*/
import java.io.*;
import java.util.Arrays;
/**
* A simple class that implements a growable array of ints, and knows how to
* serialize itself as efficiently as a non-growable array.
**/
public class IntList implements Serializable {
protected int[] data = new int[8]; // An array to store the numbers.
protected transient int size = 0; // Index of next unused element of array
/** Return an element of the array */
public int get(int index) throws ArrayIndexOutOfBoundsException {
if (index >= size)
throw new ArrayIndexOutOfBoundsException(index);
else
return data[index];
}
/** Add an int to the array, growing the array if necessary */
public void add(int x) {
if (data.length == size)
resize(data.length * 2); // Grow array if needed.
data[size++] = x; // Store the int in it.
}
/** An internal method to change the allocated size of the array */
protected void resize(int newsize) {
// int[] newdata = new int[newsize]; // Create a new array
// System.arraycopy(data, 0, newdata, 0, size); // Copy array elements.
int[] newdata = Arrays.copyOf(data, newsize);
data = newdata; // Replace old array
}
/** Get rid of unused array elements before serializing the array */
private void writeObject(ObjectOutputStream out) throws IOException {
if (data.length > size)
resize(size); // Compact the array.
out.defaultWriteObject(); // Then write it out normally.
}
/** Compute the transient size field after deserializing the array */
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Read the array normally.
size = data.length; // Restore the transient field.
}
}
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.
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:
import java.io.*;
/**
* This subclass of IntList assumes that most of the integers it contains are
* less than 32,000. It implements Externalizable so that it can define a
* compact serialization format that takes advantage of this fact.
**/
public class CompactIntList extends IntList implements Externalizable {
/**
* This version number is here in case a later revision of this class wants to
* modify the externalization format, but still retain compatibility with
* externalized objects written by this version
**/
static final byte version = 1;
/**
* This method from the Externalizable interface is responsible for saving the
* complete state of the object to the specified stream. It can write anything
* it wants as long as readExternal() can read it.
**/
public void writeExternal(ObjectOutput out) throws IOException {
if (data.length > size)
resize(size); // Compact the array.
out.writeByte(version); // Start with our version number.
out.writeInt(size); // Output the number of array elements
for (int i = 0; i < size; i++) { // Now loop through the array
int n = data[i]; // The array element to write
if ((n < Short.MAX_VALUE) && (n > Short.MIN_VALUE + 1)) {
// If n fits in a short and is not Short.MIN_VALUE, then write
// it out as a short, saving ourselves two bytes
out.writeShort(n);
} else {
// Otherwise write out the special value Short.MIN_VALUE to
// signal that the number does not fit in a short, and then
// output the number using a full 4 bytes, for 6 bytes total
out.writeShort(Short.MIN_VALUE);
out.writeInt(n);
}
}
}
/**
* This Externalizable method is responsible for completely restoring the state
* of the object. A no-arg constructor will be called to re-create the object,
* and this method must read the state written by writeExternal() to restore the
* object's state.
**/
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// Start by reading and verifying the version number.
byte v = in.readByte();
if (v != version)
throw new IOException("CompactIntList: unknown version number");
// Read the number of array elements, and make array that big
int newsize = in.readInt();
resize(newsize);
this.size = newsize;
// Now read that many values from the stream
for (int i = 0; i < newsize; i++) {
short n = in.readShort();
if (n != Short.MIN_VALUE)
data[i] = n;
else
data[i] = in.readInt();
}
}
}
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.
RandomAccessFile f1 = new RandomAccessFile("filename", "r");
RandomAccessFile f2 = new RandomAccessFile("filename", "rw");
i seguenti metodi sono disponibili:
public void seek(long pos) throws IOException;
// Positions the file pointer for subsequent read/write operation.
public int skipBytes(int numBytes) throws IOException;
// Moves the file pointer forward by the specified number of bytes.
public long getFilePointer() throws IOException;
// Gets the position of the current file pointer, in bytes, from the beginning of the file.
public long length() throws IOException;
// Returns the length of this file.
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:
public int readInt() throws IOException;
public double readDouble() throws IOException;
public void writeInt(int i) throws IOException;
public void writeDouble(double d) throws IOException;
Esempio:
This is a file with few random words.Hello World!Hello Universe!Hello again!Welcome!
ThisHello file with few random words.Hello World!Hello Universe!Hello again!Welcome!
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.
// This example is from _Java Examples in a Nutshell_. (http://www.oreilly.com)
// Copyright (c) 1997 by David Flanagan
// This example is provided WITHOUT ANY WARRANTY either expressed or implied.
// You may study, use, modify, and distribute it for non-commercial purposes.
// For any commercial use, see http://www.davidflanagan.com/javaexamples
import java.io.*;
import java.util.zip.*;
/**
* This class defines two static methods for gzipping files and zipping
* directories. It also defines a demonstration program as a nested class.
**/
public class Compress {
/** Gzip the contents of the from file and save in the to file. */
public static void gzipFile(String from, String to) throws IOException {
// Create stream to read from the from file
FileInputStream in = new FileInputStream(from);
// Create stream to compress data and write it to the to file.
GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(to));
// Copy bytes from one stream to the other
byte[] buffer = new byte[4096];
int bytes_read;
while ((bytes_read = in.read(buffer)) != -1)
out.write(buffer, 0, bytes_read);
// And close the streams
in.close();
out.close();
}
/** Zip the contents of the directory, and save it in the zipfile */
public static void zipDirectory(String dir, String zipfile) throws IOException, IllegalArgumentException {
// Check that the directory is a directory, and get its contents
File d = new File(dir);
if (!d.isDirectory())
throw new IllegalArgumentException("Compress: not a directory: " + dir);
String[] entries = d.list();
byte[] buffer = new byte[4096]; // Create a buffer for copying
int bytes_read;
// Create a stream to compress data and write it to the zipfile
ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipfile));
// Loop through all entries in the directory
for (int i = 0; i < entries.length; i++) {
File f = new File(d, entries[i]);
if (f.isDirectory())
continue; // Don't zip sub-directories
FileInputStream in = new FileInputStream(f); // Stream to read file
ZipEntry entry = new ZipEntry(f.getPath()); // Make a ZipEntry
out.putNextEntry(entry); // Store entry in zipfile
while ((bytes_read = in.read(buffer)) != -1) // Copy bytes to zipfile
out.write(buffer, 0, bytes_read);
in.close(); // Close input stream
}
// When we're done with the whole loop, close the output stream
out.close();
}
}
La classe AppCompress per l'esecuzione dell'applicazione:
import java.io.File;
import java.io.IOException;
public class AppCompress {
/**
* Compress a specified file or directory. If no destination name is specified,
* append .gz to a file name or .zip to a directory name
**/
public static void main(String args[]) throws IOException {
if ((args.length != 1) && (args.length != 2)) { // check arguments
System.err.println("Usage: java AppCompress <from> [<to>]");
System.exit(0);
}
String from = args[0], to;
File f = new File(from);
boolean directory = f.isDirectory(); // Is it a file or directory?
if (args.length == 2)
to = args[1];
else { // If destination not specified
if (directory)
to = from + ".zip"; // use a .zip suffix
else
to = from + ".gz"; // or a .gz suffix
}
if ((new File(to)).exists()) { // Make sure not to overwrite anything
System.err.println("Compress: won't overwrite existing file: " + to);
System.exit(0);
}
// Finally, call one of the methods defined above to do the work.
if (directory)
Compress.zipDirectory(from, to);
else
Compress.gzipFile(from, to);
}
}
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.
// This example is from _Java Examples in a Nutshell_. (http://www.oreilly.com)
// Copyright (c) 1997 by David Flanagan
// This example is provided WITHOUT ANY WARRANTY either expressed or implied.
// You may study, use, modify, and distribute it for non-commercial purposes.
// For any commercial use, see http://www.davidflanagan.com/javaexamples
import java.io.*;
/**
* A simple FilterReader that strips HTML tags out of a stream of characters.
* It isn't perfect: it doesn't know about <XMP> tags, for example, within
* which '<' and '>' aren't to be interpreted as tags. It will also strip
* '<' and '>' characters (and anything in between) out of plain text files.
* For this reason, it should only be used with properly formatted HTML input.
**/
public class RemoveHTMLReader extends FilterReader {
/** A trivial constructor. Just initialze our superclass */
public RemoveHTMLReader(Reader in) { super(in); }
boolean intag = false; // Used to remember whether we are "inside" a tag
/**
* This is the implementation of the no-op read() method of FilterReader.
* It calls in.read() to get a buffer full of characters, then strips
* out the HTML tags. (in is a protected field of the superclass).
**/
public int read(char[] buf, int from, int len) throws IOException {
int numchars = 0; // how many characters have been read
// Loop, because we might read a bunch of characters, then strip them
// all out, leaving us with zero characters to return.
while (numchars == 0) {
numchars = in.read(buf, from, len); // Read characters
if (numchars == -1) return -1; // Check for EOF and handle it.
// Loop through the characters we read, stripping out HTML tags.
// Characters not in tags are copied over any previous tags in the buffer
int last = from; // Index of last non-HTML char
for(int i = from; i < from + numchars; i++) {
if (!intag) { // If not in an HTML tag
if (buf[i] == '<') intag = true; // check for start of a tag
else buf[last++] = buf[i]; // and copy the character
}
else if (buf[i] == '>') intag = false; // Else, check for end of tag
}
numchars = last - from; // Figure out how many characters remain
} // And if it is more than zero characters
return numchars; // Then return that number.
}
/**
* This is another no-op read() method we have to implement. We
* implement it in terms of the method above. Our superclass implements
* the remaining read() methods in terms of these two.
**/
public int read() throws IOException {
char[] buf = new char[1];
int result = read(buf, 0, 1);
if (result == -1) return -1;
else return (int)buf[0];
}
}
La classe, AppRemoveHTMLReader, per l'esecuzione dell'applicazione:
import java.io.BufferedReader;
import java.io.FileReader;
/** This class defines a main() method to test the RemoveHTMLReader */
public class AppRemoveHTMLReader {
/** The test program: read a text file, strip HTML, print to console */
public static void main(String[] args) {
try {
if (args.length != 1)
throw new IllegalArgumentException("Wrong number of arguments");
// Create a stream to read from the file and strip tags from it
BufferedReader in = new BufferedReader(new RemoveHTMLReader(new FileReader(args[0])));
// Read line by line, printing lines to the console
String line;
while ((line = in.readLine()) != null)
System.out.println(line);
in.close(); // Close the stream.
} catch (Exception e) {
System.err.println(e);
System.err.println("Usage: java AppRemoveHTMLReader <filename>");
}
}
}
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.
// This example is from _Java Examples in a Nutshell_. (http://www.oreilly.com)
// Copyright (c) 1997 by David Flanagan
// This example is provided WITHOUT ANY WARRANTY either expressed or implied.
// You may study, use, modify, and distribute it for non-commercial purposes.
// For any commercial use, see http://www.davidflanagan.com/javaexamples
import java.io.*;
/**
* This class is a BufferedReader that filters out all lines that
* do not contain the specified pattern.
**/
public class GrepReader extends BufferedReader {
String pattern; // The string we are going to be matching.
/** Pass the stream to our superclass, and remember the pattern ourself */
public GrepReader(Reader in, String pattern) {
super(in);
this.pattern = pattern;
}
/**
* This is the filter: call our superclass's readLine() to get the
* actual lines, but only return lines that contain the pattern.
* When the superclass readLine() returns null (EOF), we return null.
**/
public final String readLine() throws IOException {
String line;
do { line = super.readLine(); }
while ((line != null) && line.indexOf(pattern) == -1);
return line;
}
}
La classe AppGrepReader, per l'esecuzione dell'applicazione:
import java.io.FileReader;
/**
* This class demonstrates the use of the GrepReader class. It prints the lines
* of a file that contain a specified substring.
**/
public class AppGrepReader {
public static void main(String args[]) {
try {
if (args.length != 2)
throw new IllegalArgumentException("Wrong number of arguments");
GrepReader in = new GrepReader(new FileReader(args[1]), args[0]);
String line;
while ((line = in.readLine()) != null)
System.out.println(line);
in.close();
} catch (Exception e) {
System.err.println(e);
System.out.println("Usage: java AppGrepReader <pattern> <file>");
}
}
}
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).