|
Java: Arte e tecnica
Sintassi e struttura del linguaggio-Parte II
Dario de Judicibus
Dopo aver descritto parole, tipi di operatori, separatori e commenti;
illustrato cos’Φ un’espressione, continuiamo la nostra analisi dalla
trattazione delle istruzioni.
Paragonando un linguaggio di programmazione a una vera lingua possiamo affermare che
l’insieme delle parole chiave e degli operatori utilizzati ne rappresentano il
vocabolario, mentre i simboli di raggruppamento e separazione corrispondono alla
punteggiatura. Il modo poi di combinare le parole riservate e i vari simboli con gli
identificatori e gli oggetti definiti dal programmatore forma quella che si pu˜ a
ragione definire la sintassi del linguaggio.
Lo scorso numero abbiamo descritto le parole riservate a Java, i vari tipi di
operatori, i separatori e i commenti. Abbiamo illustrato cos’Φ un’espressione
evidenziando come la sintassi di questo linguaggio sia fortemente caratterizzata dal fatto
che Java ha l’architettura di un linguaggio interpretato, per cui, non esistendo il
concetto di risoluzione dei collegamenti (link-edit), non prevede l’esistenza
di costrutti che non possono essere risolti durante l’esecuzione. Abbiamo anche
iniziato a trattare le istruzioni; ne esistono di tre tipi: le dichiarazioni, le
espressioni e le istruzioni di controllo del flusso. Propriamente, perchΦ
un’espressione possa essere considerata un’istruzione, essa deve terminare con
un punto e virgola. Pi∙ istruzioni racchiuse fra parentesi graffe formano un blocco di
istruzioni, o se vogliamo un’istruzione composta. In seguito useremo il termine
istruzione per indicare tanto la singola quanto un blocco di istruzioni, a meno che non
sia altrimenti specificato.
Le istruzioni per il controllo del flusso sono praticamente le stesse del C e del C++,
ovverosia i vari tipi di cicli condizionati (for, while, do-while)
e i costrutti di scelta (if-else, switch-case-default), come riportato nelle tabelle 1 e 2.
Istruzioni,
etichette, tipi, classi
Esistono, inoltre, due istruzioni di salto incondizionato (break, continue)
che possono essere utilizzate per uscire dai cicli in modo pulito, in accordo con le
regole della programmazione strutturata. L’istruzione break pu≥
essere utilizzata anche per uscire dal costrutto di scelta plurima switch. Θ
anche possibile associare a un’istruzione un’etichetta, cioΦ un identificativo
seguito dal carattere due punti. Le etichette possono essere applicate a qualunque
istruzione, ma di fatto servono solo nel caso di cicli annidati, quando si vuole indicare
esplicitamente da quale ciclo il salto incondizionato deve uscire. In Java non esistono le
istruzioni per il preprocessore come in C++, quali, per esempio, #include o
#define e non Φ permesso il goto. Inoltre, non esistono le strutture (struct),
le unioni (union) e le enumerazioni (enum),
perchΦ tutte queste collezioni di dati possono essere sostituite in modo pi∙ pulito da
opportune classi. Infine, non si usa pi∙ il qualificatore const,
sostituito dalla parola chiave final. Un altro elemento molto usato in C e C++ ma proibito in
Java Φ typedefs, dato che esso pu≥ generare confusione, problemi di
portabilitα ed Φ sempre possibile ridisegnare un programma in modo da evitarlo. In
effetti, chi ha disegnato Java ha cercato di semplificare al massimo il linguaggio
evitando costrutti ridondanti o che, per loro natura, potevano indurre in errore lo
sviluppatore. Nei prossimi numeri descriveremo come e perchΦ Java non abbia neanche le
funzioni e i puntatori. In Java esistono solo quattro tipi semplici, e cioΦ gli interi,
i numeri reali a virgola mobile, i booleani e i caratteri,
pi∙ tre tipi composti, e precisamente le matrici, le classi e le interfacce.
Al contrario dello SmallTalk, tuttavia, i tipi semplici non sono classi, anche se esistono
librerie standard di Java che permettono di rivestirli con classi vere e proprie
(wrapper). Il motivo per cui non si sono trasformati tutti i tipi in classi, come in altri
linguaggi orientati agli oggetti, Φ che tale approccio avrebbe inutilmente appesantito il
linguaggio creando seri problemi di prestazioni. Java Φ infatti un linguaggio pensato per
la rete, dove la cura nell’utilizzo delle risorse Φ fondamentale per evitare di
saturare la larghezza di banda. Effettivamente, anche l’architettura da linguaggio
interpretato affonda le sue radici nel carattere prettamente sequenziale del trasferimento
di dati in rete. Se ci pensate, la maggior parte delle variabili e degli oggetti di un
programma appartiene a uno dei quattro tipi semplici, per cui trasformarli in classi
avrebbe comportato un notevole aumento delle dimensioni del programma; una classe anche
minimale necessitα comunque di molti pi∙ byte di un singolo dato. I vari tipi sono
riportati in tabella 3.
Da notare che tutti i numeri sono con segno, che esistono quattro diversi tipi di
interi e due tipi di numeri a virgola mobile, e che ogni tipo ha una lunghezza in byte ben
definita indipendente dalla piattaforma su cui gira la macchina virtuale Java, cioΦ il
motore che interpreta il programma e lo esegue. In Java non pu≥ succedere come in C e C++
che il tipo int occupi 16 bit in un sistema e 32 in un altro. Il codice
Java non esistono le librerie di oggetti come in C, ma i cosiddetti pacchetti (package).
Un pacchetto non Φ altro che un insieme di classi raggruppate logicamente. La struttura dei
pacchetti Φ di tipo gerarchico, e cos∞ la loro nomenclatura. Per esempio, tutti i pacchetti
che contengono le classi standard di Java iniziano con il nome java
seguito da un punto. Abbiamo cos∞ java.lang e java.awt, tanto per citarne due. Se vogliamo referenziare una
classe specifica di un pacchetto, faremo precedere al nome della classe il nome del
pacchetto che la contiene, introducendo un punto di separazione. Avremo quindi nomi come java.lang.String, java.awt.Graphics e
java.applet.Applet. Dato che sarebbe alquanto
scomodo usare sempre i nomi delle classi nella loro forma estesa, cosa che renderebbe
alquanto pesante leggere il codice sorgente, Java permette di usare un’istruzione
particolare chiamata import, da posizionare in testa al file che contiene il codice.
Questa istruzione pu≥ essere considerata simile all’#include
del C, con la differenza che permette di importare tanto una singola classe quanto
l’insieme di tutte le classi di un pacchetto. Se, per esempio, ci serve usare solo la
classe Applet del pacchetto java.applet, allora scriveremo in
testa al nostro file import
java.applet.Applet;
Se invece ci servono un po’ tutte le
classi grafiche necessarie a disegnare un’interfaccia interattiva, scriveremo import java.awt.*;
Da notare come import
rappresenti un’istruzione a tutti gli effetti, al contrario della #include,
e quindi richiede il punto e virgola di chiusura. L’unico pacchetto che non richiede
l’uso di istruzione di importazione Φ java.lang, perchΦ automaticamente
importato dal compilatore. Se volete crearvi un vostro pacchetto, dovrete usare
l’istruzione package in testa a ogni file contenente una classe del pacchetto.
Tenete presente che ogni file pu≥ contenere una sola classe pubblica, cioΦ visibile
all’esterno del pacchetto.
Descrizioni, proprietα e regole
E veniamo alle dichiarazioni. Esse sono utilizzate per definire le proprietα di una
classe, di un metodo, di una variabile, di una costante e cos∞ via. La loro sintassi
dipende da cosa stiamo dichiarando. Nel descrivere le varie dichiarative, useremo le
seguenti regole:
- le parole riservate, i separatori e le parentesi sono riportate in grassetto.
- gli elementi da sostituire con parole riservate o identificativi sono in corsivo.
- le parti opzionali sono racchiuse da una coppia di parentesi quadre.
- le liste sono indicate da tre puntini.
Fate quindi attenzione a non confondere le parentesi che fanno parte della sintassi,
riportate in grassetto ([ e ]), da quelle usate per descriverla ([ e ]). La
barra verticale (|) viene infine utilizzata per separare scelte mutuamente esclusive.
Consideriamo prima la dichiarazione delle variabili. La sintassi Φ la seguente [accesso] [static] [final] [transient]
[volatile]
tipoVar nomeVar [= valoreIniziale]; dove tipoVar Φ il tipo di variabile e nomeVar Φ
l’identificativo assegnato dal programmatore. Il tipo pu≥ essere semplice, oppure il
nome di una classe o una matrice. Nel primo caso Java assegna effettivamente alla
variabile un’area di memoria. Nel secondo caso, la dichiarazione non fa altro che
informare Java che si intende creare un oggetto appartenente a quella classe.
L’allocazione vera e propria dovrα essere fatta con il metodo di classe new, come
vedremo in uno dei prossimi numeri. Questo punto Φ importante. Come accennato poco sopra,
in Java non esistono i puntatori, come del resto anche in SmallTalk. Un identificatore
pu≥ quindi rappresentare un oggetto oppure un riferimento ad esso a seconda che sia di
tipo semplice o composto. Questo vuol dire tra l’altro che in Java mancano gli
operatori di indirizzo (&) e di deriferimento (*). In
tabella 4 sono riportati alcuni esempi. Ci sono poi cinque attributi opzionali posti
davanti al tipo. Il primo serve a indicare quali altre classi hanno accesso alla variabile
in questione. I vari tipi di accesso sono riportati nella tabella 5 insieme al loro
significato.
Degli altri quattro attributi due sono utilizzati sin dalla prima versione di Java,
mentre gli altri due sono riservati a versioni future del linguaggio. Il primo, static, indica che la variabile Φ una variabile di classe, cioΦ Φ condivisa da tutti
gli oggetti appartenenti alla stessa classe. Il secondo, final, indica che la variabile Φ in realtα una costante e non pu≥ essere
modificata durante l’esecuzione del programma. Il terzo, transient,
identifica quelle variabili che non fanno parte dello stato persistente dell’oggetto.
Questo vuol dire che se l’oggetto Φ salvato su memoria di massa per poter essere
successivamente ricaricato da un programma, le variabili di questo tipo non sono
memorizzate. Il quarto, volatile, si riferisce a variabili che possono essere modificate in
modo asincrono da pi∙ threa in esecuzione contemporanea. Questo permette a
Java di assicurarsi che la variabile abbia sempre un valore coerente all’interno di
ciascuna thread.
Il valore iniziale della variabile pu≥ essere specificato contestualmente alla
dichiarativa sia che si tratti di un valore fisso (literal) sia che si tratti di una
espressione. Da notare che in Java, a differenza del C, tutte le variabili dichiarate
hanno un valore predefinito. Questo riduce la possibilitα di errori dovuti a variabili
non inizializzate. Tutti i numeri interi e reali sono inizializzati a zero, i caratteri al
carattere nullo (\u0000), i booleani a false e i riferimenti a un qualunque
oggetto a null. Inoltre, Φ possibile assegnare a una costante il proprio
valore solamente nella dichiarazione della variabile. Una matrice si dichiara come una
variabile, utilizzando una coppia di parentesi quadre per ogni dimensione vettoriale. Per
cui la dichiarazione sarα
[accesso]
[static] [final] [transient] [volatile]
tipoMatr[][[]...] nomeMatr [= valoreIniziale];
In realtα le parentesi quadre possono essere posizionate tanto dopo il tipo quanto
dopo il nome della matrice. Da notare inoltre che, al contrario del C, la dichiarazione
non contiene il numero di elementi per ogni dimensione. Tale informazione dovrα essere
fornita solo quando la matrice sarα effettivamente allocata. La dichiarazione di una
classe segue invece il modello
[accesso] [abstract|final]
class NomeClasse
[extends SuperClasse] [implements NomeInterfaccia...]
{
dichiarazioni delle variabili di classe
dichiarazioni dei metodi
}
dove NomeClasse Φ il nome della classe. L’unico modificatore di
accesso valido per una classe Φ public. Se specificato, vuol dire che la classe pu≥ essere
utilizzata da classi appartenenti ad altri pacchetti, altrimenti essa potrα essere
utilizzata solo da altre classi dello stesso pacchetto. Una classe di tipo abstract
non pu≥ essere "istanziata", non Φ possibile cioΦ creare oggetti di quella
classe. Essa pu≥ essere utilizzata solo come superclasse da cui derivare altre classi.
Una classe final Φ proprio l’opposto: essa non pu≥ essere
"sottoclassata". Ovviamente i due modificatori si escludono a vicenda. Se non
viene specificata una superclasse utilizzando la chiave extends,
allora la classe in questione Φ una sottoclasse della classe Object. Se
inoltre la classe implementa una o pi∙ interfacce, sarα utilizzata la parola chiave implements.
Un’interfaccia Φ simile a una classe ma non pu≥ essere istanziata. Le interfacce
sono utilizzate come meccanismo di ereditarietα multipla. In pratica sono un po’
come una classe astratta, dato che contengono solo costanti e metodi non implementati. Non
hanno cioΦ nΘ variabili, nΘ istruzioni all’interno dei metodi. La dichiarazione Φ
la seguente:
[accesso] [abstract] interface NomeInterfaccia
[extends AltraInterfaccia...]
{
dichiarazioni delle costanti dell’interfaccia
dichiarazioni dei metodi (vuoti)
}
Anche in questo caso l’unica chiave di accesso ammessa Φ quella pubblica, mentre
il modificatore abstract Φ permesso ma di fatto inutile, in quanto implicito nella
definizione d’interfaccia. La dichiarazione dei metodi all’interno di una classe
Φ, infine,
[accesso]
[abstract|final] [static] [native] [synchronized]
risultato NomeMetodo (argomenti...)
[throws eccezioni...]
{
dichiarazioni delle variabili di metodo
istruzioni
}
dove nomeMetodo Φ il nome del metodo e i modificatori di accesso sono gli
stessi delle variabili, e cioΦ public, protected e private, mentre risultato Φ il tipo della variabile il cui valore Φ restituito dal
metodo, se esiste. I metodi astratti non hanno un’implementazione, sono cioΦ vuoti,
e possono essere definiti solo nelle classi astratte. Inoltre, non possono essere nΘ
privati, nΘ statici o costanti. Un metodo finale non pu≥ essere ridefinito in una
sottoclasse. I metodi nativi, invece, sono dipendenti dal sistema operativo e sono
sviluppati con un linguaggio differente da Java, come il C o il C++. Del meccanismo di
sincronizzazione tratteremo un’altra volta, cos∞ come delle eccezioni e della parola
chiave throws. In quella sede parleremo anche di un altro costrutto,
ovverosia try-catch.
(ddejudicibus@tecnet.it)
Tabelle
Tabella 1 - Istruzioni di controllo del flusso: scelte
Istruzione |
Descrizione |
if (espressione) istruzione
else
istruzione |
Se l’espressione in
parentesi Φ vera, viene eseguita la prima istruzione, altrimenti viene eseguita la
seconda istruzione. |
if (espressione) istruzione
else if (espressione)
istruzione
else
istruzione |
Se l’espressione in
parentesi Φ vera, viene eseguita la prima istruzione, altrimenti viene valutata la
seconda espressione. Se questa risulta vera viene eseguita la seconda istruzione,
altrimenti viene eseguita la terza istruzione. |
Switch {
case espressione-costante:
istruzione
case espressione-costante:
istruzione
default:
istruzione
} |
Le varie espressioni sono
valutate dall’alto in basso, in sequenza. Se un’espressione Φ vera, viene
eseguita l’istruzione corrispondente, altrimenti si passa all’espressione
successiva. Le varie espressioni devono essere costanti e il flusso passa attraverso i
vari blocchi a meno che non venga incontrata un’istruzione di salto incondizionato
prima dell’espressione successiva (break). |
Tabella 2 - Istruzioni di controllo del flusso: cicli e salti
Istruzione |
Descrizione |
while (espressione) istruzione |
L’istruzione viene
eseguita fintanto che l’espressione in parentesi Φ vera. L’istruzione pu≥ non
venire mai eseguita se l’espressione Φ falsa fin dall’inizio. |
Do Istruzione
while (espressione); |
L’istruzione viene
eseguita fintanto che l’espressione in parentesi Φ vera. L’istruzione viene
comunque eseguita almeno una volta. |
for (espr1; espr2;
espr3) istruzione |
Equivale al costrutto espr1;
while (espr2)
{
istruzione
espr3;
} |
break; break
etichetta; |
Provoca l’uscita
incondizionata da un ciclo (for, while, do-while) oppure dall’istruzione di scelta plurima switch. ╚
possibile specificare un’etichetta per identificare il ciclo nel caso di costrutti
annidati. |
continue; continue
etichetta; |
Provoca il salto
incondizionato al ciclo successivo nelle istruzioni for, while e do-while. ╚ possibile specificare un’etichetta per identificare
il ciclo nel caso di costrutti annidati. |
Tabella 3 - Tipi semplici, detti anche primitivi
Tipo |
Lunghezza |
Formato e
valori |
byte |
8 bit |
Intero con segno da -128
a 127 |
short |
16 bit |
Intero con segno da
–32.768 a 32.767 |
int |
32 bit |
Intero con segno da
–2.147.483.648 a 2.147.483.647 |
long |
64 bit |
Intero con segno da
–9.223.372.036.854.775.808 a 9.223.372.036.854.775.807 |
float |
32 bit |
Reali a singola
precisione da 32 bit nel formato IEEE 754. Limite positivo superiore:
3.4028234668528860e+38
Limite positivo inferiore: 1.40129846432481707e-45 |
double |
64 bit |
Reali a doppia precisione
da 64 bit nel formato IEEE 754. Limite positivo superiore: 1.79769313486231570e+308
Limite positivo inferiore: 4.94065645841246544e-324 |
char |
16 bit |
Carattere UNICODE da due
byte |
boolean |
n.s. |
True o false. |
Tabella 4 - Esempi di dichiarazioni di variabili e matrici
Variabili |
private MenuBar barra; private
boolean visibile = true;
protected final String NOME = "Dario";
double d = 6.723401;
public Agenda personale = null; |
Matrici |
public static int [ ] vettore = new
int [100]; int x [ ], y [ ];
private float [ ] matrice [ ];
private Point estremi [ ] = linea.estremi(); |
Tabella 5 - Modificatori di accesso
Modificatore |
Descrizione |
public |
L’elemento
Φ accessibile da qualunque classe di qualsiasi pacchetto |
protected |
L’elemento
Φ accessibile da qualunque classe dello stesso pacchetto e da qualsiasi sua sottoclasse
ovunque essa sia. |
private |
L’elemento
Φ accessibile solo dalla classe nella quale Φ definito |
package |
Questo Φ
l’accesso di default se non Φ specificato altrimenti. L’accesso Φ dato a tutte
le classi nello stesso pacchetto, compresa ovviamente la classe in cui l’elemento Φ
definito. |
Accesso |
Classe |
Sottoclasse |
Pacchetto |
Qualunque |
private |
U |
|
|
|
protected |
U |
U (1) |
U |
|
public |
U |
U |
U |
U |
package |
U |
|
U |
|
(1)
L’accesso Φ ammesso solo su oggetti della sottoclasse, non della classe base. |
|