MokaByte 59 - Gennaio 2002 
foto dell'autore non disponibile
Le nuove I/O API nel JDK 1.4
Canali, Buffer, Charset, Pattern Matching e operazioni asincrone nel nuovo java.nio package
di
Alessio
Saltarin
Il primo, importante cambiamento nelle API di input/output in Java avvenne con l'uscita del JDK 1.1. All'epoca segnò l'importante correzione di una API scritta in modo affrettato e, secondo alcuni autori[1] incoerente, per raggiungere invece con la release 1.1 uno standard di alto livello, in linea con le aspettative della comunità di sviluppatori object-oriented. Oggi assistiamo ad una significativa aggiunta alle I/O API[2] che va nel senso della continuità, inserendo però delle feature di alto livello per sfruttare le capacità dei sistemi operativi moderni di trattare con i file e i dispositivi di rete. In particolare si è agito su due fronti: il primo riguarda la gestione delle operazioni di input/output per i server e client Internet, che richiedono la conversione da differenti charset - e noi lo sappiamo bene, visto che per la prima volta Java ha un occhio di riguardo per l'ISO-8859-1 tipico del nostro paese - e, soprattutto la gestione di operazioni I/O non bloccanti e multiplex per ottenere la scalabilità; il secondo riguarda una più raffinata gestione dei buffer (che ora sono specifici per tipo) e delle espressioni regolari.

Canali e Buffer
Cos'è un Buffer, nella nuova accezione del JDK1.4? E' sostanzialmente una classe contenitore. Quindi una cosa simile a java.util.Vector, per capirci. Però ha due differenze fondamentali:

  • È un contenitore di tipi primitivi
  • È direttamente associabile ad un file

La prima proprietà ci convince del fatto che si tratta di un contenitore lineare, a capacità finita. Nel quale possiamo spostarci attraverso un indice che marca la posizione di lettura/scrittura raggiunta fino a quel momento. La seconda proprietà ci dice che, da oggi in avanti, potremo scrivere e leggere dati primitivi da un file senza preoccuparci di formattarlo o tokenizzarlo in qualche modo. Questo significa, banalmente, che se dobbiamo scrivere un file contenente una serie di numeri in virgola mobile potremo semplicemente inserirli in un DoubleBuffer e poi associarli ad un file in scrittura. E poi navigarlo in lettura come crediamo, e cioè in modo assoluto passandogli l'ordinale del numero che vogliamo leggere dal file, oppure in modo relativo, cioè leggi il prossimo, leggi il precedente. Un Buffer si istanzia col metodo statico wrap, ad esempio:

int myints[] = { 0, 1, 2, 3 };
IntWrapper iw = IntWrapper.wrap(myints);

che leggeremo così:

while (iw.hasRemaining())
System.out.println((int)iw.get());

Per utilizzare un buffer in corrispondenza di una operazione di I/O, dobbiamo aprire un canale. Cos'è un canale, a questo punto? Il JavaDoc ci dice che è un "nexus" per operazioni di I/O. Cioè fondamentalmente si tratta di un mezzo di collegamento tra un oggetto software e una periferica hardware qualsiasi in grado di fare I/O. Allora è come uno stream, direte voi. Questo è parzialmente vero. Un canale ha due proprietà particolari: è possibile chiuderlo in modo definitivo (nel senso che un'operazione di I/O su un canale chiuso verrà in ogni caso respinta) e non necessariamente dal processo che lo ha aperto (dunque è asynchronously closeable) e inoltre è possibile interrompere le operazioni di I/O di thread concorrenti: vale a dire che se il mio thread scopre che il canale che deve usare è bloccato da un altro thread, può interromperlo chiamando il metodo interrupt del thread bloccante, che riceverà dal canale un'eccezione di tipo ClosedByInterruptException, e quindi il thread s'interromperà (tecnicamente, imposterà a true il valore della variabile statica interrupted di java.lang.thread). Un canale, dunque, è un'astrazione molto potente e ci permetterà di utilizzare, in particolare, il mattone principale su cui è fondata la trasmissione dei dati su Internet, il socket. Ma di questo parleremo più avanti.
Supponiamo di aver aperto un file attraverso un Canale: è necessario a questo punto mappare in memoria il contenuto del file, per poi poterlo incapsulare in un Buffer. La classe per farlo si chiama MappedByteBuffer, che è istanziata dal metodo map di un FileChannel. Tra gli argomenti di questo metodo troviamo la dimensione del file, e se questo file è in lettura o scrittura. Siccome MappedByteBuffer è un ByteBuffer, quest'ultimo tipo specifico di Buffer può essere convertito in uno qualsiasi degli altri (IntBuffer, DoubleBuffer, CharBuffer ecc.). Se volessimo aprire un file di testo ed estrarne un CharBuffer, ecco come dovremmo fare:

FileInputStream fis = new FileInputStream(f);
FileChannel fc = fis.getChannel();
int sz = (int)fc.size();
MappedByteBuffer bb = fc.map(FileChannel.MAP_RO, 0, sz);
CharBuffer cb = bb.asCharBuffer();

L'uso di questi Buffer, oltre che essere molto semplice, ci risolve una serie di problemi quotidiani non da poco, come il salvataggio di file di configurazione, la ricerca veloce all'interno di un insieme molto grande di dati e via dicendo. Manca solo una cosa, e mi sembra molto probabile che qualcuno l'abbia già implementata (del resto tutta la filosofia di questi Buffer non fa che richiamare i principi base di XML): un bel DOMBuffer che implementi Buffer. Pensate: associare ad un file scritto in un qualsiasi linguaggio XML, ad esempio XHTML, un Buffer che tenga in memoria tutta la sua struttura ad albero, con metodi getNode(), getRoot() e via dicendo. Tutto questo in maniera automatica, incapsulando le ostiche chiamate alle classi standard di JAXP (Java API for XML Processing).

 

 

Coding e decoding di charset differenti
Come ho detto, queste nuove funzioni di I/O nascono soprattutto dalle nuove necessità di sviluppo che Internet ha posto alla comunità di sviluppatori. Una di queste è la codifica dei set di caratteri, che i vari enti standardizzatori mondiali (per noi c'è ISO) hanno da tempo categorizzato. Java supporta il set di caratteri Unicode (per l'esattezza Unicode 2.1 a partire dal JDK 1.1.7), peccato che quasi nessun documento su Internet giri con quel formato. Unicode è a 16 bit, come tutti sanno, ma la gran parte dei set di caratteri antecedenti (UTF-8, per esempio, che è il formato su cui girano la maggior parte delle pagine HTML) ne supporta solo otto. Bene, a partire da questa versione di Java avremo a disposizione la classe java.nio.charset che ci permetterà di utilizzare il charset che preferiamo (mappandolo su Unicode), a patto che questo charset sia supportato dallo IANA Charset Registry (http://www.iana.org/assignments/character-sets). Utilizzare un charset significa codificare e decodificare uno stream di byte, quindi è possibile associare una specifica codifica ad un ByteBuffer, e da questo ottenere, attraverso il metodo decode() il relativo CharBuffer. Ecco come, ad esempio:

Charset charset = Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
FileInputStream fis = new FileInputStream(f);
FileChannel fc = fis.getChannel();
int sz = (int)fc.size();
MappedByteBuffer bb = fc.map(FileChannel.MAP_RO, 0, sz);
CharBuffer cb = decoder.decode(bb);

 

 

Pattern matching
La possibilità di utilizzare le espressioni regolari non è una novità per chi programma, per dirne una, in JavaScript, ma finora non c'era modo di usarle in Java se non attraverso librerie esterne oppure utilizzando StringTokenizer - che fa sì una specie di pattern matching, ma non attraverso le espressioni regolari. Benché occorra probabilmente un intero articolo per trattarle almeno superficialmente, posso dire che le espressioni regolari rappresentano una notazione per filtrare un insieme di caratteri in entrata attraverso un pattern (es.: i caratteri alfanumerici, i caratteri di escape, le frasi che cominciano con H maiuscolo, le stringhe che contengono uno e un solo carattere @ e via dicendo). Il risultato è il match tra i caratteri in entrata e le regole che sono specificate dal pattern. Chi utilizza Unix, fa pattern matching quando utilizza sed o awk. Chi utilizza il DOS, benché anche questo non supporti le espressioni regolari, fa pattern matching quando scrive, per esempio,

dir pro??.gif

Le espressioni regolari sono una particolare sintassi che specifica come descrivere il pattern. Qualche esempio:

Con Java 1.4 il supporto al pattern matching attraverso le espressioni regolari è finalmente supportato. Il fatto che lo si evidenzi all'interno delle nuove funzionalità di I/O va visto essenzialmente perché il pattern matching di fatto viene svolto su di un CharBuffer (per l'esattezza su di una CharSequence, che è la classe madre di CharBuffer, di String e di StringBuffer), e quindi, volendo, su un Canale.
L'intero nuovo package java.util.regex consta delle due nuove classi dal nome piuttosto self-explicative di Pattern e Matcher. Come è facile intuire con Pattern definiremo (compileremo, anzi, per dirla con il termine usato dai progettisti Sun) il pattern su cui Matcher effettuerà la sua comparazione. Per esempio, per stabilire un pattern che filtri, all'interno di un testo qualsiasi (multilinea!), una stringa che termina con un ritorno carrello, scriveremo:

