Stranger Things in Java Characters

Stranger Things sul tipo Java char

Questo articolo fa parte della serie “Stranger things in Java”, dedicata agli approfondimenti del linguaggio che ci permetteranno di padroneggiare anche gli scenari più strani che si possono presentare quando programmiamo. Tutti gli articoli sono ispirati dal contenuto dal libro "Java for Aliens" (in inglese) e dal libro "Il nuovo Java".

Non molti sanno che la seguente è un'istruzione Java valida:

\u0069\u006E\u0074 \u0069 \u003D \u0038\u003B

Potete provare ad inserirla all'interno del metodo main di un classe qualsiasi e compilarla. Se poi aggiungete dopo la precedente istruzione anche la seguente:

System.out.println(i);

Eseguendo la classe otterrete la stampa del numero 8! E sapete che questo commento invece produce un errore di sintassi in fase di compilazione?

/*
 * Il file verrà generato all'interno della cartella C:\users\claudio
 */

Eppure i commenti non dovrebbero produrre errori di sintassi. Spesso infatti commentiamo pezzi di codice, proprio per farli ignorare dal compilatore… ma allora cosa sta succedendo?
Se già avete capito tutto potete ignorare il resto dell'articolo, altrimenti potete perdere qualche minuto nel ripassare un po' di Java livello base: il tipo primitivo char.

Il mistero dell'errore nel commento ed altre storie...

Tipo di dato primitivo letterale

Come tutti sanno, il tipo char è uno degli otto tipi primitivi di Java. Esso ci permette di immagazzinare un solo carattere alla volta. Segue un esempio di codice che assegna un valore literal ad una variabile di tipo char:

char unCarattere = 'a';

In realtà, questo tipo di dato non è usato molto spesso, perché nella maggior parte dei casi i programmatori hanno bisogno di sequenze di caratteri e quindi preferiscono utilizzare stringhe. Il tipo String prevede che il valore assegnato sia compreso tra virgolette, da non confondere con gli apici singoli usati per i tipi char. Segue un esempio di codice che assegna un valore ad una variabile di tipo String:

String s = "Java melius semper quam latinam linguam est";

