IL PACKAGE JAVA.IO Il package java.io fornisce le classi necessarie per effettuare input e output su file/rete/console/aree di memoria RAM eccetera. Il package può essere classificato secondo vari punti di vista: • modalità di accesso alle risorse: o sequenziale: si può scrivere o leggere come su un nastro, non vi è accesso diretto; o random: si può saltare da un punto all’altro della risorsa in modo completamente libero. • codifica dell’accesso: o testuale: caratteri UNICODE o binaria • tipo di accesso: o lettura o scrittura • operazioni effettuate durante l’accesso o pura lettura e scrittura o trasformazione dati Ci concentreremo sull’accesso sequenziale, poiché è l’unico che può essere effettuato indipendentemente dalla natura della risorsa a cui si accede (l’accesso random è di norma consentito soltanto su File tramite la classe RandomAccessFile). Aimei - PACKAGE DI I/O IN JAVA 1 ACCESSO SEQUENZIALE La modalità di accesso sarà sempre del tipo: Lettura Scrittura Apri lo stream Mentre c’e’ nuova informazione Leggi e manipola dati Chiudi lo stream Apri lo strema Mentre c’e’ nuova informazione Scrivi informazione Chiudi lo stream Abbiamo 4 classi base astratte: • InputStream e OutputStream per leggere/scrivere stream di byte (come i file binari del C). (Un byte è soli 8 bit, non è un carattere UNICODE) • Reader e Writer per leggere/scrivere stream di caratteri UNICODE Esempio: se si legge un file di testo come sequenza di byte, Java non riesce a interpretare i byte letti come caratteri. In seguito, tratteremo separatamente stream di byte e di caratteri. Aimei - PACKAGE DI I/O IN JAVA 2 SORGENTI E FILTRI Due tipi di classi: • sorgenti/destinazioni: permettono l’accesso a risorse fisiche, quali ad esempio i file o un buffer di memoria (un array di byte, una stringa). Forniscono poche operazioni di base, ma sanno come accedere alla specifica risorsa; • filtri: dato uno stream qualunque, aggiungono funzionalità. Alcune consentono di leggere gruppi di informazioni, altre di interpretarle, o di comprimerle, o di crittografarle. In questo modo si riduce il numero complessivo di classi che occorre scrivere: ad esempio, si scrive un filtro in grado di crittografare i dati, e questo può funzionare tanto su file, quanto su socket (comunicazione remota) quanto su porte seriali, eccetera. Come si ottiene una funzionalità complessa? Per composizione e incapsulamento. • Si crea una sorgente; • Si crea un filtro per ottenere la funzione desiderata intorno alla sorgente, passandola come parametro al costruttore del filtro • Questo filtro può essere dato in pasto ad un altro filtro, e così via, fino ad ottenere la funzionalità desiderata (analogo ad una Matrioska). BufferedWriter CryptoWriter CompressWriter FileWriter Aimei - PACKAGE DI I/O IN JAVA 3 Esempio: (verrà dettagliato meglio in seguito) import java.io.*; … FileOutputStream f = null; f = new FileOutputStream("Prova.dat"); //creiamo 1 stream di output associato a un file // tramite l'apposita classe "sorgente" DataOutputStream os = new DataOutputStream(f); // lo incapsuliamo in una classe di "filtraggio" // che da' le funzionalità necessarie per // scrivere i tipi primitivi di Java os.writeFloat(f1); os.writeBoolean(b1); os.writeDouble(d1); os.writeChar(c1); GESTIONE ECCEZIONI Quasi tutti i metodi delle classi di I/O lanciano una IOException o una sua sottoclasse. Pertanto, le operazioni di input output devono essere scritte fra try/catch o try/finally, o rilanciare le eccezioni. DataOutputStream dos = null; try { dos = new DataOutputStream( new FileOutputStream("Prova.dat")); os.writeFloat(f1); } catch(IOException e) { e.printStackTrace(); } finally { try { if(dos != null) dos.close() } catch(IOException e) {} } STREAM DI BYTE Aimei - PACKAGE DI I/O IN JAVA 4 Due classi astratte per leggere e scrivere byte, InputStream e OutputStream. Una gerarchia di classi con sorgenti (in grigio) e filtri (in bianco). Aimei - PACKAGE DI I/O IN JAVA 5 Metodi presenti in InputStream: public int read() Legge un byte (ma lo ritorna in un intero, attenzione). Varianti per leggere un array. public long Salta un certo numero di byte, e ritorna skip(long) quanti byte ha saltato public void mark() Marca una posizione per poterci ritornare (se markSupported() ritorna true) public void reset() Ritorna all’ultimo mark Public void close() Chiude lo stream e librera le risorse Metodi presenti in OutputStream: public void write(byte b) Scrive un byte. Varianti per scrivere array di byte public void flush() Se lo stream ritarda la scrittura, fa in modo che le operazioni in sospeso vengano eseguite. Public void close() Chiude lo stream e librera le risorse Classi sorgente per l’input: • ByteArrayInputStream ne implementa i metodi nel caso particolare in cui l’input è un buffer (array) di byte, passato all’atto della costruzione del ByteArrayInputStream; • ne implementa i metodi nel caso particolare in cui l’input è un file, il cui nome è passato al costruttore di FileInputStream; in alternativa si può passare al costruttore un oggetto File (o anche un FileDescriptor). FileInputStream FileInputStream f = null; f = new FileInputStream("Prova.dat"); Aimei - PACKAGE DI I/O IN JAVA 6 Classi filtro per l’input Caratteristica comune: accettano nel costruttore un parametro di tipo InputStream. Il filtro può così agire su un altro filtro, e così via. Esempi: • FilterInputStream modifica il metodo read() (ed eventualmente gli altri) in accordo al criterio di filtraggio richiesto (per default, il filtro è trasparente e non filtra niente). Viene usata per semplificare la codifica delle classi filtro concrete, come BufferedInputStream e DataInputStream. • BufferedInputStream modifica il metodo read() in modo tale da avere un input bufferizzato tramite un buffer che aggiunge egli stesso. Questo riduce il numero di chiamate al sistema operativo e aumenta l’efficienza. • DataInputStream definisce metodi per leggere i tipi primitivi di Java scritti su un file binario, come da esempio per esempio readInteger(), readFloat(). Questi dati possono essere scritti con un DataOutputStream. Classi sorgente nell’output • ByteArrayOutputStream implementa questi metodi nel caso particolare in cui l’output è un buffer (array) di byte interno, dinamicamente espandibile, recuperabile con toByteArray() o toString(), secondo i casi. • FileOutputStream implementa questi metodi nel caso particolare in cui l’output è un file, il cui nome è passato al costruttore di FileOutputStream; in alternativa si può passare al costruttore un oggetto File (o anche un FileDescriptor) Aimei - PACKAGE DI I/O IN JAVA 7 Classi filtro nell’output: • Un FilterOutputStream modifica il metodo write() (ed eventualmente gli altri) in accordo al criterio di filtraggio richiesto (per default, il filtro è trasparente). Nuovamente, è una classe di comodo per scrivere più facilmente i filtri. • PrintStream definisce metodi per stampare sotto forma di String i tipi primitivi e le classi standard (Integer, etc.) di Java (medinte un metodo toString()). System.out è una istanza di PrintStream. La scrittura avviene nell’encoding di default della piattaforma, ovvero, i caratteri scritti possono non essere UNICODE, ma ad esempio ASCII. • DataOutputStream definisce metodi per scrivere in forma binaria i tipi primitivi e le classi standard di Java (writeInteger(), writeFloat(), etc.) • BufferedOutputStream modifica il metodo write() in modo da scrivere tramite un buffer (nuovamente, per questioni prestazionali). Aimei - PACKAGE DI I/O IN JAVA 8 ESEMPIO 1 – I/O DA FILE BINARIO Si vuole scrivere un array di interi di dimensione qualunque su un file, e poi rileggerlo. static void scriviArray(int[] arr, String path) throws IOException { DataOutputStream dos = null; try { dos = new DataOutputStream( new BufferedOutputStream( new FileOutputStream(path))); dos.writeInt(arr.length); for(int i = 0; i < arr.length; i++) dos.writeInt(arr[i]); } finally { if(dos != null) try { dos.close(); } catch(IOException e) {} } } static int[] leggiArray(String path) throws IOException { DataInputStream dis = null; int[] result = null; try { dis = new DataInputStream( new BufferedInputStream( new FileInputStream(path))); int arrLength = dis.readInt(); result = new int[arrLength]; for(int i = 0; i < result.length; i++) result[i] = dis.readInt(); } finally { if(dis != null) try { dis.close(); } catch(IOException e) {} } return result; } Aimei - PACKAGE DI I/O IN JAVA 9 IL CASO DI SYSTEM.IN In Java, il dispositivo di input standard è la variabile (static) System.in, di tipo InputStream. Poiché InputStream fornisce solo un metodo read() che legge singoli byte, si usa incapsulare System.in in un oggetto dotato di maggiori funzionalità, come ad esempio un BufferedReader, che fornisce anche un metodo readLine(). Le stringhe lette dovranno poi essere interpretate in qualche modo, con le funzioni di parsing ed eventualmente con la String.split() per separare gli argomenti import java.io.*; class EsempioIn { public static void main(String args[]){ int a = 0, b = 0; BufferedReader in = new BufferedReader( new InputStreamReader(System.in)); try { System.out.print("Primo valore: "); a = Integer.parseInt(in.readLine()); System.out.print("Secondo valore:"); b = Integer.parseInt(in.readLine()); } catch (IOException e) { System.out.println("Errore in input"); } System.out.println("La somma vale " + (a+b)); } } Aimei - PACKAGE DI I/O IN JAVA 10 LA GERARCHIA DEGLI STREAM DI CARATTERI Le classi per l’I/O da stream di caratteri sono più efficienti di quelle per l’I/O generico a byte, e gestiscono la conversione fra la rappresentazione UNICODE adottata da Java e quella specifica della piattaforma in uso (tipicamente ASCII) e della lingua adottata (cosa essenziale per il supporto dell’internazionalizzazione). Anche qui vale il principio dell’“incapsulamento” di classi sorgente in classi di filtraggio di un Reader o di un Writer da parte di classi più evolute. I metodi forniti sono del tutto analoghi a quelli degli stream, salvo che operano su char piuttosto che su byte. Aimei - PACKAGE DI I/O IN JAVA 11 STREAM DI CARATTERI - INPUT Rispetto alla classe-base (astratta) Reader, una classe concreta deve implementare read() e close(), anche se può ovviamente ridefinire altri metodi. read() restituisce un carattere UNICODE. Classi sorgenti notevoli: • CharArrayReader è il corrispondente di ByteArrayInputStream nel caso di Reader: prende l’input da array di caratteri. • StringReader è analogo al precedente, ma usa come sorgente un oggetto String invece di un array di caratteri. • InputStreamReader: reinterpreta un InputStream (stream di byte) come stream di caratteri, traslando l’input da byte a caratteri UNICODE secondo la lingua locale prescelta. L'uso tipico di questa classe è con lo stream System.In. • La sua sottoclasse FileReader crea tale InputStream a partire da un nome di file o da un oggetto File o da un FileDescriptor. Classi di filtraggio notevoli: • BufferedReader aggiunge il buffering a un Reader di altro tipo, definendo anche il metodo readLine(), che legge una riga fino al fine linea (platform-dependent) • FilterReader, classe di supporto che può essere usata per realizzare un filtro con poco sforzo. Non ci sono classi di filtraggio che "interpretano" i caratteri letti (p.e., non c'è un DataInputReader). Aimei - PACKAGE DI I/O IN JAVA 12 STREAM DI CARATTERI - OUTPUT Classi sorgenti notevoli: • CharArrayOutputStream è il corrispondente di ByteArrayOutputStream nel caso di Writer. • OutputStreamWriter adatta un OutputStream (stream di byte) come stream di caratteri, traslando l’output da caratteri UNICODE a byte secondo la lingua locale prescelta. L'uso tipico è con System.Out. • La sua sottoclasse FileWriter crea tale OutputStream a partire da un nome di file o da un oggetto File o da un FileDescriptor. Classi di Filtraggio Notevoli: • BufferedWriter aggiunge il buffering a un Writer di altro tipo definendo anche il metodo newLine() per emettere il fine linea (platform-dependent) • FilterWriter è una classe base che permette di scrivere con sforzo ridotto un filtro. Non ci sono classi di filtraggio che "interpretano" i dati da scrivere (p.e., non c'è un DataOutputReader). Aimei - PACKAGE DI I/O IN JAVA 13 ESEMPIO 2 – I/O DA FILE DI TESTO static void scriviSuFile(BufferedReader input, String file) throws FileNotFoundException, IOException { BufferedWriter bw = null; try { bw = new BufferedWriter( new FileWriter(file)); String line = null; while(!(line = input.readLine()).equals("")){ bw.write(line); bw.newLine(); } } finally { if(bw != null) try { bw.close(); // questo implica la flush } catch(Exception e) {} } } static void stampaSuOutput(PrintStream out, String file) throws FileNotFoundException, IOException { BufferedReader br = null; try { br = new BufferedReader(new FileReader(file)); String line = null; while((line = br.readLine()) != null) { out.println(line); } } finally { if(br != null) try { br.close(); } catch(Exception e) {} } } Aimei - PACKAGE DI I/O IN JAVA 14 SERIALIZZAZIONE DI OGGETTI Serializzare un oggetto significa trasformarlo in uno stream di byte, salvabile su file; analogamente, deserializzare un oggetto significa ricostruirlo a partire dalla sua rappresentazione su file. Una classe serializzabile deve implementare l'interfaccia Serializable (è vuota, funge da marcatore). • ObjectOutputStream è la sottoclasse di OutputStream usata per serializzare un oggetto • ObjectInputStream è la sottoclasse di InputStream usata per deserializzare un oggetto Queste due classi funzionano in larga misura come DataInputStream e DataOutputStream, con una differenza: aggiungono la capacità di scrivere/leggere oggetti non primitivi su/da uno stram di byte. Un oggetto viene • serializzato invocando il metodo writeObject() di ObjectOutputStream; • deserializzato invocando il metodo readObject() di ObjectInputStream. Questi metodi scrivono / leggono tutti i dati, inclusi i campi privati e quelli ereditati dalla superclasse. I campi dati che sono riferimenti a oggetti sono serializzati invocando ricorsivamente writeObject() sull'oggetto referenziato: serializzare un oggetto può quindi comportare la serializzazione di un intero grafo di oggetti. L'opposto accade quando si deserializza un oggetto. NB: se uno stesso oggetto è referenziato più volte nel grafo, viene serializzato una sola volta, onde evitare che writeObject() cada in una ricorsione infinita. Aimei - PACKAGE DI I/O IN JAVA 15 CAMPI TRANSIENTI A volte, non tutti i campi dati di un oggetto devono essere serializzati. Alcuni dati, infatti, possono non essere significativi (ad esempio, la posizione di un cursore grafico) in quanto devono essere ricalcolati o resettati all'atto del ricaricamento dell'oggetto (ad esempio, il cursore può dover essere riposizionato al centro dell'area). È possibile distinguere i campi da non serializzare etichettandoli transient. PERSONALIZZAZIONE DELLA SERIALIZZAZIONE A volte, può essere necessario personalizzare il modo in cui una classe viene serializzata e deserializzata. Per fare questo, occorre definire nella classe i due metodi (privati!) writeObject() e readObject(). GESTIONE DELE VERSIONI La versione serializzata di un oggetto contiene un numero di versione, che è importante per verificare se la classe è in grado di deserializzare l'oggetto. Infatti, una nuova versione di una classe potrebbe non essere in grado di deserializzare un oggetto di una versione precedente della stessa classe. Aimei - PACKAGE DI I/O IN JAVA 16