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