In particolare, ci sono tre modi per assegnare un valore literal ad un tipo char, e tutti e tre questi modi richiedono l'inclusione del valore all'interno di una coppia di apici singoli:

  • utilizzare un unico carattere stampabile presente sulla tastiera (per esempio '&');
  • utilizzare il formato Unicode con notazione esadecimale (per esempio '\u0061', che equivale al numero decimale 97 e che identifica la lettera 'a');
  • utilizzare un carattere di escape per rappresentare particolari caratteri non stampabili (per esempio '\n' che indica il carattere line feed (andare a capo).
Vediamo in dettaglio questi tre casi nei prossimi paragrafi:

Caratteri sulla tastiera stampabili

Possiamo assegnare ad un char un qualsiasi carattere che si trova sulla nostra tastiera, a patto che:

  • le impostazioni di sistema supportino il carattere richiesto (per esempio il vostro sistema operativo potrebbe non supportare il cirillico o gli ideogrammi giapponesi).
  • il carattere sia stampabile (per esempio i tasti di cancellazione, o di Invio, o le freccette non sono stampabili).
In ogni caso, il valore literal assegnabile ad un tipo primitivo char è sempre incluso all'interno di una coppia di apici singoli. Seguono tre esempi:

char aMaiuscolo = 'A';
char trattino = '-';
char at = '@';

Il tipo di dato char viene memorizzato in 2 byte (16 bit), con un range composto da soli numeri positivi che vanno da 0 a 65535. Infatti esiste una mappatura che associa ad ogni numero un certo carattere. Tale mappatura (o codifica) è definita dallo standard Unicode (descritto meglio nel prossimo paragrafo).

Formato Unicode (notazione esadecimale)

Abbiamo detto che il tipo primitivo char è immagazzinato in 16 bit, e che quindi può definire ben 65536 caratteri diversi. Infatti la codifica Unicode si occupa di standardizzare tutti i caratteri (ma anche simboli, emoji, ideogrammi etc.) esistenti su questo pianeta. Possiamo notare che Unicode è un'estensione della codifica nota come UTF-8, che a sua volta è basato sul vecchio standard Extended ASCII ad 8 bit, che a sua volta contiene il più antico standard noto come ASCII code. (acronimo per American Standard Code for Information Interchange ovvero Codice Standard Americano per lo Scambio di Informazioni). È possibile visualizzare una tabella ASCII a questo link. Possiamo assegnare ad un char direttamente un valore Unicode in formato esadecimale, utilizzando 4 cifre che identificano univocamente un determinato carattere, anteponendo ad esse il prefisso \u. Per esempio:

char letteraGrecaPhi  = '\u03A6';  // lettera greca Φ
char carattereUnicodeNonIdentificato = '\uABC8';

In questo caso parliamo di valore in formato Unicode (o in formato esadecimale). Infatti utilizzando 4 cifre con il formato esadecimale si coprono esattamente 65536 caratteri. In realtà, Java 15 supporta la versione 13.0 di Unicode che contiene molti più caratteri rispetto ai 65536 di cui abbiamo parlato. Infatti, oggi lo standard Unicode si è evoluto molto, ed ora permette di rappresentare oltre un milione di caratteri, anche se solo 143859 numeri sono stati già assegnati ad un carattere. Ma lo standard è in continua evoluzione (per maggiori informazioni visitare questo link). Ad ogni modo, per assegnare valori Unicode che sono rappresentati da valori numerici che risiedono al di fuori dell'intervallo di 16-bit di un tipo char, di solito utilizziamo le classi String e Character.

Caratteri di escape speciali

In un tipo char è possibile anche immagazzinare caratteri di escape speciali, ovvero sequenze di caratteri che provocano particolari comportamenti nella stampa:

  • \b che equivale ad un backspace, ovvero una cancellazione verso sinistra (equivalente al tasto Delete)
  • \n che equivale ad un line feed ovvero ad un andare a capo (equivalente al tasto Invio)
  • \\ che equivale ad un solo \ (proprio perché il carattere \ serve per i caratteri di escape)
  • \t che equivale ad una tabulazione orizzontale (equivalente al tasto TAB)
  • \' che equivale ad un apice singolo (un apice singolo delimita il literal di un carattere)
  • \" che equivale ad un doppio apice (un doppio apice delimita il literal di una stringa)
  • \r che rappresenta un carriage return (carattere speciale che sposta il cursore all'inizio della riga)
  • \f che rappresenta un form feed (carattere speciale in disuso che rappresenta lo spostamento del cursore alla pagina successiva del documento)
Da notare che assegnare il literal '"' ad un carattere è perfettamente legale. Quindi la seguente istruzione:

System.out.println('"');

che è equivalente al seguente codice:

char virgolette = '"';
System.out.println(virgolette);

è corretta e stamperà il carattere virgolette:

"

Mentre se provassimo a non utilizzare il carattere di escape per un apice singolo, per esempio, scrivendo la seguente istruzione:

System.out.println(''');

otterremo i seguenti errori in compilazione, dal momento che il compilatore non potrà distinguere i delimitatori del carattere:

error: empty character literal
        System.out.println(''');
                           ^
error: unclosed character literal
        System.out.println(''');
                             ^
2 errors

Siccome i delimitatori per i valori delle stringhe sono rappresentati con i doppi apici (virgolette), allora la situazione si capovolge: è possibile rappresentare apici singoli all'interno di una stringa:

System.out.println("'IQ'");

che stamperà:

'IQ'

Invece bisogna usare il carattere di escape \" per utilizzare le virgolette all'interno di una stringa. Infatti la seguente istruzione:

System.out.println(""IQ"");

provocherà i seguenti errori di compilazione:

error: ')' expected
        System.out.println(""IQ"");
                             ^
error: ';' expected
        System.out.println(""IQ"");
                               ^
2 errors

Invece la seguente istruzione è corretta:

System.out.println("\"IQ\"");

e stamperà:

"IQ"

Scrivere codice Java con il formato Unicode

Il formato Unicode, può essere utilizzato anche per sostituire qualsiasi linea del nostro codice. Infatti il compilatore prima trasforma in carattere il formato Unicode, e poi valuta la sintassi. Per esempio potremmo riscrivere la seguente dichiarazione:

int i = 8;

nella seguente maniera:

\u0069\u006E\u0074 \u0069 \u003D \u0038\u003B

Infatti se poi facciamo seguire alla riga precedente lo statement:

System.out.println("i = " + i);

questo stamperà:

i = 8

Indubbiamente non si tratta di un stile utile per scrivere il nostro codice. Più che altro dobbiamo conoscere questa caratteristica per comprendere alcuni errori che possono capitare raramente.

Formato Unicode per caratteri di escape

Il fatto che il formato esadecimale Unicode venga trasformato dal compilatore prima che essa valuti il codice, ha delle conseguenze, soprattutto quando si ha a che fare con i caratteri di escape. Per esempio consideriamo il carattere line feed (andare a capo) che può essere rappresentato con il carattere di escape \n, e che corrisponde nella codifica Unicode con il numero decimale 10 (che corrisponde al numero esadecimale A). Se proviamo a definirlo tramite il formato Unicode:

char lineFeed = '\u000A';

otterremo il seguente errore in compilazione:

error: illegal line end in character literal
        char lineFeed = '\u000A';
                        ^
1 error

infatti il compilatore trasforma il codice precedente nel seguente prima di valutarlo:

char lineFeed = '
';

Ovvero il formato Unicode è stato trasformato nel carattere andare a capo, e la sintassi precedente non è una sintassi valida per il compilatore Java. Allo stesso modo anche il carattere apice singolo (') che corrisponde al numero esadecimale 27 (equivalente al numero decimale 39) e che possiamo rappresentare con il carattere di escape \', non può essere r appresentato con il formato Unicode:

char apice = '\u0027';

Anche in questo caso il compilatore trasformerà il codice precedente in questo modo:

char apice = ''';

che darà luogo ai seguenti errori in compilazione:

error: empty character literal
        char apice = '\u0027';
                     ^
error: unclosed character literal
        char apice = '\u0027';
                            ^
2 errors

L'errore iniziale è dovuto al fatto che la prima coppia di apici non contiene un carattere, mentre il secondo errore ci indica che specificando il terzo apice c'è un valore literal non chiuso. Anche per quanto riguarda il carattere carriage return, rappresentato dal numero esadecimale D (corrispondente al numero decimale 13), e già rappresentabile con il carattere di escape \r, ci sono dei problemi. Infatti se scriviamo:

char carriageReturn = '\u000d';

avremo il seguente errore in compilazione:

error: illegal line end in character literal
        char carriageReturn = '\u000d';
                              ^
1 error

Infatti, la compilatore ha trasformato il numero in formato Unicode in carriage return facendo tornare il cursore ad inizio riga, e che quello che doveva essere il primo apice è diventato il secondo. Per quanto riguarda il carattere \, rappresentato dal numero esadecimale 5C (corrispondente al numero decimale 92), e, rappresentabile con il carattere di escape \\, se scriviamo:

char backSlash = '\u005C';

otterremo il seguente errore in compilazione:

error: unclosed character literal
        char backSlash = '\u005C';
                         ^
1 error

Questo perché il codice precedente sarà stato trasformato nel seguente:

char backSlash = '\';

e quindi la coppia \' viene considerata un carattere di escape corrispondente ad un apice ', e quindi manca la chiusura del literal con un altro apice singolo. Invece se consideriamo il carattere ", rappresentato dal numero esadecimale 22 (corrispondente al numero decimale 34), e, rappresentabile con il carattere di escape \", se scriviamo:

char quotationMark = '\u0022';

non ci sarà nessun problema. Ma se useremo questo carattere all'interno di una stringa:

String quotationMarkString = "\u0022";

otterremo il seguente errore in compilazione:

error: unclosed string literal
   String quotationMarkString = "\u0022";
                                       ^
1 error 

visto che il codice precedente sarà stato trasformato nel seguente:

String quotationMarkString = """

Il mistero dell'errore nel commento

Una situazione ancora più strana la si trova quando si utilizzano i commenti ad una linea, per commentare formati Unicode come il carriage return o il line feed. Per esempio, nonostante siano commentate, entrambe le seguenti dichiarazioni darebbero luogo ad errori in compilazione!

// char lineFeed = '\u000A';  
// char carriageReturn = '\u000d'; 

Questo perché i formati esadecimali sono sempre trasformati dal compilatore con i caratteri line feed e carriage return, che non sono compatibili con i commenti ad una riga perché stampano caratteri al di fuori del commento! Per risolvere questa particolare situazione bisogna utilizzare la notazione dei commenti su più righe, per esempio:

/* char lineFeed = '\u000A';  
   char carriageReturn = '\u000d'; */

Un altro errore che può far perdere tanto tempo ad un programmatore, è quando per caso all'interno di un commento si utilizza la sequenza \u. Per esempio, con il seguente commento, otterremo un errore in compilazione:

/*
 * Il file verrà generato all'interno della cartella C:\users\claudio
 */

Infatti il compilatore non trovando una sequenza di 4 caratteri esadecimali valida dopo \u, stamperà il seguente errore:

error: illegal unicode escape
 * Il file verrà generato all'interno della cartella C:\users\claudio
                                                         ^
1 error

Conclusioni

Il questo articolo abbiamo visto che l'utilizzo del tipo char in Java, nasconde dei casi particolari davvero sorprendenti. In particolare, abbiamo visto che è possibile scrivere codice Java, utilizzando il formato Unicode. Questo perché il compilatore prima trasforma in carattere il formato Unicode, e poi valuta la sintassi. Questo implica che i programmatori possono trovare errori di sintassi proprio dove non se lo aspetterebbero mai, ovvero all'interno dei commenti.

Note dell'autore

Questo articolo rappresenta un breve estratto del paragrafo 3.3.5 Primitive Character Data Type dal primo volume del mio libro in inglese intitolato "Java for Aliens". Per maggiori informazioni, potete visitare www.javaforaliens.com, dove potrete scaricare l'intero paragrafo 3.3.5 (e altre anteprime) nella sezione Samples. Questo articolo è presente su questo blog anche in versione inglese, sul portale DZone.

Lascia un commento

Una email sarà mandata a
claudio@claudiodesio.com


Caricamento
Il tuo messaggio è stato mandato. Grazie!