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 decimale97
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).
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).
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 tastoDelete
)\n
che equivale ad un line feed ovvero ad un andare a capo (equivalente al tastoInvio
)\\
che equivale ad un solo\
(proprio perché il carattere\
serve per i caratteri di escape)\t
che equivale ad una tabulazione orizzontale (equivalente al tastoTAB
)\'
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)
'"'
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.