Introduzione agli stream in Java

In Java gli input e gli output vengono organizzati in stream, ossia flussi di byte rispettivamente entranti ed uscenti dal sistema di elaborazione centrale, e ciascuno di essi viene gestito interfacciandolo con un oggetto di un’opportuna classe, in funzione del tipo di stream. Ad esempio, nel caso di uno stream di input proveniente da una tastiera o dalla lettura di un file di testo, tali byte devono essere letti come dei caratteri. Si avrà quindi bisogno di un oggetto cosiddetto lettore (text scanner), che sia cioè in grado di interpretare il flusso di byte dello stream di input come caratteri, ossia, secondo il codice dei caratteri, che si ricorda in Java è il codice Unicode su 2 byte, l’UTF-16. La stessa situazione si ha nel caso di uno stream di output verso la console oppure in scrittura in un file di testo, per lo stesso motivo si ha bisogno di un oggetto cosiddetto scrittore. Quando invece i byte di uno stream devono essere trattati come byte ‘grezzi’, cioè cosi come sono, secondo la rappresentazione dei dati interna della ‘macchina’ (ndr, il computer), è questo il caso della lettura e scrittura di un file binario, allora bisognerà utilizzare oggetti opportuni che non siano ‘lettori’ o ‘scrittori’.

In Java il package java.io mette a disposizione molte classi per gli stream, distinguendo fra classi per gli stream di byte e classi per gli stream di caratteri, disorientando non poco chi si avvicina a questo linguaggio per la prima volta, soprattutto se si proviene da altri linguaggi come C++ . Allora proviamo a mettere un po’ d’ordine!

Le classi a cui si accennerà sono molteplici (vedi il diagramma UML della figura di sotto, che riporta solo alcune delle tante classi presenti nel package java.io), pertanto di ciascuna di esse metterò in risalto solo gli aspetti fondamentali. Per approfondimenti si rimanda alla documentazione ufficiale del linguaggio Java (link), che viene da me utilizzata come fonte di questo articolo.

stream01

La classe System

Per la gestione degli stream di input e di output standard (tastiera e console), Java mette a disposizione la classe System che fa parte del package java.lang. Essa ha tre attributi statici, che per default sono associati a questi stream nel modo seguente:

  • in, è il riferimento ad un oggetto di classe InputStream associato per default allo stream di input standard (la tastiera);
  • out, è il riferimento ad un oggetto di classe PrintStream associato per default allo stream di output standard (la console);
  • err, è il riferimento ad un oggetto di classe PrintStream associato per default allo stream error standard (la console).

L’oggetto out rende estremamente semplice realizzare un output sulla console grazie ad alcuni suoi metodi di facile utilizzo, quali printf()print() println(). In particolare gli ultimi due, grazie alle diverse ridefinizioni di overloading disponibili, rendono semplice stampare sulla console un qualunque tipo di dato, oggetti compresi.

Purtroppo non si può dire altrettanto per la lettura di un input da tastiera. L’oggetto in, infatti, mette a disposizione solo dei metodi che leggono e restituiscono byte grezzi (in particolare il metodo int read() restituisce al chiamante un intero che contiene un byte letto dallo stream di input). Pertanto se si vuole utilizzare questo metodo per prelevare degli input da tastiera, è necessario operare delle elaborazioni e conversioni di tipo. Una ‘bella’ scomodità!

L’input da tastiera

Un modo più comodo di operare un input da tastiera consiste nell’utilizzare un oggetto ‘lettore’. Per inciso le classi ‘lettrici’ sono quelle che derivano dalla superclasse Reader. Una di queste è la classe InputStreamReader, per cui un oggetto di questa classe può fare da ponte tra lo stream di byte proveniente dalla tastiera (associato all’oggetto System.in) e lo stream di caratteri da produrre, occupandosi di eseguire il lavoro di conversione dei byte dello stream in caratteri. Lo stream dei caratteri così ottenuto, poi, può essere convenientemente (incapsulato) “agganciato” ad un oggetto ‘lettore’ di una classe specializzata nella lettura di uno stream di caratteri, per sfruttare la maggiore versatilità dei suoi metodi. E’ questo il caso della classe BufferedReader che mette a disposizione i comodi metodi int read() e String readLine() che, rispettivamente, restituisco un carattere e una stringa, provenienti dallo stream di caratteri (l’oggetto di classe InputStreamReader) fornito come parametro al costruttore. Ciò si può tradurre in Java con le istruzioni 7, 8 e 11 del codice dell’esempio seguente:

Quello che accade con le istruzioni 7, 8 e 11, quindi, è che l’oggetto System.in, di classe InputStream, legge uno ad uno i byte dello stream della tastiera; l’oggetto lettore, di classe InputStreamReader, trasforma la sequenza di byte in una sequenza di caratteri secondo il codice Unicode adottato da Java; infine, l’oggetto input di classe BufferedReader, tramite il metodo readLine() restituisce la stringa dei caratteri presente nello stream fino al primo carattere ‘\n‘ (corrispondente alla pressione dell’invio da parte dell’utente). Si fa notare che la classe BufferedReader non è una classe specializzata su stream di caratteri provenienti dalla tastiera, bensì è una classe che può gestire stream di caratteri provenienti da fonti diverse, come ad esempio un file di testo o altri computer collegati in rete. Per questo motivo i suoi metodi sono stati scritti in modo da gestire la possibilità che si verifichino dei malfunzionamenti che possano interrompere lo stream, lanciando un’eccezione di classe IOException. Per malfunzionamento s’intende una qualsiasi situazione che interrompa il flusso di dati in input.

Input di dati numerici

Con questo tipo di soluzione, quando bisogna prelevare in input dei dati numerici, per operare la conversione del dato acquisito sotto forma di stringa in uno dei tipi numerici primitivi (short, int, long, float o double), si consiglia di utilizzare i metodi statici delle classi wrapper dei tipi numerici corrispondenti:

  • short Short.parseShort(String)
  • int Integer.parseInt(String)
  • float Float.parseFloat(String)
  • double Double.parseFloat(String)

Questi metodi lanciano un’eccezione di classe NumberFormatException quando la stringa passata come parametro non è convertibile in un numero e ciò permette di gestire facilmente gli errori .

NOTA – Per realizzare l’input da tastiera, esiste una soluzione alternativa e più semplice della precedente che consiste nell’utilizzare la classe Scanner del package java.util. Questa classe viene introdotta in un altro articolo a cui si rimanda: link.