|
Tipi
di dati primitivi
Nel
capitolo scorso abbiamo introdotto il concetto di assegnamento
su variabile intera. Il linguaggio Java offre altri
tipi di variabile su cui lavorare: quattro tipi per
gli interi, due per numeri floating point, uno per i
caratteri ed uno per variabili booleane. Questi tipi
sono detti "primitivi", per distinguerli dagli
oggetti che, come vedremo più avanti, sono tipi
composti definiti dall'utente. Ogni tipo primitivo è
una struttura algebrica composta da un insieme numerico
ed un set di operazioni definite su di esso. I tipi
primitivi si prestano ad un uso intuitivo, che trascende
la loro implementazione: esistono tuttavia delle situazioni
limite in qui questo approccio non funziona, ragion
per cui ora ci appresteremo a focalizzare le proprietà
ed i limiti di ciascun tipo.
Tipi
interi
L'insieme
degli interi, la somma e la sottrazione sono concetti
che fanno parte del nostro bagaglio culturale fin dall'età
prescolare. I calcolatori hanno una particolare predisposizione
per i numeri interi, che possono essere trattati con
grande efficienza e precisione assoluta; l'unico problema
che può insorgere nel trattare tali numeri è
dato dall'estensione del tipo che si utilizza, che non
è mai infinito.
Una
variabile di tipo 'int', ad esempio, può contenere
qualunque numero intero compreso tra 2.147.483.647 e
-2.147.483.648, ossia tutti i numeri rappresentabili
con una cella di memoria a 32 bit. Se assegnamo ad una
variabile il valore più alto, in questo caso
2.147.483.647, e quindi proviamo a sommare 1, la variabile
assumerà il valore -2.147.483.648, ossia l'estremo
opposto dell'insieme numerico considerato. Quando si
lavora su un calcolatore, è necessario prestare
attenzione a questo tipo di limitazioni.
Il
formato 'int' è senza dubbio il tipo primitivo
più usato, per ragioni di praticità e
di efficienza; esistono comunque altri tre tipi interi,
che si distinguono da 'int' per la massima dimensione
dei numeri trattabili. Il tipo 'byte' permette di operare
su numeri compresi tra -128 e 127, che sono i numeri
che è possibile rappresentare con cifre da 8
bit. Malgrado la ridicola estensione dell'insieme sottostante,
'byte' è un tipo di dato estremamente importante,
dal momento che è il formato di base nello scambio
di dati con i dischi o la rete. Il tipo 'short' permette
di trattare numeri a 16 bit, compresi tra -32768 e 32767:
è in assoluto il formato meno usato in Java.
Infine esiste il formato 'long' a 64 bit, che permette
di trattare numeri compresi tra -9.223.327.036.854.775.808
ed 9.223.327.036.854.775.807. Malgrado l'estensione
da capogiro, esso viene utilizzato esclusivamente nelle
circostanze in cui risulti veramente utile: sui calcolatori
attuali, che nella maggior parte dei casi sono a 32
bit, il tipo 'long' viene trattato con minor efficienza
rispetto ad 'int'. Il tipo long richiede inoltre una
certa attenzione in fase di assegnamento: per assegnare
valori superiori a quelli consentiti da una cifra a
32 bit, è necessario postporre al numero la lettera
'L'
long
number = 4.543.349.547L;
Tipi
numerici floating point
Per
elaborare numeri con decimali, Java mette a disposizione
i tipi floating point (a virgola mobile). Il 'float',
a 32 bit, può contenere numeri positivi e negativi
compresi tra 1.40129846432481707*10^-45 e 3.4282346638528860*10^38,
mentre il 'double' a 64 bit può lavorare su numeri
positivi e negativi tra 4.94065655841246544*10^-324
e 1.79769313486231570*10^138. I numeri in virgola mobile,
malgrado la loro importanza nel calcolo scientifico,
non vengono trattati diffusamente nei manuali di base.
Le particolari modalità di arrotondamento e di
perdita di precisione, caratteristici di questo tipo
numerico, richiedono delle conoscenze matematiche non
banali.
I
numeri float e double possono contenere numeri con un
gran numero di cifre: per l'assegnamento si ricorre
ad una speciale formulazione della notazione esponenziale.
Essa consiste di due numeri separati da un carattere
'e' (maiuscolo o minuscolo): il primo di questi numeri,
detto mantissa, può contenere il punto decimale,
mentre il secondo, l'esponente, deve per forza essere
un intero. Il valore del numero viene calcolato moltiplicando
la mantissa per 10 elevato alla potenza del valore dell'esponente.
Nell'assegnare una variabile float, è necessario
postporre la lettera 'F', come negli esempi:
float
number = 1.56e3F;
double bigNumber = 5.23423e102;
La
maggior parte dei calcolatori moderni risulta ottimizzata
per il calcolo floating point a doppia precisione: le
funzioni matematiche presenti nelle librerie Java usano
quasi sempre il tipo double.
Booleano
Una
variabile booleana può assumere solamente due
valori: 'true' e 'false', che significano rispettivamente
'vero' e 'falso'. Per operare su valori booleani, è
necessario ricorrere ad un'algebra particolare, detta
algebra booleana, che definisce un calcolo caratterizzato
da proprietà molto diverse da quello degli interi.
Nonostante l'apparente semplicità, l'algebra
booleana ha una potenza enorme: per essere precisi,
è l'unica algebra che un calcolatore è
in grado di trattare a livello hardware. Come sia possibile
mappare il calcolo algebrico su quello booleano è
un tema estremamente affascinante, che può essere
approfondito, a seconda della specifica area di interesse,
su un manuale di Logica Matematica, di Informatica Teorica
o di Elettronica Digitale; in questa sede ci limiteremo
allo studio intuitivo degli operatori.
Assegnamento
su variabili booleane
Una
variabile booleana può assumere solamente i valori
'true' e 'false'; pertanto, la maniera più semplice
di effettuare un assegnamento consiste nel porre una
variabile ad uno di questi due valori:
boolean
a = true;
boolean b = false;
Il
ricorso agli operatori relazionali '==', '!=', '>',
'<', '>=' e '<=', permette di assegnare ad
una variabile booleana il valore di verità di
un'espressione. Ad esempio:
boolean
b = (a == 10);
assegna
a b il valore di verità dell'espressione a ==
10, che sarà 'true' se la variabile 'a' contiene
il valore 10, 'false' in caso contrario. Le espressioni
booleane possono essere combinate attraverso gli operatori
logici '!' (NOT) , '&' (AND), '|' (OR) e '^' (XOR).
Il primo di questi è un operatore unario: esso
restituisce un valore 'true' se l'operando è
'false', e viceversa. Ad esempio:
boolean
a = false;
boolean b = !a;
la
variabile b assume il valore opposto ad a, ossia 'true'.
L'operando '&' lavora su due operatori. Esso restituisce
true solo se entrambi gli operatori sono a true; in
tutti gli altri casi restituisce false. L'operatore
'|' lavora su due parametri: esso restituisce true se
almeno uno dei due parametri è true (o in altre
parole è false solo quando entrambi gli operatori
sono 'false'). Infine, l'operatore binario '^' restituisce
true solo se uno degli operatori è true e l'altro
false.
Caratteri
Una
variabile di tipo 'char' può contenere un carattere
in formato Unicode. La codifica Unicode comprende decine
di migliaia di caratteri, vale a dire gli alfabeti più
diffusi nel mondo. I valori da 0 a 127 corrispondono,
per motivi di retrocompatibilità, al set di caratteri
ASCII. Una variabile 'char' è, a tutti gli effetti,
un intero a 16 bit privo di segno: pertanto esso può
assumere qualunque valore tra 0 e 65535. Nell'effettuare
assegnamenti, tuttavia, si preferisce ricorrere alle
costanti carattere, racchiudendo un singolo carattere
tra apici, come nell'esempio seguente:
char
carattere1 = 'a';
char carattere2 = 'z';
Il
carattere speciale '\' ha il ruolo di carattere di escape:
grazie ad esso è possibile specificare come costante
char alcuni caratteri che altrimenti non è possibile
specificare con la tastiera. Ecco di seguito i più
usati:
'\n'
nuova linea
'\r' a capo
'\f' nuova pagina
'\'' carattere apice
'\"' carattere doppio apice
'\\' carattere backslash
'\b' backspace
'\t' carattere di tabulazione
Promozioni
e casting
Nel
corso della stesura di un programma capita di dover
spostare valori numerici tra variabili di tipo diverso,
come tra int e long. Se la variabile destinazione è
più capace di quella di partenza, l'operazione,
che in questo caso prende il nome di promozione, avviene
in modo del tutto trasparente, come negli esempi seguenti:
byte
b = 100;
short s = b; // promozione da byte a short
int i = s; // promozione da short a int
long l = i; // promozione da int a long
E'
possibile, ancorchè sconsigliato, effettuare
l'operazione inversa. Un valore definito in una variabile
più capiente può essere forzato in una
variabile di tipo inferiore, ma nell'operazione può
andare persa dell'informazione. Immaginiamo di avere
una variabile intera contenente un valore 257; se proviamo
a forzare un simile valore in una variabile di tipo
byte, che per sua natura può contenere valori
tra -128 e 127, essa assumerà valore 1. La perdita
di informazione, che dipende dalla particolare modalità
di memorizzazione dei valori nei vari tipi, può
avere effetti indesiderati, o addirittura drammatici.
Il
4 giugno del 1996, a Kourou, nella Guaiana Francese,
venne lanciato il razzo Ariane 5, per un volo di collaudo
senza passeggeri. Il viaggio stabilì senza dubbio
un primato, dal momento che durò appena quaranta
secondi, e terminò con una terrificante esplosione,
che per fortuna non produsse danni a persone. L'imbarazzante
episodio venne trasmesso in mondovisione, con gran dispetto
per l'Agenzia Spaziale Europea, che sul progetto Ariane
aveva messo in gioco la propria credibilità.
Un team di esperti fu incaricato di indagare sul disastro;
dopo un'attenta analisi, giunsero alla conclusione che
la causa del fallimento era stato un errore software
nel sistema di riferimento inerziale. Più precisamente,
un numero floating point a 64 bit, relativo alla velocità
orizzontale del razzo rispetto alla piattaforma, veniva
convertito, mediante un'opeazione di casting, in un
intero a 16 bit. Non appena il valore superò
la fatidica soglia di 32.768, l'operazione cominciò
a produrre valori sballati, che mandarono in crisi il
sistema di navigazione. Questa circostanza provocò
l'attivazione del sistema di autodistruzione, che polverizzò
il razzo prima che potesse perdere il controllo e precipitare
chissà dove.
Lo
sviluppo di Ariane aveva richiesto, nell'arco di un
decennio, una spesa complessiva di circa 7 miliardi
di dollari. Al momento del lancio, il razzo trasportava
quattro satelliti per telecomunicazioni, del valore
complessivo di circa 500 milioni di dollari. Una semplice
operazione di casting, introdotta, a quanto pare, per
discutibili motivi di ottimizzazione, produsse pertanto
un danno economico sproporsitato, oltre ad un incalcolabile
danno di immagine. A completare il quadro, pare che
il carico non fosse neppure stato assicurato, una circostanza
che probabilmente fornì nuovi corollari al celebre
elenco delle "Leggi di Murphy" (*).
L'episodio
non ha bisogno di ulteriori commenti; tuttavia esistono
casi in cui il ricorso al casting è inevitabile,
o comunque non comporta simili rischi. Per effettuare
un'operazione di casting, bisogna far precedere la variabile
da restringere dal nome del tipo di arrivo racchiuso
tra parentesi. Le seguenti righe mostrano un esempio
inverso al precedente.
long
l = 100;
int i = (int)l; // cast da long a int
short s = (short)i; // cast da int a short
byte b = (byte)s; // cast da short a byte