Pattern linea = Pattern.compile("*\r?\n", Pattern.MULTILINE);

Per effettuare un match su un testo in entrata, supponendo di avere a disposizione un CharBuffer che si chiami cb, utilizzeremo:

Matcher match = linea.matcher(cb);

A questo punto Match diventa un contenitore che è navigabile semplicemente così:

while(m.find())
System.out.println("Trovato un match!");

Un'altra applicazione molto utile delle espressioni regolari riguarda il controllo di particolari formattazioni di stringhe di input. Il seguente, per esempio, controlla che input non abbia caratteri non validi per essere un indirizzo di e-mail.

String input = "tizio@sito.net";
Pattern p = Pattern.compile("[^A-Za-z0-9\\.\\@_\\-~#]+");
Matcher m = p.matcher(input);
if (m.find())
System.err.println("Indirizzo e-mail non valido.");

 

 

 

Socket Channel e operazioni asincrone sul canale
Ma tutte queste novità - leggi charset, canali, buffer di caratteri - vi fanno venire in mente qualcosa? Certamente vi saranno milioni di campi di applicazione, ma a me ne viene in mente uno che li comprende tutti: la scrittura di un server Web. In effetti, ora esiste un modo per scrivere e leggere in modo efficiente e scalabile (quindi utilizzando tecniche di multithreading in cui ciascun thread possa agire in modo non bloccante sul canale) via socket. Mi spiego meglio. Fino ad ora, avessimo voluto scrivere un Web Server in Java - a meno di utilizzare tecniche o librerie estranee alla Java API - avremmo dovuto limitarci ad un thread per connessione, e questo thread avrebbe il compito di monitorare ciò che accade sulla connessione. Ebbene, un canale ci permette di operare in modo asincrono su una connessione. Ciò vuol dire che, una volta aperto il canale, possiamo descrivergli gli eventi a cui siamo interessati - per esempio: avvenuta connessione, avvenuta lettura, avvenuta scrittura - e aspettare che ci venga notificato quando l'evento si verifica. Ad esempio, per un client HTTP:

