Capitolo 2 - Scrivere ed eseguire un programma Java

i
i
i
i
2
Scrivere ed eseguire
un programma Java
Contenuto
2.1
Scrittura, compilazione ed esecuzione
2.2
La compilazione
2.3
L’esecuzione
2.4
I package e la direttiva import
2.5
Cosa può andare male?
2.6
Esercizi
Nel capitolo precedente abbiamo spiegato che un algoritmo può essere codificato
in un programma scritto in un certo linguaggio formale, e quindi può essere mandato
in esecuzione su di un calcolatore. Vediamo ora concretamente come si può scrivere
ed eseguire un primo semplice programma Java: questo ci permetterà di introdurre
alcuni concetti elementari del linguaggio.
2.1
Scrittura, compilazione ed esecuzione
Java è un linguaggio di programmazione orientato agli oggetti, progettato e realizzato
agli inizi degli anni novanta da James Gosling e da un gruppo di sviluppatori della
Sun Microsystems (acquisita nel 2010 da Oracle). Un programma Java contiene in
generale una o più classi. Ogni classe, identificata da un nome, è una raccolta di
proprietà e funzionalità correlate. Ciascuna funzionalità viene realizzata da un metodo. Ogni metodo ha un nome e contiene una sequenza di istruzioni la cui esecuzione
implementa la corrispondente funzionalità.
Per i primi programmi che presenteremo, identificheremo il programma con una
classe avente lo stesso nome e contenente un metodo di nome main. La funzionalità
del programma sarà quella realizzata dal metodo main. Per esempio, il listato che
segue mostra il programma Hello, il nostro primo programma Java che analizzeremo
in dettaglio in questo capitolo.
/*
*/
Un programma che chiede il tuo nome e ti saluta
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
14
Scrivere ed eseguire un programma Java
import jbook . util . Input ;
public class Hello {
public static void main ( String [] args ){
System . out . print ( " Come ti chiami ? " ); // stampa
String persona ;
persona = Input . readString ();
// legge
System . out . println ( " Ciao " + persona + '! ' ); // stampa
}
}
Listato 2.1: Il programma Hello
Per scrivere il programma possiamo utilizzare un qualunque editor di testi disponibile sul calcolatore. La classe dovrà essere contenuta in un file avente lo stesso
nome della classe, seguito dal suffisso .java; quindi il programma Hello deve essere
scritto in un file chiamato Hello.java. Per eseguire il programma, come anticipato
nella Sezione 1.1, abbiamo bisogno sul nostro calcolatore di un compilatore e di un
interprete per Java. Esistono molte versioni di questi programmi: noi useremo quelli contenuti nel Java Standard Edition Development Kit (JDK) che si può scaricare liberamente dal sito http://www.oracle.com/technetwork/java/javase/
downloads/index.html
Prima di compilare il programma Hello, è necessario copiare nella directory in cui
si trova il file Hello.java la directory jbook e tutto il suo contenuto: questa directory
può essere scaricata dal sito web del libro. Fatto questo, per compilare il programma
Hello possiamo eseguire il comando
$ javac Hello . java
in un interprete di comandi, come una shell sotto Linux o un interprete DOS sotto
Windows.1 Se il programma è stato scritto correttamente, la compilazione produce
un file chiamato Hello.class che contiene la traduzione del programma Hello in
bytecode, cioè in un linguaggio di programmazione di livello intermedio.
A questo punto possiamo mandare in esecuzione il programma con il comando
$ java Hello
Il comando java lancia la Java Virtual Machine, cioè l’interprete che è in grado di
eseguire i programmi scritti in bytecode. L’esecuzione del programma Hello provoca un’interazione con l’utente. Come prima cosa, il programma scrive sul terminale
Come ti chiami? e si mette in attesa di un input da parte dell’utente. Per proseguire l’utente deve scrivere da tastiera una sequenza di caratteri, per esempio Maria,
terminata da un ritorno a capo. A questo punto il programma completa l’esecuzione
scrivendo Ciao Maria!. Rappresentiamo l’interazione come segue, dove evidenziamo
in corsivo l’input da tastiera dell’utente:
1 Con il carattere $ indichiamo genericamente il prompt dell’interprete di comandi, cioè i caratteri da esso
stampati per indicare che è pronto per leggere e interpretare il prossimo comando.
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
2.1 Scrittura, compilazione ed esecuzione
15
Come ti chiami ? Maria
Ciao Maria !
Dando uno sguardo al programma Hello è abbastanza semplice capire che le istruzioni System.out.print(...) e System.out.println(...) permettono di scrivere delle parole sullo schermo, mentre Input.readString() permette di leggere una parola
da tastiera. Nel resto del capitolo descriveremo in dettaglio non solo il significato
delle linee di codice che costituiscono il programma, ma anche l’effetto dei comandi
javac Hello.java e java Hello.
Gli ambienti integrati di sviluppo
Nel descrivere come si può compilare ed eseguire un programma Java, abbiamo assunto di avere a disposizione sul nostro calcolatore un ambiente di sviluppo minimale,
cioè un editor di testi (per la scrittura dei programmi), un compilatore e un interprete
per Java, e naturalmente un interprete di comandi fornito dal sistema operativo che
ci consenta di lanciare l’esecuzione del compilatore e dell’interprete con i comandi
java e javac, come descritto sopra.
In alternativa, per lo sviluppo di programmi Java si può usare un ambiente integrato di sviluppo, chiamato più brevemente IDE, acronimo di Integrated Development Environment. Un IDE normalmente fornisce un editor guidato dalla sintassi
in grado di individuare alcuni errori sintattici già durante la scrittura, e consente di
compilare ed eseguire un programma selezionando opportuni pulsanti (o comandi di
menu, o combinazioni di tasti) dell’interfaccia grafica. In alcuni IDE la compilazione
del programma avviene in maniera totalmente automatica, ogni volta che il sorgente viene modificato tramite l’editor. Oltre a integrare editor, compilatore e ambiente
di esecuzione, molti IDE forniscono agevolazioni quali il completamento automatico
del codice, l’indentazione automatica, l’accesso alla documentazione del linguaggio
e delle librerie, la navigazione ipertestuale dei problemi riscontrati, e l’integrazione
di potenti strumenti per il collaudo e il debugging. Queste caratteristiche facilitano
notevolmente la scrittura di programmi Java e grazie a numerose altre funzionalità disponibili nell’ambiente permettono di gestire in modo efficace lo sviluppo di progetti
Java di grandi dimensioni.
La Figura 2.1 mostra l’aspetto di Eclipse, un IDE che si sta affermando come uno standard di fatto sia in ambito accademico che industriale, al termine della scrittura e dell’esecuzione del programma Hello: oltre al programma nella finestra dell’editor, al centro, si può intravedere in basso l’output risultante dall’interazione con il programma. Eclipse può essere scaricato liberamente dalla URL
http://www.eclipse.org/ .
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
16
Scrivere ed eseguire un programma Java
Figura 2.1: L’ambiente integrato di sviluppo Eclipse
2.2
La compilazione
La traduzione di un programma Java nel corrispondente programma in bytecode effettuata dal compilatore javac è un’operazione alquanto complessa. Durante la generazione dei bytecode il compilatore analizza il programma sorgente effettuando una
serie di controlli. Alcuni di questi controlli servono per verificare la correttezza sintattica, cioè che il programma sia effettivamente una sequenza di simboli generata
dalla grammatica di Java. Altri controlli sono relativi alla cosiddetta semantica statica, e servono a verificare che il programma rispetti alcune regole del linguaggio che
potrebbero essere violate anche da un programma sintatticamente corretto.
Per esempio, con l’analisi di semantica statica si controllano regole come ogni
variabile deve essere dichiarata prima di essere usata, oppure il tipo dell’espressione a destra dell’operatore di assegnamento deve essere compatibile con il tipo della
variabile alla sua sinistra. Se tutte le regole rilevanti sono soddisfatte, il compilatore
completa la traduzione del programma generando il codice intermedio in bytecode.
Altrimenti la traduzione viene interrotta e il compilatore scrive un messaggio descrivendo gli errori individuati. Le regole di semantica statica saranno discusse ampiamente nei prossimi capitoli, man mano che introdurremo i costrutti del linguaggio ai
quali esse si applicano.
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
2.2 La compilazione
2.2.1
17
L’analisi lessicale e l’analisi sintattica
Come accennato sopra, l’analisi sintattica controlla che il programma sia effettivamente generabile dalla grammatica del linguaggio Java. In realtà la “grammatica” di
Java, come quelle di tutti i linguaggi ad alto livello, è formata da due grammatiche
distinte: la grammatica lessicale definisce i token o elementi lessicali del linguaggio, mentre la grammatica sintattica definisce i costrutti veri e propri del linguaggio
usando i token come simboli terminali. Di conseguenza l’analisi sintattica è costituita da due fasi principali: l’analisi lessicale, che legge la sequenza di caratteri che
costituisce il programma e la trasforma in una sequenza di token, e l’analisi sintattica propriamente detta che analizza la sequenza di token risultante e controlla che sia
corretta, cioè che sia generata dalla grammatica sintattica.
Lo studio degli algoritmi che il compilatore utilizza per verificare che un programma sia sintatticamente corretto è un argomento che va al di là degli obiettivi di questo
libro. Presenteremo però in modo sistematico le due grammatiche che definiscono
Java: in questo modo il lettore non solo potrà verificare che gli esempi di programmi che proponiamo siano sintatticamente corretti, ma avrà anche a disposizione uno
strumento per poter generare esempi originali dei vari costrutti del linguaggio.
Le produzioni delle grammatiche di Java presentate, talvolta in modo incrementale, nel corso dei prossimi capitoli sono raccolte nella forma più generale
nell’Appendice A.
2.2.2
I commenti e la formattazione
Durante l’analisi lessicale il compilatore legge la sequenza di caratteri che costituisce il programma e la trasforma in una sequenza di token. Durante questo procedimento, il compilatore ignora i commenti, che sono frammenti di codice che hanno
principalmente la funzione di documentare il programma per gli esseri umani.
Un commento si può estendere su più linee, compreso tra i delimitatori /* e */
come nelle prime linee del file Hello.java:
/*
*/
Un programma che chiede il tuo nome e ti saluta
Oppure si estende dal delimitatore // fino alla fine della linea in cui esso compare,
come nella parte finale della seguente linea del programma:
System . out . println ( " Ciao " + persona + '! ' ); // stampa
Quando scriviamo un programma, per facilitarne la lettura possiamo inserire a piacere commenti, spazi e caratteri di tabulazione, e possiamo spezzare e indentare le linee
a piacimento (tranne che all’interno delle costanti letterali di tipo String, introdotte
nella Sezione 4.5.2). Per capire l’importanza di una buona formattazione, si confronti
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
18
Scrivere ed eseguire un programma Java
il seguente programma con il Listato 2.1: per il compilatore essi sono del tutto equivalenti, poiché differiscono solo per commenti e caratteri non significativi, ma per un
programmatore hanno un grado di leggibilità drammaticamente diverso.
import jbook . util . Input ; public class Hello { public static
void main ( String [] args ){ System . out . print ( " Come ti chiami ? " )
; String persona ; persona = Input . readString (); System . out .
println ( " Ciao " + persona + '! ' );}}
Nei programmi che proporremo verrà usato uno stile standard di formattazione e
indentazione del codice, seguendo le convenzioni reperibili alla URL http://www.
oracle.com/technetwork/java/codeconv-138413.html :2 invitiamo anche
il lettore a seguire questo stile.
2.2.3
I token
Una volta eliminati dal programma i commenti e gli altri caratteri non rilevanti, l’analisi lessicale procede con l’esame della sequenza di caratteri risultante, e individua
al suo interno i token. Ci sono cinque categorie sintattiche di token in Java, come
decritto dalla seguente produzione della grammatica lessicale:
Token
::=
ParolaChiave | Separatore | Operatore |
CostanteLetterale | Id
Le parole chiave sono parole riservate: hanno un significato predefinito nel linguaggio e non possono essere usate in altro modo. Esse sono elencate nella seguente
produzione:3
ParolaChiave ::=
abstract
| assert
case
| catch
continue
| default
enum
| extends
for
| goto
instanceof | interface
new
| package
return
| short
switch
| synchronized
transient
| try
| boolean
| char
| double
| finally
| if
| int
| private
| static
| this
| void
| break
| class
| do
| final
| implements
| long
| protected
| strictfp
| throws
| volatile
| byte
| const
| else
| float
| import
| native
| public
| super
| throw
| while
|
|
|
|
|
|
|
|
|
I separatori e gli operatori del linguaggio sono sequenze di uno o più caratteri
generate dalle seguenti produzioni:
2 Ci
concederemo piccole eccezioni per chiarezza espositiva o per presentare gli esempi in modo più compatto.
curiosità, const e goto non sono usate in Java, anche se sono riservate.
3 Come
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
2.2 La compilazione
19
Separatore ::= ( |
Operatore ::=
=
| > | <
>= | != | &&
/
| & | |
-= | *= | /=
)
|
|
|
|
|
[
|
!
||
ˆ
&=
|
|
|
|
˜
++
%
|=
]
|
|
|
|
{
|
?
-<<
ˆ=
|
|
|
|
|
}
:
+
>>
%=
|
;
|
|
|
|
==
>>>
<<=
,
|
|
|
|
|
|
.
<=
*
+=
|
|
|
|
>>=
>>>=
Spiegheremo il significato dei vari operatori nel Capitolo 5. Le costanti letterali sono
sequenze di caratteri che rappresentano valori di un certo tipo di dati.
CostanteLetterale
::=
LettIntero | LettVirgolaMobile | LettCarattere |
LettStringa | true | false | null
Descriveremo in dettaglio la sintassi delle costanti letterali nel Capitolo 4, quando introdurremo i tipi di dati corrispondenti. Solo a titolo di esempio, sono costanti letterali
corrette gli interi 345 e -12345678, i numeri in virgola mobile 34.56 e 3456e3, i caratteri
'c', '\n' e '\u0041', e la stringa (cioè una sequenza di caratteri) "Come ti chiami? ".
Si noti che oltre alle parole chiave, le uniche altre parole riservate di Java sono le
costanti letterali booleane true e false, e la costante letterale null.
L’ultima categoria di token sono gli identificatori (non-terminale Id). Un identificatore è una sequenza di lunghezza arbitraria composta da lettere, cifre (i caratteri
da 0 a 9) e i caratteri $ e _ (underscore), che non inizi con una cifra e che sia diversa da una parola riservata. Java è un linguaggio case sensitive, ovvero distingue
tra maiuscole e minuscole: ciò vuol dire, per esempio, che gli identificatori pippo e
Pippo sono considerati diversi, e che True (diverso dalla costante letterale true) è un
identificatore ammissibile.
Anche se qualunque sequenza di caratteri che rispetti le regole appena elencate è
riconosciuta come un identificatore dal compilatore, in pratica esistono varie convenzioni nella scelta degli identificatori che i programmatori sono tenuti a seguire. Per
esempio, il carattere $ andrebbe usato solo in programmi generati automaticamente,
mentre _ andrebbe usato solo in nomi di costanti composti da più parole (vedi Sezione 3.5). Vedremo altre convenzioni che riguardano l’uso di maiuscole e minuscole
quando introdurremo le variabili, le costanti, i metodi e le classi.
Le lettere che possono comparire in un identificatore comprendono non solo le
lettere maiuscole e minuscole dell’alfabeto inglese, ma anche le lettere di numerosi
altri alfabeti di tutto il mondo codificati nello standard UNICODE. Comunque, per
garantire una migliore portabilità dei sorgenti dei nostri programmi, noi useremo solo
lettere dell’alfabeto inglese evitando le lettere accentate del nostro alfabeto: questo
perché il supporto fornito dagli editor di testo a queste lettere non sempre è affidabile.
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
20
Scrivere ed eseguire un programma Java
2.2.4
L’analisi sintattica
Per favorire la leggibilità, nel seguito useremo per i listati dei programmi alcune semplici convenzioni tipografiche. Per esempio, il programma Hello del Listato 2.1 sarà
visualizzato così:
1
/*
2
3
*/
Un programma che chiede il tuo nome e ti saluta
4
5
i m p o r t jbook . util . Input ;
6
7
8
9
10
11
12
13
14
p u b l i c c l a s s Hello {
p u b l i c s t a t i c v o i d main ( String [] args ){
System . out . print ( " Come ti chiami ? " ); // stampa
String persona ;
persona = Input . readString ();
// legge
System . out . println ( " Ciao " + persona + '! ' ); // stampa
}
}
Quindi le linee di codice sono numerate a sinistra, le parole riservate di Java sono in
grassetto, mentre i commenti sono resi in corsivo.
Analizziamo in dettaglio la struttura sintattica del programma. Procederemo in
modo informale, introducendo la terminologia e i concetti necessari per descrivere i
costrutti del linguaggio che incontreremo: la presentazione completa di tali costrutti,
con le relative produzioni della grammatica, verrà fatta nei capitoli successivi.
La prima linea significativa del programma è la direttiva di importazione
5
i m p o r t jbook . util . Input ;
Questa direttiva comunica al compilatore l’intenzione di usare nel programma la
classe Input del package jbook.util, come in effetti avviene nella linea 11. Approfondiremo il ruolo dei package, delle clausole di importazione e della classe Input
nelle Sezioni 2.4 e 3.4.1.
Le linee 7–14 contengono la dichiarazione della classe Hello. La sua intestazione,
costituita dalla settima linea, comprende la parola chiave class seguita da un identificatore, il nome della classe, e preceduta dal modificatore di visibilità public di cui
parleremo nel Capitolo 8. Il nome della classe è seguito da una parentesi graffa che
viene chiusa nella linea 14: queste parentesi delimitano il corpo della classe.
Il corpo della classe Hello contiene un solo membro: la dichiarazione del metodo
di nome main, che ha una struttura simile a quella della classe: l’intestazione del
metodo nella linea 8 è seguita dal corpo del metodo nelle linee 9–12, delimitato dalle
parentesi graffe nelle linee 8 e 13. Nell’intestazione, il nome del metodo è preceduto
da alcune parole chiave, ed è seguito dalla lista dei parametri formali racchiusa tra
parentesi: illustreremo tutto questo nel Capitolo 8.
Il (corpo del) metodo main contiene tre istruzioni o comandi e una dichiarazione:
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
2.2 La compilazione
9
10
11
12
21
System . out . print ( " Come ti chiami ? " ); // stampa
String persona ;
persona = Input . readString ();
// legge
System . out . println ( " Ciao " + persona + '! ' ); // stampa
Le istruzioni delle linee 9 e 12 sono due invocazioni o chiamate di metodi: vengono chiamati i metodi print e println dell’oggetto System.out, passando come
argomenti delle stringhe, e precisamente la costante letterale "Come ti chiami? " e
l’espressione "Ciao "+ persona + '!'. La linea 10 è una dichiarazione di variabile:
viene dichiarata una variabile chiamata persona, il cui tipo è la classe String. Infine
la linea 11 contiene un comando di assegnamento, costituito dalla variabile persona
seguita dall’operatore di assegnamento =, seguito a sua volta dell’invocazione del
metodo readString() della classe Input.
La dot notation
Nel listato del programma Hello ci sono diverse occorrenze del separatore punto,
cioè del carattere “.”, con significati diversi. La dot notation, ovvero l’uso del punto
(dot in inglese) come “selettore”, è tipica dei linguaggi di programmazione e dei
formalismi orientati agli oggetti mentre è usata meno spesso in altri paradigmi: essa
necessita quindi di una breve spiegazione. In generale, la notazione
nome1 . nome2
dove sia nome1 che nome2 sono identificatori, rappresenta la “proprietà” o “entità”
nome2 nel contesto del significato di nome1. Il significato preciso dipende da cosa
rappresentano gli identificatori.
Per esempio, Input.readString() denota il metodo readString() contenuto nella classe Input. Analogamente, System.out denota la variabile out contenuta nella
classe System. Infine jbook.util denota il sottopackage util contenuto nel package
jbook (si veda la Sezione 2.4).
La dot notation può essere iterata. Per esempio, System.out.println() denota
il metodo println() dell’oggetto denotato da System.out, mentre jbook.util.Input
denota la classe Input del package jbook.util.
Nel descrivere alcuni metodi offerti dalle API di Java tenderemo a specificare se si
tratta di metodi statici o di metodi d’istanza. La differenza tra i due concetti sarà chiarita nei Capitoli 7 e 10, quando i temi della programmazione orientata a oggetti saranno approfonditi. Per il momento ci basti sapere che i metodi statici vengono invocati
usando come prefisso un nome di classe (per esempio jbook.util.Input.readInt()
invoca il metodo statico readInt() della classe jbook.util.Input) mentre quelli
d’istanza seguono un oggetto che di fatto costituisce un parametro implicito del
metodo (System.out.println() invoca il metodo d’istanza println() sull’oggetto
System.out).
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
22
Scrivere ed eseguire un programma Java
La notazione per i metodi
Java permette di definire e usare più varianti di metodi che hanno lo stesso nome
ma che differiscono per il numero e/o tipo dei parametri che ricevono. Questa caratteristica, chiamata overloading di simboli, è molto importante e verrà discussa più
approfonditamente nel Capitolo 8. Come verrà spiegato nella Sezione 8.3, la firma
di un metodo consiste del nome del metodo e della lista dei tipi dei suoi argomenti. Quindi l’overloading permette la coesistenza di metodi con lo stesso nome ma
firma diversa. A livello di notazione, nel riferire un metodo tramite il nome faremo
uso delle seguenti convenzioni sintattiche: (1) quando intendiamo riferire un particolare metodo indicheremo la firma completa (per esempio readInt(String)); (2)
quando è utile riferire esplicitamente nel testo un particolare argomento del metodo
inseriremo anche un identificatore per ciascun parametro (per esempio il nome msg
in readInt(String msg)); (3) quando intendiamo riferire l’intera famiglia di metodi,
indipendentemente dal numero e tipo dei parametri useremo semplicemente il nome
del metodo seguito dalla coppia di parentesi tonde, come nel caso del metodo senza
parametri (per esempio readInt()).
2.3
L’esecuzione
Lanciando il comando java Hello viene mandata in esecuzione la JVM che interpreta i bytecode del file Hello.class generato dal compilatore. Lo studio del formato
e del significato dei bytecode esula dagli obiettivi di questo libro, e quindi seguiremo la prassi comune di descrivere l’esecuzione del programma facendo riferimento
direttamente al codice sorgente della classe Hello.java.
L’interprete java inizia l’esecuzione dal metodo main della classe passata come
argomento. Se tale classe non avesse un metodo main (con intestazione analoga a
quella della linea 8 di Hello.java) verrebbe segnalato un errore. L’esecuzione del
metodo consiste, nel caso in esame, nell’esecuzione sequenziale delle linee 9–12. Il
primo comando,
System . out . print ( " Come ti chiami ? " ); // stampa
9
invoca una funzionalità fornita dalle librerie di Java, e ha l’effetto di scrivere in una
opportuna finestra dello schermo la stringa
Come ti chiami ?
Più precisamente, System.out (la variabile out contenuta nella classe System) rappresenta lo standard output, che normalmente è una finestra sullo schermo del calcolatore. L’esecuzione del metodo print() invocato su di esso ha l’effetto di scrivere
sullo standard output la stringa passata per argomento, cioè racchiusa tra parentesi
dopo il nome del metodo.
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
2.4 I package e la direttiva import
23
La dichiarazione di variabile della linea 10 e il successivo comando di
assegnamento
String persona ;
persona = Input . readString ();
10
11
// legge
hanno il seguente effetto. Dapprima viene riservata una zona di memoria cui viene associato il nome persona, di dimensione adatta a contenere un riferimento a una stringa
(un oggetto di tipo String). Successivamente viene invocato il metodo readString()
della classe Input che ha l’effetto di leggere una stringa terminata da un ritorno a capo dallo standard input, che tipicamente vuol dire dalla tastiera, e di restituirla come
risultato; infine viene messo un riferimento a questa stringa nella zona di memoria
associata al nome persona. Per concludere, il comando
System . out . println ( " Ciao " + persona + '! ' ); // stampa
12
per prima cosa valuta l’espressione "Ciao "+ persona + '!'. In Java l’operatore +
applicato a due stringhe ne restituisce la concatenazione, cioè una stringa costituita
dai caratteri della prima seguiti dai caratteri della seconda. Pertanto in questo caso il
risultato è la stringa ottenuta concatenando le stringhe "Ciao ", quella memorizzata
nella variabile persona e il singolo carattere '!'. Infine viene stampata questa stringa
sullo standard output passandola come argomento al metodo System.out.println().
Quindi se l’utente ha scritto da tastiera Maria seguita da invio, l’interazione completa
con il programma risulta essere la seguente:
Come ti chiami ? Maria
Ciao Maria !
2.4
I package e la direttiva import
Anche i più semplici programmi Java hanno bisogno, per essere compilati ed eseguiti,
di altre classi, oltre naturalmente a quella che contiene il metodo main. Per esempio,
il programma Hello usa direttamente le classi System, String e Input. Durante la
compilazione di una classe, quando il compilatore incontra il nome di un’altra classe
esso controlla che la classe esista, e se non è già compilata la compila a sua volta.
Le classi di Java sono organizzate in package, i quali formano uno spazio di nomi
gerarchico: un package può contenere classi e/o altri package. Per semplicità si può
pensare ai package come alle cartelle di un file system, ma questa analogia è solo
concettuale: concretamente i package e le classi in essi contenute potrebbero anche
essere memorizzati in un database oppure in un singolo file (come un archivio .jar).
Per le regole sintattiche del linguaggio il nome di un package può essere un qualunque identificatore ammissibile (si veda la Sezione 2.2.3), ma normalmente si usano solo lettere minuscole. Il nome completo di un package è formato dalla sequenza
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
24
Scrivere ed eseguire un programma Java
dei nomi dei package che lo contengono, separati dal carattere '.'. Per esempio,
java.util.concurrent è il nome completo del package concurrent, sottopackage del
package util, a sua volta contenuto nel package java.
La distribuzione di Java comprende centinaia di classi organizzate in decine di
package. Queste classi forniscono un ricco insieme di librerie e di tipi di dati che
possono essere usati liberamente da ogni programmatore per rendere i propri programmi più compatti ed efficienti: non c’è bisogno di riprogrammare funzionalità
già offerte nelle classi della distribuzione, e comunque sarebbe difficile realizzarle in
modo più efficiente.
Ogni classe Java appartiene a un package, che deve essere dichiarato con una opportuna direttiva prima della dichiarazione della classe, e prima di eventuali direttive
di importazione. Per esempio, il file Input.java del package jbook.util inizia nel
seguente modo:
1
p a c k a g e jbook . util ;
2
3
4
5
6
import
import
import
import
java . io . BufferedReader ;
java . io . InputSt reamReader ;
java . io . IOException ;
java . util . Vector ;
7
8
9
10
11
12
/* *
Una semplice classe per leggere stringhe e numeri
dallo standard input .
*/
p u b l i c c l a s s Input {
La direttiva package jbook.util; dichiara appunto che la classe appartiene al package
jbook.util. Questa direttiva può mancare, come nel caso della nostra classe Hello:
in tal caso la classe è considerata appartenere a un package senza nome (unnamed).
Le classi contenute in uno stesso package devono avere nomi diversi, ma si possono avere classi con lo stesso nome in package diversi. Un programma può far
riferimento a un’altra classe usando il suo nome semplice solo se essa si trova nel suo
stesso package oppure nel package java.lang, le cui classi sono parte integrante della
definizione del linguaggio Java (come String e System, usate nell’esempio). Altrimenti occorre usare il nome completo della classe, che comprende anche il package
che la contiene, oppure si può usare una direttiva di importazione.
Nel caso del programma Hello, la presenza della direttiva di linea 5 permette di
invocare il metodo readString() della classe jbook.util.Input semplicemente come
Input.readString() in linea 11. Alternativamente si potrebbe eliminare la direttiva,
a patto di sostituire la chiamata del metodo con jbook.util.Input.readString().
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
2.5 Cosa può andare male?
2.5
25
Cosa può andare male?
Quando si scrive un programma, è del tutto normale commettere degli errori, che possono essere di varia natura e gravità. Gli errori sintattici e quelli di semantica statica
sono rilevati dal compilatore: se il programma non è generabile dalla grammatica
di Java oppure non rispetta tutte le regole di semantica statica, allora la compilazione fallisce, il bytecode non viene generato, e il compilatore stampa una sequenza di
messaggi indicando gli errori individuati.
È estremamente importante imparare a leggere i messaggi di errore del compilatore, per individuare e correggere rapidamente gli errori. Per questo motivo quasi tutti
i capitoli di questo libro contengono una sezione come la presente, in cui vengono
presentati alcuni messaggi di errore tipici, rilevanti per i concetti introdotti nel capitolo stesso. Questi messaggi, anche se differiscono a seconda del compilatore che si
usa, sono in genere molto precisi, e indicano sia la natura dell’errore che il punto del
programma dove esso si è verificato.
Noi descriveremo alcuni messaggi di errore prodotti dal compilatore della distribuzione JDK 7.0 di Java. Data la complessità del linguaggio e del compilatore, sarebbe impossibile presentare tutte le possibili tipologie di messaggi di errore: ci limiteremo quindi a considerare quelle che, nella nostra esperienza, un programmatore
incontra più frequentemente.
Nome di file errato. Supponiamo di aver salvato il programma Hello in un file
chiamato Ciao.java. La compilazione del file produce il seguente output:
$ javac Ciao . java
Ciao . java :7: error : class Hello is public , should be
declared in a file named Hello . java
public class Hello {
^
1 error
Il messaggio di errore ci ricorda che la classe Hello è pubblica, e deve essere dichiarata in un file chiamato Hello.java. Questa regola vale per tutte le classi pubbliche, cioè quelle la cui dichiarazione comincia con la parola chiave public: parleremo
di questo nel Capitolo 10.
Errata dichiarazione del metodo main. Nella Sezione 2.3 abbiamo accennato al
fatto che l’interprete inizia l’esecuzione con il metodo main della classe passatagli
come argomento, e che questo metodo deve avere l’intestazione simile a quella della
linea 8 del programma Hello, cioè:
8
public
s t a t i c v o i d main ( String [] args ){
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
26
Scrivere ed eseguire un programma Java
Senza anticipare il significato delle varie parti dell’intestazione che saranno discusse successivamente, da un punto di vista puramente sintattico l’intestazione del
metodo main deve soddisfare le seguenti condizioni:
• main deve essere preceduto da void;
• void deve essere preceduto da public e da static, in qualunque ordine;
• il contenuto delle parentesi dopo main deve cominciare con String, seguito da
un coppia di parentesi quadre ([]) o da tre punti (...), e infine un qualunque
identificatore. L’identificatore può anche precedere le parentesi quadre.
Se una classe sintatticamente corretta non contiene un metodo main che soddisfi
tutte queste condizioni, quando si lancia l’esecuzione l’interprete segnalerà un errore:
$ java Prova
Error : Main method not found in class Prova , please
define the main method as :
public static void main ( String [] args )
Omissione di separatori. Supponiamo ora che scrivendo il programma Hello sia
stato omesso il separatore ; alla fine della direttiva import di linea 5:
1
/*
2
3
*/
Un programma che chiede il tuo nome e ti saluta
4
5
i m p o r t jbook . util . Input
6
7
p u b l i c c l a s s Hello {
In questo caso la compilazione del programma Hello produce il seguente output:
Hello . java :5: error : '; ' expected
import jbook . util . Input
^
1 error
La prima linea del messaggio di errore comunica all’utente in quale linea di codice
è stato individuato l’errore (Hello.java:5, cioè la quinta linea del file Hello.java),
nonché la tipologia dell’errore: ';'expected, cioè ci si aspettava il carattere ';'. Le
due linee successive indicano precisamente dove, all’interno della linea incriminata, è
stato individuato l’errore: prima viene riportata integralmente la linea Hello.java:5,
e poi l’unico carattere della linea successiva, il caret ^, si trova esattamente sotto la
posizione dove dovrebbe trovarsi il ; . Naturalmente per correggere l’errore basta
mettere un ; dove richiesto.
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
2.5 Cosa può andare male?
27
Errori di battitura in parole chiave o identificatori. Supponiamo di aver scritto Public invece di public in linea 7, nell’intestazione della classe. L’output del
compilatore sarà
Hello . java :7: error : class , interface , or enum expected
Public class Hello {
^
1 error
confermando il fatto che Java è case sensitive: nella posizione indicata è illegale
la presenza del token Public, che viene interpretato come un identificatore, mentre
sarebbe stata legittima un’occorrenza dalla parola chiave public.
Un altro errore sintattico molto frequente consiste nello scrivere in modo errato
un identificatore, che può essere il nome di una classe, di una variabile, di un metodo,
eccetera. In questo caso, il compilatore non trova una definizione associata all’identificatore. Per esempio, il seguente messaggio di errore viene ottenuto compilando il
programma Hello dopo aver cambiato readString() in redString():
Hello . java :11: error : cannot find symbol
persona = Input . redString ();
// legge
^
symbol :
method redString ()
location : class Input
1 error
Nella prima linea del messaggio di errore il compilatore segnala la linea di codice
rilevante e il tipo di errore: cannot find symbol, non trovo il simbolo. Nelle due
linee successive spiega qual è il simbolo che non trova, cioè il metodo redString(), e
dove dovrebbe trovarsi il metodo cercato, cioè nella classe jbook.util.Input. Infine
il consueto ^ indica la posizione dove è stato trovato il simbolo non riconosciuto.
Errori multipli. In base a quanto abbiamo visto nella Sezione 2.2, possiamo dividere concettualmente il lavoro del compilatore in tre fasi: l’analisi sintattica, l’analisi
di semantica statica e la generazione del bytecode.
In generale la compilazione non si ferma appena trova un errore, ma procede fino
alla fine della fase corrente cercando di individuare eventuali altri errori, e stampando
alla fine un messaggio per ognuno degli errori individuati. Ne segue che una fase di
compilazione non viene iniziata se vi sono degli errori nella fase precedente.
Vediamo un esempio. Supponiamo di aver modificato Hello.java in modo che le
linee 10 e 11 siano:
10
11
String persona
person = Input . readString ()
// legge
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
28
Scrivere ed eseguire un programma Java
Manca il separatore ; alla fine delle linee 10 e 11, due errori sintattici, e l’identificatore person in linea 11 non è dichiarato, perché è diverso dalla variabile persona
della linea 10, un errore di semantica statica. Compilando otteniamo:
Hello . java :10: error : '; ' expected
String persona
^
Hello . java :11: error : '; ' expected
person = Input . readString ()
2 errors
^
// legge
Quindi vengono riportati solo i due errori sintattici. Se li correggiamo entrambi e ricompiliamo, otteniamo come previsto la segnalazione che person non è
riconosciuto:
Hello . java :11: error : cannot find symbol
person = Input . readString ();
// legge
^
symbol :
variable person
location : class Hello
1 error
Nel caso il compilatore segnali più errori, una buona prassi consiste nel cercare di
individuare e correggere gli errori nell’ordine in cui vengono segnalati, ricompilando
frequentemente il programma. Infatti alcuni errori segnalati potrebbero essere solo
una conseguenza di errori precedenti.
Purtroppo non sempre i messaggi di errore del compilatore risultano chiari e informativi come quelli visti sopra. Per esempio, se nel solito programma Hello dimentichiamo la parentesi graffa aperta alla fine dell’intestazione del metodo main in linea
8, l’output della compilazione segnalerà ben 10 errori tra cui i seguenti:
Hello . java :8: error : '; ' expected
public static void main ( String [] args )
^
Hello . java :11: error : < identifier > expected
persona = Input . readString ();
// legge
^
Hello . java :12: error : < identifier > expected
System . out . println ( " Ciao " + persona + '! ' ); // stampa
^
...
Hello . java :14: error : class , interface , or enum expected
}
^
10 errors
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
2.6 Esercizi
29
In questo caso solo il primo messaggio di errore è significativo, ma solo in parte. Il
programmatore dovrà riconoscere che alla fine della linea 8 manca in realtà un {, e
non un ; come indicato: correggendo e ricompilando si vedrà che anche gli altri errori
spariscono.
2.6
Esercizi
Esercizio 2.1 Dire quali delle seguenti affermazioni sono vere e quali false:
– javac è un compilatore di programmi
– javac è un interprete di programmi
– java è un IDE
– eclipse è un IDE
– javac è un editor di programmi
– java è un sistema operativo
– java è un interprete di programmi
– eclipse è un sistema operativo
Esercizio 2.2 Si scriva, compili ed esegua la classe Hello presentata nel testo, e se
ne verifichi il funzionamento come atteso.
Esercizio 2.3 Si modifichi la classe Hello presentata nel testo in modo che essa chieda all’utente, separatamente, nome e cognome, e saluti con un più formale
Buongiorno seguito da nome e cognome. Si compili ed esegua poi il programma
per verificarne il funzionamento.
Esercizio 2.4 Si identifichino nel seguente frammento di codice Java i vari token, distinguendoli in parole chiave, separatori, operatori, costanti letterali e
identificatori.
public static void uno () { if ( x == 1)
System . out . println ( " x vale 1 " );
else { x - -; flag = true ; } return ;}
Esercizio 2.5 Si riscriva il codice dell’esercizio 2.4 con la formattazione adeguata,
seguendo lo stile degli altri esempi presentati nel testo.
Esercizio 2.6 Si identifichino i vari token (distinti in parole chiave, separatori,
operatori, costanti letterali, identificatori) nel sorgente della classe Hello:
/*
*/
Un programma che chiede il tuo nome e ti saluta
import jbook . util . Input ;
public class Hello {
public static void main ( String [] args ){
System . out . print ( " Come ti chiami ? " ); // stampa
(C) 2011 Apogeo
i
i
i
i
i
i
i
i
30
Scrivere ed eseguire un programma Java
}
}
String persona ;
persona = Input . readString ();
// legge
System . out . println ( " Ciao " + persona + '! ' ); // stampa
Esercizio 2.7 Per ciascuna delle seguenti sequenze di caratteri, si dica se si tratta di
un identificatore legale in Java oppure no, giustificando la risposta:
pippo
Pippo
PIPPO
pIppO
"pippo"
'pippo'
_pippo_
<pippo>
paolo3
p1n0
3ug3n10
B0
int
program
Else
null
Pino&Laura
Pino e Laura
Pino_e_Laura
Pino_<3_Laura
Carlo_Motta
CarloMotta
Carlo.Motta
Carlo-Motta
$HOME
$_$_$_$
dubbio...atroce
pagina_____6
Esercizio 2.8* 4 Si identifichino, nel codice seguente della classe Errori.java, tutti gli errori di sintassi. Si provi dapprima a farlo su carta, senza l’aiuto del
compilatore, e si verifichi poi il risultato provando a compilare il programma.
import java . util . Input :
*/ un programma che contiene
molti errori /*
public class Errori {
pubblic static void Main ( String {} args ) {
String a ;
a = Input , readString (); / legge
Sting b = Imput . readString ();
system . out . println ( a + b ) // stampa
return ;
}
Esercizio 2.9* L’ordine delle istruzioni è importante. Quali permutazioni delle linee
9-12 che compongono il programma Hello sarebbero accettate dal compilatore?
Quali avrebbero senso?
4 Gli esercizi marcati con un asterisco sono di maggiore difficoltà rispetto ad altri; è raccomandabile affrontarli
dopo aver acquisito sufficiente familiarità con l’oggetto del capitolo.
(C) 2011 Apogeo
i
i
i
i