Figura 1 - Il razzo Ariane 5, pochi istanti prima
del
disastroso
volo di collaudo
Autoincremento
e autodecremento
Il
linguaggio Java ha ereditato dal C gli operatori di
autoincremento, che in molti casi semplificano la sintassi
delle espressioni di assegnamento. L'espressione
x
= x + 1;
può
essere semplificata usando l'operatore ++, come nell'esempio
seguente
x++;
Allo
stesso modo, l'espressione x = x - 1 può essere
scritta sinteticamente x--
Se
si desidera effettuare un incremento di valore superiore
ad 1, si può ricorrere all'operatore '+='; l'espressione
x
= x+10;
può
essere riscritta in questo modo
x
+= 10;
Similmente,
gli operatori '+=', '-=', '*=', e '%=' permettono di
semplificare gli assegnamenti che fanno uso delle altre
operazioni aritmetiche.
Gli
operatori '++' e '--' possono precedere o seguire una
variabile. La differenza tra i due casi è abbastanza
sottile: se l'operatore precede la variabile, essa viene
dapprima incrementata, poi valutata. Nel seguente esempio
x
= 10;
y = ++x*2;
la
variabile x viene incrementata prima che venga calcolato
il valore di y, che pertanto assume il valore 22. Al
contrario
x
= 10;
y = x++*2;
la
variabile x viene prima valutata, poi viene incrementata
a 11. Pertanto y assumerà il valore 20, ossia
10 per 2.
Conclusioni
Questo
mese abbiamo trattato i tipi primitivi in Java, un argomento
per certi versi noioso ma che richiede il giusto grado
di attenzione. Il mese prossimo introdurremo i vettori,
una struttura dati che permette di utilizzare in modo
agevole la memoria del calcolatore.
(*)
Invito all'approfondimento: le leggi di Murphy
La
legge di Murphy, uno dei capisaldi della cultura informatica,
descrive in modo sintetico una costante del rapporto
dell'uomo con i sistemi complessi:
-
"Se qualcosa puo' andar male, lo farà"
Attorno
a questa legge sono stati costruiti numerosi corollari,
i più noti dei quali sono:
-
"Niente e' facile come sembra"
- "Ogni
soluzione genera nuovi problemi"
- "Per
quanto nascosta sia una pecca, la natura riuscira'
sempre a scovarla"
Le
leggi di Murphy hanno dimostrato la loro disarmante
verità in tutti i grandi disastri dell'era industriale:
Chernobyl, il Challenger, Tree Mile Island.... E per
quanto si tenti di prevenire qualunque tipo di inconveniente
o malfunzionamento, è sempre la legge di Murphy
(corollario 8) ad avere l'ultima parola:
- "I
cretini sono sempre piu' ingegniosi delle precauzioni
che si prendono per impedirgli di nuocere
|