SocketChannel ch = SocketChannel.open();
ch.configureBlocking(false);
ch.connect( new InetSocketAddress(args[0],80 );
Selector sel = Selector.open();
ch.register(sel, SelectionKey.OP_CONNECT);
while (sel.select(500)){
   […]
}

Dunque: il tipo di canale per questo genere di operazioni è il SocketChannel. Specificheremo che vogliamo utilizzarlo in modo non esclusivo (non bloccante). Colleghiamo il canale alla URL voluta (args[0]) e alla porta voluta. A questo punto associamo un Selector al SocketChannel, specificando che siamo interessati all'evento "avvenuta connessione" (OP_CONNECT). Poi mettiamo il thread in attesa che l'evento si verifichi (per 500 millisecondi). Ricordate quanto ho detto prima a proposito dei canali, e che cioè è possibile interromperli o chiuderli in modo asincrono da un altro processo? Ecco spiegata la tecnica della concorrenza su un solo canale (connessione) da parte di più thread concorrenti. Cioè, al verificarsi di un evento, il thread interrompe le eventuali operazioni sul canale, lo usa, e lo rilascia, in modo del tutto automatico e senza che lo sviluppatore debba preoccuparsene. Ad esempio:


if (ch.isConnectionPending())
   ch.finishConnect();
else
  ch.write(…);

Qui supponiamo di essere all'interno del ciclo while di cui sopra e di utilizzare il canale per effettuare la connessione. Siccome il canale è non-bloccante e asincrono, l'operazione di connessione richiesta prima può o non può essere stata accolta. Se non lo è stata, o si è verificato un errore, nel qual caso riceveremo un' IOException, oppure la connessione aspetta un aknowledge da parte nostra, cosa che viene effettuata con finishConnect() (notate come questo sia il vero spirito Internet, e quindi sviluppare applicazioni asincrone quando si utilizza un protocollo come HTTP risulti molto più semplice e logico). Se la connessione è attiva sul canale, è possibile inviare la nostra sequenza di byte. Ricordo che questo accade nel preciso momento in cui il sistema ci ha comunicato che l'evento OP_CONNECT si è verificato. Al contrario, avessimo scelto di utilizzare un canonico approccio bloccante, avremmo dovuto avvertire che cominciavamo una sequenza di connessione, con il metodo begin() di AbstractChannel (il genitore di Channel), aspettare la stringa di feedback del Web Server, quindi inviare l'acknowledge e chiudere il canale, con end() di AbstractChannel.

boolean completed = false;
try {
   begin();
   completed = ...; // Operazioni di IO bloccanti
   return ...;
}
finally {
   end(completed);
}

 

 

Conclusioni
L'introduzione dei concetti di canale di I/O in Java, con la sua possibile gestione attraverso la notifica asincrona di eventi, è un significativo passo in avanti e mi sembra che rappresenti un adeguamento al megatrend in atto che prevede l'utilizzo di Internet come supporto per l'invocazione remota dei metodi (leggi SOAP e WebServices[3]) utilizzando XML con tutti i suoi charset) come documento di scambio. Oltre a questo, l'introduzione dei Buffer è certamente benvenuta e la sua semplicità di utilizzo potrà rendere questo il modo preferenziale per effettuare I/O di dati strutturati.

Riferimenti
[1] Bruce Eckel - Thinking in Java 2nd Ed. - Cap. 10
[2] John Zukowski - New I/0 Functionality for JavaTM 2 Standard Edition 1.4 - http://developer.java.sun.com/developer/technicalArticles/releases/nio/
[3] BluePrints for Web Services - Initial Thoughts - http://java.sun.com/j2ee/blueprints/WS_article.html

 

 

Esempi
Scarica gli esempi descritti in questo articolo


Alessio Saltarin è laureato in Ingegneria. Si occupa di progettazione e sviluppo di sistemi e applicazioni Web, in particolare usando Java 2 Enterprise Edition.


MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati 
E' vietata la riproduzione anche parziale 
Per comunicazioni inviare una mail a info@mokabyte.it