In questo articolo si affronta una delle problematiche tipiche delle applicazioni Java distribuite, ovvero quella legata all‘encoding delle stringhe. L‘argomento è sviluppato con un taglio pratico, senza parlare di internazionalizzazione, ma fornendo un quadro teorico per capire il comportamento di Java e una serie di indicazioni per prevenire e risolvere la visualizzazione di determinati caratteri con i soliti “???”.
Un po‘ di storia
L‘avvento della codifica ASCII permise di rappresentare tutte le lettere inglesi non accentate usando 7 bit. I computer di allora, che lavoravano a 8 bit, erano quindi in grado di memorizzare l‘insieme dei possibili caratteri ASCII e in più avevano a disposizione 1 bit che avanzava. Il problema fu che in molti decisero di sfruttare lo spazio di codici 128-255, relativo a questo ottavo bit.
Tra questi, IBM cominciò a usare per i suoi PC un set di caratteri, denominato OEM, che comprendeva alcune lettere accentate tipiche delle lingue europee, barre orizzontali e verticali etc…
Con la diffusione dei PC in tutto il mondo, i set di caratteri OEM proliferarono e si adattarono alle esigenze delle diverse lingue (p.e., il codice 130 corrispondeva al carattere europeo “è”, ma anche al carattere ebraico Gimel).
Per rimettere ordine, fu quindi coniato lo standard ANSI, che fissò le codifiche dei primi 128 caratteri, come aveva fatto l‘ASCII, e prese atto dei differenti sistemi di gestione dei rimanenti codici, quelli compresi tra 128 e 255, ai quali dette il nome di code page. Comparvero versioni di MS-DOS contenenti dozzine di code page per rispondere alle esigenze delle diverse lingue. Ben presto però non fu più sufficiente, sia perché i canonici 8 bit stavano comunque stretti ad alcuni alfabeti con migliaia di caratteri, sia perché, con la comparsa di Internet, si manifestò l‘esigenza di scambiare stringhe tra computer dislocati nelle diverse parti del mondo. Questi motivi portarono all‘invenzione dello lo standard di codifica Unicode.
Unicode
Unicode deriva quindi dal tentativo di creare un set di caratteri che vada bene per tutte le lingue conosciute. Unicode è un sistema di codifica a 16 bit, per un totale di 65536 possibili rappresentazioni, dette code point. La novità è che, mentre fino ad allora ogni lettera era mappata in una sequenza di bit, in Unicode ad ogni carattere corrisponde un code point (p.e., U+0645), però non viene stabilito come rappresentarlo in memoria. Ed è qui che entra in gioco il concetto di encoding.
Oggi esistono molti sistemi di encoding, ma bisogna distinguere tra quelli tradizionali (CP-1252, ISO8859-1, etc.), che sono in grado di memorizzare solo alcuni code point in modo corretto e mappano i restanti nel carattere “?”, da quelli che possono invece memorizzare tutti i code point della codifica Unicode (UTF-7, UTF-8, UTF-16, etc.).
Tra questi ultimi sistemi di encoding, uno dei più usati è l‘UTF-8, che memorizza i vari code point con 8 bit alla volta (fino ad un massimo di 6 byte) e che per i code point 0-127 usa il primo byte. Quest‘ultima caratteristica fa sì che il normale testo inglese possa essere memorizzato allo stesso modo dei vari ASCII, ANSI, OEM e rende pertanto l‘UTF-8 compatibile con i precedenti sistemi di codifica a byte singolo (p.e., una vecchia stringa di testo in ASCII continua ad essere visualizzata correttamente in un sistema che usa la codifica UTF-8).
La rappresentazione dei caratteri in Java
Il linguaggio Java rappresenta i caratteri usando il sistema di codifica Unicode. Per questo motivo, ogni volta che in un‘architettura distribuita vengono scambiati dei dati testuali (per semplicità, si pensi a un file di testo) tra un sistema esterno e un‘applicazione Java avviene una conversione in Unicode.
Ora, tale conversione avviene in modo automatico e senza problemi se l‘encoding del testo corrisponde a quello di default della Java Virtual Machine (JVM). Ma se i due encoding differiscono, la conversione deve essere gestita in modo programmatico, pena la renderizzazione errata di alcuni caratteri con i soliti “?”. Questi errori di visualizzazione possono verificarsi, ad esempio, quando si fa una conversione da un file esterno con codifica UTF-8 a una applicazione Java su una piattaforma con encoding CP-1252 (si pensi a Windows che utilizza quest‘ultimo come encoding di default). Infatti il CP-1252, a differenza dell‘UTF-8, non è in grado di memorizzare tutti i code point del sistema Unicode ed è passibile di errori di trascodifica del file.
Vediamo a questo punto come si determina l‘encoding di default della JVM e quali API consentono la gestione della conversione del testo in Unicode. Per quanto riguarda l‘encoding della JVM, va detto che generalmente corrisponde all‘encoding di default della piattaforma sottostante. Comunque, a scanso di equivoci, per individuare l‘encoding di default usato è sufficiente creare un OutputStreamWriter e invocare il metodo getEncoding come mostrato nell‘esempio che segue:
OutputStreamWriter out = new OutputStreamWriter(new ByteArrayOutputStream()); System.out.println("encoding: " + out.getEncoding());
Parlando, invece, della gestione programmatica della conversione in Unicode, bisogna sottolineare che essa differisce a seconda che si usino gli stream di caratteri o gli stream di byte: nel primo caso (stream di caratteri), infatti, si deve creare un oggetto Charset corrispondente all‘encoding del file di testo e lo si deve passare al costruttore di InputStreamReader, mentre nel secondo caso (stream di byte) basta specificare l‘encoding nel costruttore della classe String.
Di seguito è riportato un frammento di codice d‘esempio che effettua la conversione di un file UTF-8 (TestoUTF-8.txt) in Unicode, usando sia gli stream di byte che di caratteri:
System.out.println("Reading strings"); try {
Charset charset = Charset.forName("UTF-8"); inString = new BufferedReader(new InputStreamReader( new FileInputStream(new File("TestoUTF-8.txt")), charset)); } catch (FileNotFoundException e) { e.printStackTrace(); } try { String s; while ((s = inString.readLine()) != null){ System.out.println(s); } } catch (IOException e) { e.printStackTrace(); } System.out.println("Reading bytes"); try { inByte = new FileInputStream(new File("TestoUTF-8.txt")); } catch (FileNotFoundException e) { e.printStackTrace(); } byte buffer[] = new byte[1000]; int n; String b; try { while((n= inByte.read(buffer)) > -1){ b = new String(buffer,0,n,"UTF-8"); System.out.print(b); } } catch (IOException e) { e.printStackTrace(); } try { inString.close(); inByte.close(); } catch (IOException e) { e.printStackTrace(); }
Consigli pratici
Vediamo ora in concreto come bisogna comportarsi per evitare i problemi che si hanno in caso di applicazioni Java e sistemi esterni che si scambiano stringhe. Tali problemi, come detto, derivano da errori di trascodifica che si verificano quando si usano encoding differenti e che portano alla visualizzazione di caratteri “???”.
Innanzitutto dovrebbe essere ormai chiaro che quando si ha una stringa in un testo bisogna conoscere il suo encoding, altrimenti non si è sicuri di interpretarla e visualizzarla correttamente. Detto ciò, il modo più semplice per non avere problemi nel caso di un‘architettura distribuita da realizzare ex-novo è di scegliere una codifica di riferimento per i dati testuali e di usarla sia per i sistemi esterni che per le applicazioni Java. A tal proposito la codifica UTF-8, per le caratteristiche menzionate, si sta affermando come standard de facto.
Parlando, invece, di applicazioni già implementate si può scegliere di intervenire a livello programmatico o cambiando l‘encoding di default sulle diverse piattaforme. Nel primo caso si opera a livello di API, come mostrato nel frammento di codice precedente, in modo da garantire la corretta trascodifica in Unicode. Questa soluzione non presenta controindicazioni, tuttavia non sempre è praticabile in quanto si può avere a che fare con delle API che non consentono di specificare l‘encoding. Si pensi ad esempio a una piattaforma con encoding di default ASCII su cui è presente un‘applicazione Java, che riceve un file di testo UTF-8 e logga con Log4J le stringhe lette. In questo caso, infatti, pur essendo presente la giusta trascodifica del file UTF-8, al momento di loggare le stringhe corrette che risiedono in memoria non si ha la possibilità di specificare l‘encoding da usare; si ripiegherebbe quindi sull‘ASCII (l‘encoding di default della JVM), incorrendo nei noti problemi di visualizzazione di caratteri.
Per quanto riguarda il cambiamento dell‘encoding di default si hanno a disposizione le alternative seguenti, ciascuna con i suoi pro e contro.
La prima possibilità consiste nel fissare a posteriori un encoding di default e di impostarlo sulle diverse piattaforme che ospitano i vari sistemi dell‘architettura distribuita in questione. Tale soluzione è la più drastica, in quanto l‘impostazione dell‘encoding a livello di sistema operativo si ripercuoterebbe anche sulle altre applicazioni presenti, con effetti da valutare caso per caso. Tale soluzione “drastica”, però, presenta il vantaggio di risolvere il problema anche per le future applicazioni distribuite che risiederanno sulle piattaforme interessate da tale modifica.
La seconda soluzione è meno invasiva, ma è limitata alle applicazioni Java. Per queste ultime è, infatti, possibile usare la proprietà di sistema file.encoding per impostare un determintato encoding a livello di JVM, senza cambiare quello del sistema operativo sottostante. Per farlo, è sufficiente aggiungere l‘espressione
-Dfile.encoding= DefaultEncoding
nel lancio della JVM, impostando in questo modo il DefaultEncoding senza avere ripercussioni sulle altre applicazioni. In realtà , a essere precisi, si dovrebbero tenere in conto altre applicazioni Java contenute nella JVM interessata, ma in questi casi si possono eventualmente utilizzare delle JVM separate.
Va però sottolineato che, a livello ufficiale, Sun dichiara che la proprietà di sistema file.encoding è read-only, e di conseguenza non è modificabile. Pertanto, sebbene cambiare le impostazioni di tale proprietà funzioni in diversi casi, al momento si tratta di una pratica per la quale Sun non garantisce affidabilità e supporto.
Conclusioni
Nel corso dell‘articolo si sono approfonditi il comportamento di Java nella rappresentazione di caratteri e i relativi problemi di trascodifica in un‘architettura distribuita in cui vengono scambiati dei dati testuali tra un sistema esterno e un‘applicazione Java. Per ovviare a questo tipo di problemi è sufficiente definire nelle nuove implementazioni lo stesso encoding per i dati testuali e per le applicazioni Java. Nel caso invece di applicazioni esistenti, sono state presentate due soluzioni: l‘una di tipo programmatico, l‘altra consistente nell‘impostazione dell‘encoding di default. La prima è quella preferibile poiché non ha controindicazioni, ma purtroppo non è sempre praticabile. L‘altra prevede due possibilità (il setting dell‘encoding di default a livello di sistema operativo e il setting dell‘encoding con la proprietà di sistema file.encoding), ciascuna con i suoi pro e i suoi contro.
Riferimenti
http://java.sun.com/docs/books/tutorial/i18n/text/convertintro.html
http://www.cs.tut.fi/~jkorpela/chars.html
http://www.joelonsoftware.com/articles/Unicode.html
http://ppewww.physics.gla.ac.uk/~flavell/iso8859/iso8859-pointers.html