Informatica Fondamenti della Programmazione in Java Leonardo Vanneschi 1 1. Fondamenti Il primo programma in Java Il programma che stampa a video una sequenza di caratteri (o stringa): class Esempio { public static void main (String[] args) { System.out.println("Hello World!"); } } Nota: per poter essere utilizzato, questo programma deve necessariamente essere contenuto in un file di testo di nome Esempio.java. Spiegazione dettagliata del programma Per i più "esperti": • • • • • • • Esempio è il nome della classe che contiene il metodo main; public: il metodo main può essere invocato anche esternamente alla classe che lo contiene; static: il metodo main si riferisce alla classe Esempio e non ad una particolare istanza di esse; in altri termini, esiste uno ed un solo metodo main, identico per ogni istanza della classe Esempio; void: il metodo main non restituisce alcun risultato (dunque ha il solo ruolo di compiere delle azioni); main è il nome del metodo principale di un programma Java: il primo ad essere eseguito. String[] args: l'argomento del metodo main è un vettore di stringhe di caratteri chiamato args; System.out.println è il comando predefinito di Java per stampare a video una sequenza di caratteri. Tale sequenza va passata come parametro a questo comando. Tutti questi concetti verranno chiariti durante il corso. Per i "principianti": • • • La prima riga significa che il programma che sto scrivendo si chiama Esempio. Affinché esso possa funzionare, esso deve essere contenuto in un file di testo con lo stesso nome e con l'estensione ".java" (quindi in questo caso Esempio.java). La seconda può essere interpretata come l' "inizio del programma": a partire dalla riga successiva a questa, ha inizio il codice che viene eseguito. In prima approssimazione, le prime due righe possono essere considerate come qualcosa che non deve essere compreso nei dettagli, ma che va scritto ogni volta in modo meccanico e sempre uguale, con la sola eccezione del fatto che il nome del programma (la parola che segue la parola class nella prima riga) può essere scelta di volta in volta e con il vincolo che tale nome deve essere identico al nome del file che contiene il codice. 2 • System.out.println è il comando predefinito di Java per stampare a video una sequenza di caratteri. La sequenza di caratteri da stampare deve essere scritta dopo il comando, tra parentesi e tra virgolette. Tutti questi concetti verranno chiariti durante il corso. Compilazione ed esecuzione Per poter eseguire un programma Java, occorre prima compilarlo. Una volta che si è scritto il programma, infatti, esso è in una forma comprensibile da parte di un essere umano, ma non da parte di un calcolatore (si dice che è scritto in un linguaggio di programmazione di alto livello). Viceversa, un calcolatore è una macchina in grado di eseguire programmi scritti in un linguaggio macchina, o, nel gergo di Java, in byte-code (un programma scritto in linguaggio macchina può approssimativamente essere immaginato come una sequenza di bit, ovvero di caratteri 0 e 1). Quindi, il programma scritto da un essere umano, per poter essere eseguito deve essere tradotto in linguaggio macchina. Questo è il compito principale del compilatore. Le tappe da compiere per poter eseguire il programma Esempio.java sono schematizzate qua sotto: Programma di alto livello (file Esempio.java) compilatore Programma in byte-code (file Esempio.class) esecuzione Comando per compilare: Comando per eseguire: javac Esempio.java java Esempio e possono essere riassunte dai seguenti punti: • Compilare il programma con il comando javac Esempio.java (questo comando, se viene eseguito senza errori, fa si che venga creato un nuovo file chiamato Esempio.class che è la traduzione in byte-code del file Esempio.java). • Eseguire il programma con il comando java Esempio L'effetto sarà la visualizzazione a video della sequenza di caratteri Hello World! 3 Note: • i comandi javac e java (detti anche compilatore e interprete di Java) sono comandi che devono essere eseguiti dal Sistema Operativo presente sul vostro calcolatore (probabilmente Linux o Windows nel vostro caso). • Il meccanismo più elementare per "interagire" con un Sistema Operativo (e quello che verrà utilizzato in questo corso) è tramite un terminale. Quindi, ad esempio per eseguire il comando javac Esempio.java, occorre aprire un terminale con l'apposito meccanismo previsto dal vostro sistema operativo (normalmente lo si può fare tramite un apposito menu), posizionarsi nella directory in cui è contenuto il file Esempio.java, scrivere con la tastiera javac Esempio.java e premere il tasto return. • Per poter essere eseguiti, i comandi javac e java devono essere stati precedentemente installati. Essi possono essere scaricati ed installati gratuitamente dal sito http://java.sun.com/ seguendo il link downloads e scegliendo il Sistema Operativo utilizzato. Oltre a quello di tradurre programmi ad alto livello in programmi in linguaggio macchina, il compilatore ha anche il compito di segnalare gli errori che sono stati commessi al momento della scrittura del programma. Si noti che i soli errori che possono essere segnalati da un compilatore sono errori di sintassi. Esempi di errori di sintassi: • • • • Scrivere Sytsem.otu.priltn invece di System.out.println. Dimenticare il simbolo ";" alla fine di un comando. Dimenticarsi di chiudere una parentesi che era stata precedentemente aperta. ... (i possibili errori di sintassi sono moltissimi, ve ne accorgerete presto in laboratorio, purtroppo... E' importante non scoraggiarsi se all'inizio commettete molti di questi errori: è del tutto normale! Vedrete che con l'esperienza le cose andranno meglio). Esempi di errori di semantica: • Il programma che volevo scrivere non doveva stampare a video la stringa Hello World!, ma doveva fare la somma di due numeri interi. Chiaramente il compilatore non è in grado di segnalare errori di semantica. Questi ultimi sono di sola competenza del programmatore, che ha tutta la responsabilità di far si che il programma si comporti come desiderato. Quando il compilatore trova un errore si sintassi all'interno del programma, si interrompe e segnala sul terminale il tipo di errore e la riga di codice in cui l'errore è contenuto. In questo modo (con un po' di esperienza!) dovrebbe risultare facile da parte del programmatore identificare l'errore, correggerlo e riprovare a compilare. Quando il programma non contiene errori di sintassi, il compilatore genera un nuovo file (nella stessa directory in cui vi trovate) chiamato Esempio.class, che è la traduzione in byte-code del file Esempio.java. Se si cerca di aprire il file Esempio.class con un editore di testo, si vedrà che questo file risulta del tutto illeggibile da parte di un essere umano. Questo codice non deve essere ne' letto, ne' capito, ma deve essere eseguito, tramite il comando java Esempio. 4 2. Variabili, Tipi di Dato, Espressioni Il Concetto di Variabile Per introdurre il concetto di variabile (uno dei concetti fondamentali della programmazione), si consideri un programma un po' più complesso di quello visto fin qui: class Esempio { public static void main (String[] args) { System.out.println("2"); System.out.println("2"); System.out.println("2"); } } E' chiaro che questo programma stampa a video tre volte la stringa 2. Supponiamo adesso che dopo aver scritto questo programma, ci si renda conto che la stringa che doveva essere stampata non era 2, bensì (ad esempio) 5. Per correggere il programma da questo errore di semantica, occorrerà correggere tre punti del programma. In questo caso il lavoro non sarebbe granché oneroso, ma è facile immaginare che, se la stringa dovesse essere stampata mille volte, risulterebbe assai fastidioso dover effettuare mille correzioni dello stesso errore! Questo problema (ovvero quello di scrivere un programma che deve poi essere modificato in un secondo tempo per qualche motivo) è molto frequente. Uno dei metodi per affrontare in modo più efficace questo problema è quello di utilizzare il concetto di variabile. In prima approssimazione, ed in modo del tutto intuitivo e informale, una variabile può essere definita come una scatola (o, un po' più correttamente, come una parte della memoria del calcolatore) che contiene dei valori. Una variabile è identificata da: • • • un nome; un tipo di dato; un valore. Per esempio, una possibile definizione di variabile in Java può essere: int x = 5; In questo modo, abbiamo definito una variabile tale che: • • • il suo nome è x; il suo tipo è int, ovvero questa variabile è un numero intero; il suo valore è 5. Il tipo di dato di una variabile è legato alla sua taglia, ovvero allo spazio di memoria che essa occupa. Dopo che la riga di codice int x = 5; è stata eseguita, infatti, viene riservato uno 5 spazio di memoria per la variabile x e la taglia di questo spazio di memoria è esattamente la dimensione sufficiente per contenere un numero intero (32 bit in Java). Successivamente, all'interno di questa regione di memoria viene registrato il valore 5. Altro esempio: char c = 'a'; • • • il suo nome è c; il suo tipo è char, che significa che si tratta di un carattere alfanumerico; il suo valore è il carattere a. In questo caso, al momento dell'esecuzione della riga di codice char c = 'a'; viene riservato, all'interno della memoria del calcolatore, uno spazio per la variabile c della taglia necessaria per contenere un qualsiasi carattere alfanumerico (16 bit in Java) e successivamente viene registrato il valore a all'interno di questa regione. Si noti che la rappresentazione binaria del carattere a è uguale a quella di un numero intero di tipo short, quindi occorre utilizzare i simboli ' e ' per distinguere questi due casi. Vediamo adesso come può essere riscritto il programma precedente utilizzando una variabile: class Esempio { public static void main (String[] args) { int n = 2; System.out.println(n); System.out.println(n); System.out.println(n); } } Note: • E' chiaro che stavolta se occorre cambiare il valore che deve essere stampato a video, il cambiamento può essere fatto una sola volta, in un sol punto del programma. • E' importante notare la differenza tra l'uso del comando System.out.println con e senza le virgolette: un comando come System.out.println("n"); stampa a video la lettera n; viceversa, un comando come System.out.println(n); implica la ricerca all'interno della memoria di una variabile di nome n e la stampa a video del suo valore. Adesso che abbiamo (parzialmente) capito l'utilità del concetto di variabile, possiamo dedicarci con un livello maggiore di dettaglio alle tre caratteristiche tipiche delle variabili: i tipi di dato, i nomi (detti anche identificatori) e i valori. 6 Tipi di Dato In Java esistono due categorie di tipi di dato: • • Tipi di dato primitivi Tipi di dato complessi (o classi predefinite) Inoltre, in Java il programmatore ha la possibilità di definire dei nuovi tipi di dato. I tipi di dato primitivi di Java sono: • • • • • • • short int long float double char boolean numeri interi rappresentabili con 16 bit numeri interi rappresentabili con 32 bit numeri interi rappresentabili con 64 bit numeri reali rappresentabili con 32 bit numeri reali rappresentabili con 64 bit caratteri alfanumerici (rappresentabili con 16 bit) i valori di verità. I possibili valori sono true (vero) e false (falso) e quindi, dato che i possibili valori sono 2, sono rappresentabili con 1 bit. I tipi di dato short, int, long, float e double sono detti tipi numerici. Esistono (pochi) altri tipi di dato semplici in Java (come ad esempio il tipo byte), che vengono utilizzati di rado e non sono di interesse in questo corso. Tra i tipi di dato complessi (si noti che, come risulterà più chiaro alla fine del corso, il termine classi predefinite è più corretto ed appropriato, ma in questa fase si è preferito utilizzare il termine tipi di dato complessi perché più intuitivo) in Java sono moltissimi. Fin qui ne abbiamo visto uno soltanto: • String sequenze di caratteri alfanumerici Risulterà chiaro durante il corso il motivo per cui i tipi di dato complessi debbano essere distinti dai tipi di dato semplice. Inoltre, durante il corso si vedranno altri esempi di tipi di dato complessi di Java (anche se non ci avvicineremo neanche ad una analisi esaustiva di tutte le classi predefinite del linguaggio) e impareremo a definire nuovi tipi di dato. 7 Identificatori Un identificatore è una sequenza di caratteri: c0c1...cN-1 tale che: • c0 non è una cifra; • ∀i tale che 0 ≤ i ≤ N-1: ci non è un simbolo di operazione aritmetica (+,-,*,/,...) o logica (&, |, !) e non è uguale a uno spazio vuoto (carattere di spaziatura). Esempi: • • • • • • • • somma è un identificatore corretto; 3aPagina NON è un identificatore corretto perché comincia con una cifra; due+tre NON è un identificatore corretto perché contiene un simbolo di operazione aritmetica; il-mio-nome NON è un identificatore corretto perché contiene simboli di operazioni aritmetiche; il_mio_nome è un identificatore corretto (si noti la differenza col caso precedente); ilMioNome è un identificatore corretto; il mio nome NON è un identificatore corretto perché contiene degli spazi bianchi. x2 è un identificatore corretto (contiene o più cifre, ma nessuna di esse appare come primo carattere). 8 Valori delle variabili I valori costanti delle variabili e la loro rappresentazione dipendono fortemente dal tipo di dato. In particolare: • I valori di tipo short, int e long si rappresentano scrivendo semplicemente dei numeri senza la virgola. Chiaramente il tipo long contiene un insieme di possibili valori più grande rispetto al tipo int e quest'ultimo contiene un insieme di possibili valori più grande rispetto al tipo short, dato che i valori di tipo long sono rappresentati con 64 bit, quelli di tipo int con 32 bit e quelli di tipo short con 16 bit. • I valori di tipo float e double si rappresentano come numeri con la virgola (la notazione è americana, ovvero viene utilizzato il punto per separare le cifre intere da quelle decimali). Ancora una volta, il tipo double contiene un insieme di possibili valori più grande rispetto al tipo float, perché i suoi valori si rappresentano utilizzando un numero superiore di bit. • I valori di tipo char si rappresentano scrivendo un carattere alfanumerico tra apice singolo. • I valori di tipo boolean si rappresentano scrivendo le parole true e false senza alcuna virgoletta o apice (attenzione: la differenza tra maiuscole e minuscole in Java è sempre importante; True non è un valore booleano, true si). • I valori di tipo String si rappresentano scrivendo sequenze di caratteri tra virgolette (o apici doppi). Esempi di definizioni di variabili int x = 5; short pippo = 5; long y = 5; float numero = 5.3; double ciao = 5.3; char ilMioCarattere = 'w'; boolean b = true; String s = "Ciao a tutti, come va questa lezione di informatica?"; Oltre ai valori costanti, per ogni tipo di dato si possono avere valori dati dal risultato di una espressione più complessa, come risulterà chiaro dopo la lettura del prossimo paragrafo. Oltre a definire le variabili come negli esempi qui sopra, si può anche: • Definire una variabile senza assegnarle (immediatamente) un valore. Ad esempio, una definizione come: int n; 9 definisce una variable intera senza alcun valore (si può immaginare che le celle di memoria riservate per questa variabile vengano lasciate vuote). In questo caso si dice che la variabile è stata dichiarata, ma non inizializzata. In alcuni casi, è utile dichiarare una variabile senza assegnarle immediatamente un valore, ma è bene tenere presente che il linguaggio Java non consente (per fortuna!) l'utilizzo di una variabile se prima non le è stato assegnato un valore. Inoltre il linguaggio Java vieta l'uso di una variabile se prima non è stata inizializzata. Morale: non usate mai variabili senza dichiararle e inizializzarle (l'inizializzazione può essere fatta al momento della dichiarazione o successivamente, ma sempre prima del loro primo utilizzo). In seguito vedremo come sia possibile assegnare un valore a una variabile che è stata definita precedentemente, oppure cambiare il valore di una variabile, tramite il cosiddetto comando di assegnamento. • Definire più variabili "in un colpo solo". Una definizione come: int x, y, z; è assolutamente equivalente alla sequenza di definizioni: int x; int y; int z; 10 Espressioni Le principali espressioni in Java sono le espressioni aritmetiche, le espressioni relazionali, le espressioni logiche e le espressioni su stringhe. Esse vengono trattate separatamente qua sotto. Espressioni aritmetiche La definizione di queste espressioni è data dai seguenti punti: • Un numero è una espressione aritmetica. • Se A e B sono espressioni aritmetiche, allora: - A+B (addizione) - A-B (sottrazione) - A/B (divisione) - A*B (moltiplicazione) - A%B (modulo o resto della divisione intera) sono espressioni aritmetiche. • Se A è un'espressione aritmetica, allora anche (A) è un'espressione aritmetica. Esempio di espressione aritmetica: (5+7)*3+2 Precedenza degli operatori aritmetici: gli operatori * e / hanno la precedenza rispetto a + e -. Questo significa che * e / vengono eseguiti prima di + e - indipendentemente dalla posizione in cui si trovano nell'espressione. Esempio: 5 + 7 * 2 restituisce come risultato 19 in altri termini, prima viene calcolato 7*2 e poi il risultato viene sommato a 5. Per cambiare la precedenza degli operatori si possono usare le parentesi. Esempio: ( 5 + 7 ) * 2 restituisce come risultato 24 in altri termini, prima viene calcolato 5+7 e poi il risultato viene moltiplicato per 2. 11 Espressioni relazionali La definizione di queste espressioni è data dai seguenti punti: • Se A e B sono espressioni aritmetiche, allora: - A == B (uguale) -A > B (maggiore) -A < B (minore) - A >= B (maggiore o uguale) - A <= B (minore o uguale) - A != B (diverso) sono espressioni relazionali. • Se A è un'espressione relazionale, allora anche (A) è un'espressione relazionale. Si noti il fatto che le espressioni relazionali restituiscono sempre come risultato un valore di tipo boolean. Esempi di espressioni relazionali: • 5 == 8 • • • • • 5 == 5 3+4 > 5 >= 5 5 >= 8 5 >= 3 2+1 il risultato è false (o "è falsa" o "vale false") perché 5 non è uguale a 8 vale true vale true perché 7 è maggiore di 3 vale true vale false vale true Espressioni Logiche La definizione di queste espressioni è data dai seguenti punti: • true e false sono espressioni logiche. • Se A è un'espressione relazionale, allora A è anche un'espressione logica. • Se A e B sono espressioni logiche, allora: - !A (negazione logica) - A & B (and logico) - A | B (or logico) - A && B (and logico ottimizzato) - A || B (or logico ottimizzato) sono espressioni logiche. • Se A è un'espressione logica, allora anche (A) è un'espressione logica. 12 Il valore restituito come risultato da un'espressione logica è un valore di tipo boolean. Questo valore può essere sempre calcolato in funzione dei valori di A e B grazie all'uso delle cosiddette tabelle di verità. Le tabelle di verità della negazione logica, dell'and logico e dell'or logico sono riportate brevemente qua sotto: A true false !A false true A true true false false B true false true false A & B true false false false A true true false false B true false true false A | B true true true false 13 Esempi di espressioni logiche • • • (4 > 3) & (2 > 1) (7+1 > 2+2) | (8+1 > 100) true & (4 < 1) vale false vale true vale false Differenza tra gli operatori & ed &&: • A & B valuta le espressioni A e B e ne calcola l'and logico • A && B valuta l'espressione A; se il valore di A è uguale a false, allora l'espressione B non viene valutata ma viene restituito direttamente il valore false come risultato. In caso contrario, viene valutata l'espressione B e viene restituito l'and logico tra A e B. E' chiaro che il risultato restituito da A && B è sempre uguale a quello restituito da A & B, ma valutare A && B è meno costoso che valutare A & B. Nel caso in cui l'espressione B sia molto complessa, questa differenza di efficienza può essere significativa! Morale: se non si hanno validi motivi per valutare anche l'espressione a destra dell'operatore, usare sempre && quando si vuole eseguire un and logico. Esempio: (4 < 3) && ((5 <= 9) | (7%6 == 1) & ...) L'espressione (4 < 3) è falsa, quindi tutto il resto dell'espressione non è calcolata, ma viene restituito immediatamente il valore false. Differenza tra gli operatori | ed ||: • A | B valuta le espressioni A e B e ne calcola l'or logico • A || B valuta l'espressione A; se il valore di A è uguale a true, allora l'espressione B non viene valutata ma viene restituito direttamente il valore true come risultato. In caso contrario, viene valutata l'espressione B e viene restituito l'or logico tra A e B. Ancora una volta, è chiaro che il risultato restituito da A || B è sempre uguale a quello restituito da A | B, ma valutare A || B è meno costoso che valutare A | B. Morale: se non si hanno validi motivi per valutare anche l'espressione a destra dell'operatore, usare sempre || quando si vuole eseguire un or logico. 14 Espressioni su stringhe Le espressioni che possono essere formate un Java utilizzando le stringhe sono molte e non è scopo di questo corso una analisi esaustiva di ognuna di esse. Ci limiteremo a trattare le espressioni formate da concatenazione di stringhe: se A e B sono due stringhe, allora A+B è una stringa formata dalla concatenazione delle stringhe A e B. La concatenazione tra due stringhe A e B è semplicemente una sequenza formata da tutti i caratteri che compongono A seguiti da tutti i caratteri che compongono B (senza alcun carattere di separazione). Esempio: "buon" + "Giorno" "buonGiorno". è una espressione che restituisce come risultato la stringa Nota: se uno solo degli operandi dell'operatore + è una stringa, allora l'altro operando viene trasformato in una stringa e successivamente viene eseguita l'operazione di concatenazione. Ad esempio: • String s = "ciao" + 4; alla fine di questo comando, il valore della stringa s è "ciao4". 15 Definizione di variabile Adesso siamo pronti a definire in modo più preciso la sintassi della definizione di una variabile all'interno di un programma Java: [<modificatore>] <tipo T> <identificatore> ⎡= <espressione di tipo T>⎤; Si osservi che con questa notazione per definire la sintassi, tutto ciò che è racchiuso tra i simboli ⎡ e ⎤ è inteso come "opzionale" (ovvero può essere o non essere presente nel codice). Inoltre, si noti che: • Un modificatore è una tra le parole chiave di Java public, private, static, ecc. ... I modificatori verranno introdotti nella parte finale del corso e per adesso non verranno utilizzati. • L'espressione che compare alla sinistra del simbolo di uguaglianza deve essere dello stesso tipo rispetto a quello dichiarato a sinistra del nome della variabile. • Il simbolo ; è obbligatorio alla fine di una dichiarazione di variabile, come alla fine di ogni comando Java (separatore di comandi). Esempi di definizioni di variabili corrette: • • • boolean variabileLogica = (5>4) & (3<10) & true; int x = 5+9*8+2; double pippo = 10/3; Esempi di definizioni errate: • • boolean ciao = 10*4; double x = (5 == 3); 16 Esempio di programma Java che utilizza variabili ed espressioni: class Esercizio { public static void main (String[] args) { int x = 5; int y = 3; int somma; somma = x + y; System.out.println("La somma è: " + somma); } } Si cerchi di capire nei minimi dettagli il funzionamento di questo programma. Nota: Come già detto, l'operatore + in Java ha un significato diverso a seconda del tipo di dato dei suoi operandi: • • se entrambi gli operandi sono di tipo numerico, + è l'operazione aritmetica di somma. se almeno uno dei due operandi è una stringa, + è l'operatore di concatenazione tra stringhe. Quindi: • • • • 5+4 è una espressione di tipo intero il cui risultato è 9 "5"+4 è una espressione di tipo stringa il cui risultato è "54" 4+"5" è una espressione di tipo stringa il cui risultato è "54" "4"+"5" è una espressione di tipo stringa il cui risultato è "54" Gli ultimi tre risultati sono valori di tipo String e non hanno nulla a che vedere con il numero 54! Cercare di capire bene questi concetti prima di proseguire con la lettura. 17 3. Interazione con l'utente Un programma Java può interagire con il mondo esterno tramite la classe SavitchIn. Ad esempio, tramite questa classe è possibile fare in modo che sia l'utilizzatore del programma (e non il programmatore!) a settare il valore di alcune variabili. Ad esempio, si consideri la seguente definizione di variabile: int n = SavitchIn.readLineInt(); al momento dell'esecuzione di questa linea di codice, l'interprete di Java di arresta ed aspetta che l'utilizzatore scriva un numero intero e prema il tasto return. Una volta che l'utilizzatore preme il tasto return, tutto ciò che è stato scritto precedentemente è interpretato come il valore della variabile n e l'esecuzione del programma continua (si noti che se l'utilizzatore scrive un valore di un tipo diverso dal tipo int, l'esecuzione del programma si blocca e viene segnalato un errore). Esempio Il seguente programma esegue la somma di due numeri digitati dall'utilizzatore: class Esercizio { public static void main (String [] args) { int x; int y; System.out.println("Scrivi il primo numero: "); x = SavitchIn.readLineInt(); System.out.println("Scrivi il secondo numero: "); y = SavitchIn.readLineInt(); int somma = x + y; System.out.println("Somma dei numeri che hai scritto = " + somma); } } Compilare ed eseguire questo programma per capirne il comportamento. Cercare di capire questo programma nei minimi dettagli prima di proseguire. Si noti che è impossibile scrivere un programma che esegue la somma di due numeri qualsiasi senza far uso delle variabili! La classe SavitchIn non è sempre installata su tutti i PC. In tal caso deve essere installata manualmente. La si può scaricare gratuitamente, ad esempio, dal sito: http://aleph0.clarku.edu/~djoyce/cs101/labs/SavitchIn.java Altri metodi della classe SavitchIn: • • • double x = SavitchIn.readLineDouble(); char c = SavitchIn.readLineChar(); String s = SavitchIn.readLineWord(); 18 4. Istruzione di Assegnamento E' l'istruzione che permette di modificare il valore di una variabile (o di assegnarle un valore per la prima volta, se esso non è già stato assegnato al momento della sua dichiarazione). Sintassi: <identificatore> = <espressione>; dove <espressione> deve restituire un valore dello stesso tipo di quello utilizzato al momento della dichiarazione di <identificatore> (altrimenti il compilatore di Java segnala un errore e si interrompe). Semantica: • • Si valuta l'espressione ottenendo, se la valutazione termina, un valore v. Il valore v viene scritto nella regione di memoria che è stata associata alla variabile al momento della sua dichiarazione. Un caso interessante: in Java (come nella maggior parte dei linguaggi di programmazione) è permesso l'utilizzo di una istruzione di assegnamento come la seguente: x = espressione(x) ; dove espressione(x) è una espressione che fa uso di x. Ad esempio, si può scrivere una istruzione come: x = x+1; Il significato è: • • Viene sommato 1 al vecchio valore di x. Il risultato viene scritto di nuovo nella regione di memoria associata a x. In generale: • • Viene valutata espressione(x) utilizzando il vecchio valore di x. Il risultato viene scritto di nuovo nella regione di memoria associata a x. Esempio class Esercizio { public static void main (String[] args) { int x; x=10; x=x+1; 19 x=x+2; x=x+10; } } Cercare di capire il funzionamento di questo programma. Qual'è il valore della variabile x alla fine dell'esecuzione? Nota: Il linguaggio Java consente anche la notazione: x++; che ha esattamente lo stesso significato di x=x+1; ma è (leggermente) più efficiente. Esempio: class Esercizio { public static void main (String[] args) { int x = 10; x++; x++; System.out.println(x); } } Che cosa stampa a video questo programma? Il linguaggio Java consente anche la notazione ++x; il risultato è sempre lo stesso, ma c'è un'importante differenza tra x++ e ++x. Per comprenderla, si considerino i seguenti esempi: • Il frammento di codice: int x = 2; System.out.println((x++) == 3); System.out.println(x); stampa a video: false 3 • Il frammento di codice: int x = 2; System.out.println((++x) == 3); System.out.println(x); stampa a video: 20 true 3 In altri termini, c'è una differenza tra x++ e ++x soltanto quando li si utilizza all'interno di una espressione relazionale: • • Nel caso di ++x prima si incrementa il valore di x e poi si valuta l'espressione relazionale. Nel caso di x++ prima si valuta l'espressione relazionale e poi si incrementa il valore di x. 21 Importanza dell'inizializzazione di una variabile Inizializzare una variabile significa assegnarle un valore per la prima volta da quando è stata dichiarata. Non si può utilizzare una variabile a destra del simbolo = in una istruzione di assegnamento se essa non è stata prima inizializzata (pena: errore del compilatore). Esempio: class Esercizio { public static void main (String[] args) { int x; int y; x = 5; int somma; somma = x+y; } } Il compilatore restituisce un errore quando si tenta di eseguire questo programma: y non è stata inizializzata (non ha un valore o, più correttamente, non ha un valore significativo). Importanza dei tipi di dato Non si può (quasi mai) assegnare ad una variabile di un tipo un valore di un altro tipo! (Pena: errore del compilatore). Esempio: class Esercizio { public static void main (String[] args) { String s = "ciao"; int x = 5; x = s; --------------------> Errore s = x; --------------------> Errore } } In questo caso, il programma restituisce un errore a tempo di compilazione perché i tipi int e String non sono compatibili. Una variabile di un tipo T1 può essere assegnata a un valore di tipo T2 se e solo se T1 e T2 sono tipi compatibili. Di seguito vengono trattate le regole per stabilire se due tipi di dato sono o meno compatibili. 22 Compatibilità dei Tipi di Dato Le regole per stabilire se due tipi di dato sono o meno compatibili (prendendo in considerazione solo i tipi di dato che sono stati considerati fin qui) sono le seguenti: • • • I tipi numerici sono compatibili con gli altri tipi (String, char, boolean). String, char e boolean non sono compatibili tra di loro (per ogni loro permutazione). I tipi numerici possono essere compatibili tra di loro, con le seguenti regole: Vedendo un tipo di dato come un insieme, esistono le seguenti relazioni tra i tipi numerici in Java: short ⊆ int ⊆ long ⊆ float ⊆ double Un tipo numerico T1 è compatibile con un altro tipo numerico T2 (in altri termini un valore di tipo T1 può essere assegnato a una variabile di tipo T2) se e solo se: T1 ⊆ T2 Esempio: double x; x = 1; Questo assegnamento è consentito perché int ⊆ double. Il valore double "corrispondente" al valore intero 1 è 1.0. Esempio: int x; x = 1.5; Questo assegnamento NON è consentito in quanto non è vero che il tipo double è contenuto in int. Il valore double 1.5 NON ha un corrispettivo nel tipo int. Errore a tempo di compilazione. Esempio: int x = 5; double y = 7.3; double somma = x+y; Questo assegnamento è consentito; il risultato è un double: il valore intero di x viene prima trasformato nel suo corrispettivo nel tipo double e poi i due double vengono sommati. 23 Esempio: int x = 5; double y = 7.3; int somma = x+y; Questo assegnamento NON è consentito: il risultato della somma tra x e y è un double e non si può assegnare un double a una variabile di tipo int. Qualche osservazione sull'operazione di divisione Si consideri il seguente frammento di codice: int x = 10; int y = 7; int d = x / y; il programma termina senza errori e il valore di d alla fine dell'esecuzione del programma è 1. Infatti, il risultato della divisione è (approssimativamente) uguale a 1.428; dato che questo valore deve essere assegnato a una variabile intera, esso viene prima arrotondato. In altri termini, quando entrambi gli operandi di una divisione sono numeri interi, la divisione viene interpretata come una divisione intera (ovvero come un operatore di divisione che arrotonda il risultato per difetto al più vicino numero intero). Si consideri adesso il seguente frammento di codice: int x = 10; int y = 7; double d = x / y; il programma termina senza errori e il valore di d alla fine dell'esecuzione del programma è 1.0. Infatti, dato che i due operandi della divisione sono entrambi interi, la divisione viene comunque interpretata come una divisione intera (ovvero con arrotondamento). Successivamente, dato che il risultato deve essere assegnato a una variabile di tipo double, il risultato intero viene trasformato nell'equivalente double. Per ottenere il risultato di una divisione senza arrotondamento, occorre necessariamente che almeno uno dei due operandi della divisione sia di tipo double o float. Si considerino, ad esempio, i seguenti tre frammenti di codice: double x = 10.0; int y = 7; double d = x/y; int x = 10; double y = 7.0; double d = x/y; double x = 10.0; double y = 7.0; double d = x/y; In tutti e tre questi casi, il valore di d alla fine dell'esecuzione del codice è uguale a 1.428... Attenzione: in tutti e tre questi casi la variabile d deve essere stata dichiarata di tipo double, altrimenti il compilatore restituisce un errore! 24 In altri termini: quando almeno uno degli operandi di una divisione è di tipo double (o float), la divisione viene interpretata come una divisione tra numeri reali (ovvero una divisione senza arrotondamento) ed il risultato di questa divisione è sempre un valore di tipo double. Esempio: class Esercizio { public static void main (String[] args) { int x = 10; int y = 7; System.out.println(x/y); System.out.println(x%y); } } Il programma stampa: 1 3 Esempio: class Esercizio { public static void main (String[] args) { short x = 33000; x++; x++; } } L'esecuzione di questo programma genera il seguente errore: "Possible loss of precision: found short, required int" Perché? Si noti che questo errore NON è un errore di sintassi (in altri termini, per scoprire questo errore occorre valutare il valore di x alla fine dell'esecuzione del codice), quindi il compilatore NON riesce a rilevare questo errore. L'errore viene dunque segnalato al momento dell'esecuzione (ovvero al momento in cui effettivamente viene valutato il valore di x). 25 5. Comandi di stampa Fin'ora abbiamo visto soltanto il metodo System.out.println. Questo metodo stampa a video il parametro che gli viene passato e va a capo. Esiste anche un metodo che stampa a video senza poi andare a capo: System.out.print. Quindi: System.out.println ("ciao"); System.out.println ("ciao"); Stampa: ciao ciao Mentre: System.out.print ("ciao"); System.out.print ("ciao"); Stampa: ciaociao Inoltre, è bene ripetere (e ricordarsi!) che le chiamate: System.out.println("ciao"); e System.out.print("ciao"); stampano a video la sequenza di caratteri ciao, mentre le chiamate: System.out.println(ciao); e System.out.print(ciao); (attenzione alle virgolette!) implicano una ricerca nella memoria del calcolatore di una variabile di nome ciao e la stampa a video del suo valore. 26 Il Tipo String Un valore di tipo String è una sequenza di caratteri contenuta tra i simboli " e ". Esempi: "ciao" ma anche: "Ciao a tutti" "3 è il numero perfetto" Accertarsi di aver capito la differenza tra un valore di tipo String ed un identificatore prima di proseguire con la lettura. Il linguaggio Java mette a disposizione alcuni metodi per manipolare le stringhe: • • • • La lunghezza di una stringa w (ovvero il numero dei caratteri che la compongono) può essere calcolata scrivendo: w.length() Il carattere i-esimo della stringa w può essere ottenuto scrivendo: w.charAt(i) Si può testare se due stringhe s e w sono uguali oppure no tramite l'invocazione del metodo: s.equals(w); il risultato è un booleano (true se s e w sono due stringhe identiche, false altrimenti). Nota: s.equals(w) e w.equals(s) restituiscono sempre lo stesso risultato. Si può concatenare il valore di due stringhe s e w (come abbiamo già visto) tramite la scrittura s+w. Importante: Si noti la differenza tra l'operatore per testare l'uguaglianza tra due stringhe (equals) e l'operatore per testare l'uguaglianza tra due numeri (==). Esistono molti altri metodi per manipolare le stringhe. Una documentazione completa e dettagliata si trova sul sito http://java.sun.com/j2se/1.5.0/docs/api dove si trova la documentazione di tutte le classi predefinite del linguaggio Java. Clickando su String nel menu sulla sinistra di questa pagina web, è possibile vedere la documentazione di tutti i metodi della classe String. Esempio: si consideri il seguente frammento di codice: String s; s = "ciao a tutti"; System.out.println(s.length()); viene stampato 12: lo spazio vuoto viene contato come un carattere dal metodo length. 27 Esempio: si consideri il seguente frammento di codice: String s; s = "ciao a tutti"; System.out.println(s.charAt(3)); viene stampato il carattere o e non il carattere a, come ci si sarebbe potuti aspettare: nel linguaggio Java si comincia a contare gli indici cominciando sempre da 0 (e non da 1) !! Esempio: Il frammento di codice: String s; s = "ciao a tutti"; String secondaStringa = "arrivederci"; System.out.println(s.equals(secondaStringa)); stampa false. Esempio: Il frammento di codice: String s; s = "ciao a tutti"; String secondaStringa = "arrivederci"; System.out.println(s+secondaStringa); stampa ciao a tuttiarrivederci Esempio: Il frammento di codice: String s; s = "ciao a tutti"; System.out.println(s.charAt(12)); genera un errore: "String index out of range: 12". Il compilatore Java stampa anche la linea di codice nella quale si è verificato il problema (in questo caso, la linea in cui compare l'istruzione System.out.println(s.charAt(12));). 28 Esempio: Il frammento di codice: int n = 12; String s = "sono le ore " + n + " e tutto va bene"; System.out.println(s); stampa sono le 12 e tutto va bene Che cosa stamperebbe questo frammento di codice se al posto di System.out.println(s); fosse stato scritto System.out.println("s"); ? Accertarsi di aver compreso questi esempi prima di poseguire con la lettura. 29 Commenti All'interno di un codice Java è possibile inserire frasi che servono al programmatore per capire meglio il codice (ad esempio, spiegazioni in italiano del motivo per cui un certo programma è stato scritto in un certo modo, oppure una descrizione informale del suo comportamento) e che non vengono compilate ne' eseguite. Queste frasi si chiamano commenti. In Java ci sono due metodi per scrivere i commenti: • // commento • /* commento */ Nel primo caso, tutto ciò che segue il simbolo // fino alla fine della riga è considerato un commento (e quindi non viene compilato, ne' eseguito). Nel secondo caso, tutto ciò che è compreso tra i simboli /* e i simboli */ è considerato un commento (anche se è scritto su più righe). Esempio: class Esercizio { public static void main (String[] args) { int x = 10; int y = 7; double d = x/y; /* Attenzione: sia x che y sono variabili intere, quindi il risultato che viene memorizzato nella variabile d viene ottenuto tramite un arrotondamento!! */ System.out.println(d); stampato il valore di d // con questa istruzione ho } } 30 6. Istruzioni Una istruzione in Java può essere: • • • • • • • • un'istruzione di assegnamento (le abbiamo già studiate), un'istruzione condizionale if, un'istruzione consizionale switch (non verrà trattata in questo documento), un'istruzione iterativa while, un'istruzione iterativa do-while, un'istruzione iterativa for, la chiamata di un metodo, un blocco di istruzioni. Un blocco di istruzioni è una sequenza di istruzioni separate dal carattere ; e compreso tra parentesi graffe { e }. Ad esempio, siano A, B, C, D quattro istruzioni, { A; B; C; D; } è un blocco di istruzioni (nota: l'indentazione è fortemente consigliata ma non obbligatoria). L'effetto di eseguire un blocco di istruzioni è quello di eseguire la prima (A nell'esempio), se essa termina eseguire la seconda (B nell'esempio), ..., e così via fino all'ultima. Nel seguito, vengono trattati gli altri tipi di istruzione possibile, cominciando con le istruzioni condizionali. 31 7. Istruzione Condizionale if Istruzione Condizionale if "a due vie" Sintassi: if (<espressione booleana>) <istruzione1> else <istruzione2> Semantica: • Si valuta l'espressione booleana. • - Se il valore dell'espressione booleana è true, si esegue <istruzione1> - Se il valore dell'espressione booleana è false, si esegue <istruzione2>. • Continua l'esecuzione del programma con la prossima istruzione. Esempio: Il seguente frammento di codice: if ( 5 < (4+8) / 2 ) System.out.println("uno"); else System.out.println("due"); stampa uno. 32 Istruzione Condizionale if "a una via" Sintassi: if ( <espressione booleana>) <istruzione> Semantica: • Valuta l'espressione booleana. • - Se il valore dell'espressione booleana è true, esegui <istruzione> e poi l'esecuzione del programma continua con la prossima istruzione. - Se il valore dell'espressione booleana è false, continua l'esecuzione del programma con la prossima istruzione. Esempio: Il seguente frammento di codice: if ( 2 == 4%3 ) System.out.println ("uno"); System.out.println("due"); stampa due. Si noti il modo in cui il programma è stato indentato. Informalmente potremmo dire che questa indentazione consente di distinguere l'istruzione che "è dentro" l'if (<istruzione> nella sintassi qua sopra), da quella che "ne è fuori" (la "prossima istruzione" secondo la terminolgia usata nella descrizione semantica qua sopra). Questa indentazione aiuta molto la leggibilità del codice. Non è obbligatoria, ma è fortemente consigliata. 33 Istruzioni condizionali "in cascata" (o "innestate") Può accadere che le istruzioni condizionali contenute in una istruzione condizionale siano a loro volta istruzioni condizionali. Esempio: Il frammento di codice: if (5 == (2+8)/2) if (3 < 7%2) System.out.println("uno"); else System.out.println("due"); else System.out.println("tre"); stampa due. Ancora una volta, si noti che l'indentazione aiuta molto la leggibilità del codice. Non è obbligatoria, ma è fortemente consigliata. Ambiguità Si consideri il seguente frammento di codice: if (5 == (2+8)/2) if (3 < 7%2) System.out.println("uno"); else System.out.println("due"); a quale if si riferisce la clausola else? In Java una clausola else si riferisce sempre all'if senza clausola else più vicino. Quindi nel frammento di codice precedente, l'else si riferisce al secondo if e quindi questo frammento di codice stampa due. 34 Istruzione, blocco di istruzioni e istruzione condizionale Come abbiamo già visto quando abbiamo definito il concetto di istruzione, una sequenza di istruzioni come la seguente: istruzione1; istruzione2; ... istruzioneN; non è un'istruzione! Come tale, non può stare in una clausola if !! Per renderla un'istruzione, occorre trasformarla in un blocco di istruzioni, usando le parentesi graffe: { istruzione1; istruzione2; ... istruzioneN; } questa è un'istruzione e quindi può stare in una clausola if. Esempio: Il seguente frammento di codice: if ( 5 == (3+8)/2 ) System.out.println("buon"); System.out.println("giorno"); else System.out.println("buona"); System.out.println("sera"); genera un errore a tempo di esecuzione: "else without if". Invece il seguente frammento di codice: if ( 5 == (3+8)/2 ) { System.out.println("buon"); System.out.println("giorno"); } else { System.out.println("buona"); System.out.println("sera"); } non genera alcun errore e stampa buon giorno Attenzione al seguente esempio! 35 Anche il seguente frammento di codice if ( 5 == (3+8)/2 ) { System.out.println("buon"); System.out.println("giorno"); } else System.out.println("buona"); System.out.println("sera"); termina senza alcun errore. Il suo effetto è quello di stampare a video: buon giorno sera In altri termini, l'istruzione System.out.println("buona"); viene interpretata come l'unica istruzione appartenente alla clausola else e l'istruzione System.out.println("sera"); viene interpretata come la prima istruzione che segue l'istruzione condizionale if "a due vie". Si noti che l'indentazione è solo un aiuto per la leggibilità e non ha niente a che fare ne' con il significato ne' con il comportamento di un programma! In questo caso, anche se l'istruzione System.out.println("sera"); è indentata allo stesso livello di System.out.println("buona"); la seconda fa parte della clausola else, mentre la prima no. Sono le parentesi graffe e non l'indentazione a decidere quali istruzioni fanno parte di una clausola if o di una clausola else. Suggerimento pratico: quando si scrive una istruzione condizionale if, usare le parentesi graffe in ogni caso in cui una clausola if o una clausola else debbano contenere più di una istruzione elementare. Evitare di utilizzare le parentesi graffe solo quando si è sicuri che una clausola if o una clausola else debbano contenere una e una sola istruzioni elementari. In alternativa: usare sempre le parentesi graffe! In questo modo non si sbaglia mai! Se si decide per questa ultima via, la sintassi dell'if "a due vie" può essere considerata come segue: if (<espressione booleana>) { <istruzione1> } else { <istruzione2> } e quella dell'if "a una via" come segue: if (<espressione booleana>) { <istruzione1> } 36 Esempio (il massimo di due numeri): Il seguente programma Java calcola e stampa a video il massimo tra due numeri digitati dall'utente. class Massimo { public static void main (String[] args) { int a, b, max; System.out.println("Scrivi il primo numero intero: "); a = SavitchIn.readLineInt(); System.out.println("Scrivi il secondo numero intero: "); b = SavitchIn.readLineInt(); if (a > b) { max = a; } else { max = b; } System.out.println("Massimo tra i due numeri che hai scritto = " + max); } } Questo programma contiene al suo interno molti dei concetti che sono stati considerati fin qui. Una sua comprensione approfondita è assolutamente necessaria prima di proseguire con la lettura. Una volta compreso nei minimi dettagli il programma precedente, si cerchi di scrivere il programma che calcola il massimo tra tre numeri digitati dall'utente. 37 8. Istruzione iterativa while Sintassi: while (<espressione booleana>) <istruzione> Semantica: 1. Valuta l'espressione booleana. 2. a. Se l'espressione booleana vale true, esegui <istruzione> e torna al passo 1. b. Se l'espressione booleana vale false, il programma prosegue con l'esecuzione della prossima istruzione. Esempio: Il seguente frammento di codice: int x = 0; while (x < 3) { System.out.println(x); x = x+1; } stampa 0 1 2 38 9. Istruzione iterativa do-while Sintassi: do <istruzione> while (<espressione booleana>); Semantica: 1. Esegui l'istruzione che segue il do. 2. Valutare l'espressione booleana. a. Se l'espressione booleana vale true, torna al passo 1. b. Se l'espressione booleana vale false, il programma prosegue con l'esecuzione della prossima istruzione. La differenza con l'istruzione while è che nel caso del do-while, <istruzione> viene comunque eseguita almeno una volta. L'istruzione: while (<espressione booleana>) <istruzione> ha la stessa semantica di: if (<espressione booleana>) { do <istruzione> while (<espressione booleana>); } e l'istruzione: do <istruzione> while (<espressione booleana>); ha la stessa semantica di: <istruzione>; while (<espressione booleana>) <istruzione> 39 10. Istruzione iterativa for Sintassi: for ( <istruzione1> ; <istruzione3> <espressione booleana> ; <istruzione2> ) Semantica: 1. Esegui <istruzione1>. 2. Valuta <espressione booleana> a. Se <espressione booleana> vale true, esegui <istruzione3>, poi <istruzione2> (attenzione all'ordine: non è un errore di stampa!) e infine ritorna al passo 2. b. Se <espressione booleana> vale false, prosegui eseguendo la prossima istruzione nel programma. Si noti che <istruzione1> viene eseguita una e una sola volta prima di "entrare nel ciclo" (di solito la si usa per inizializzare una variabile, o contatore, che servirà per determinare la terminazione del ciclo), <espressione booleana> viene valutata ogni volta prima di una nuova iterazione (è la condizione di terminazione), mentre <istruzione2> viene eseguita ogni volta alla fine di ogni iterazione (di solito serve per modificare il contatore), ovvero dopo aver eseguito <istruzione3> (spesso detta anche corpo del ciclo). L'istruzione: for ( <istruzione1> ; <istruzione3> <espressione booleana> ; <istruzione2> ) ha la stessa semantica di: <istruzione1>; while (<espressione booleana>) { <istruzione3>; <istruzione2>; } (ancora una volta, si faccia attenzione all'ordine di queste ultime due istruzioni: non è un errore di stampa!). 40 Instruzioni, blocchi di istruzioni e istruzioni iterative Per decidere quali istruzioni sono comprese all'interno del corpo di una istruzione iterativa, valgono regole analoghe a quelle già viste per il comando condizionale: a meno che il corpo di un'istruzione iterativa non contenga una sola istruzione semplice, sono le parentesi graffe a decidere quali istruzioni stanno dentro al corpo di un comando iterativo e quali fuori. Esempio Supponiamo di voler stampare tutti i numeri interi da 0 a 9 e supponiamo di scrivere il programma come segue: for (int i = 0; i < 10; i++) System.out.print("Adesso stampo il numero: "); System.out.println(i); solo l'istruzione System.out.print("Adesso stampo il numero: "); fa parte del corpo del for, mentre l'istruzione System.out.println(i); è esterna al ciclo e quindi viene eseguita solo dopo la terminazione dell'esecuzione del ciclo. Quindi, il programma non si comporta come volevamo, ma piuttosto stampa a video la seguente sequenza di caratteri: Adesso stampo il numero: Adesso stampo il numero: Adesso stampo il numero: Adesso stampo il numero: Adesso stampo il numero: Adesso stampo il numero: Adesso stampo il numero: Adesso stampo il numero: Adesso stampo il numero: Adesso stampo il numero: 10 Se invece vogliamo stampare tutti i numeri tra 0 e 9 (in altri termini, se vogliamo che la stampa del numero corrente i sia interna al ciclo), dobbiamo usare le parentesi graffe: for (int i = 0; i < 10; i++) { System.out.print("Adesso stampo il numero: "); System.out.println(i); } Questo nuovo frammento di codice, infatti, stampa a video, come desideravamo: Adesso Adesso Adesso Adesso Adesso Adesso Adesso Adesso Adesso Adesso stampo stampo stampo stampo stampo stampo stampo stampo stampo stampo il il il il il il il il il il numero: numero: numero: numero: numero: numero: numero: numero: numero: numero: 0 1 2 3 4 5 6 7 8 9 Ancora una volta, si noti come l'indentazione del codice è assolutamente inutile nel decidere quale istruzione fa parte del ciclo e quale no. 41 Esempio (TIPICO ERRORE!) Si supponga di voler stampare a video la sequenza dei numeri interi da 0 a 9 (stavolta senza voler premettere la stampa di ogni numero con la sequenza di caratteri "Adesso stampo il numero: ") con un'istruzione iterativa while. In altri termini, vogliamo che il nostro programma stampi a video: 0 1 2 3 4 5 6 7 8 9 Supponiamo di scrivere il programma come segue: int i = 0; while (i < 10) System.out.print(i + " "); i++; anche in questo caso, l'istruzione i++; non fa parte del ciclo, dato che se non vengono usate le parentesi graffe, solo la prima istruzione dopo il while è considerata interna al ciclo. Di conseguenza, all'interno del ciclo il valore della variabile i non viene mai modificato. In altri termini, il valore di i rimane sempre uguale a zero e quindi il programma NON TERMINA MAI! L'effetto a video sarà la stampa della sequenza di caratteri: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0...... fino a che la memoria viva del calcolatore non verrà riempita ed il programma terminerà forzatamente. Affinché il programma faccia ciò che volevamo, occorre usare le parentesi graffe in modo che anche l'istruzione i++; faccia parte del ciclo e quindi venga eseguita ad ogni iterazione. Ecco una versione corretta del programma: int i = 0; while (i < 10) { System.out.print(i + " "); i++; } In conclusione, si tenga presente il fatto che un buon metodo per essere sicuri di non incappare in questo tipo di errore è quello di usare sempre le parentesi graffe per racchiudere quelle istruzioni che vogliamo facciano parte del corpo di un ciclo. In tal modo, ad esempio, la sintassi dell'istruzione while diventa: while (<espressione booleana>) { <istruzione>; } e quella del comando for: for ( <istruzione1> ; <espressione booleana> ; <istruzione3>; } <istruzione2> ) { 42 11. I vettori Un vettore è una sequenza di elementi dello stesso tipo. Ad esempio: 4 5 7 3 1 è un vettore di numeri interi. Definizione di un vettore. Sintassi: <tipo> [] <identificatore> = new <tipo> [<lunghezza>] dove il tipo utilizzato a sinistra del simbolo = deve essere obbligatoriamente lo stesso di quello usato a destra. Semantica: viene definita una variabile di nome <identificatore>. Il tipo di questa variabile è vettore di elementi di tipo <tipo>. La lunghezza di questo vettore è <lunghezza>. Al momento in cui questa definizione viene eseguita, viene riservata un'area per la variabile <identificatore> della dimensione sufficiente per contenere <lunghezza> elementi di tipo <tipo>. Esempio: int[] vec = new int[10]; crea un vettore di nome vec composto da 10 numeri interi. Nota: si può anche separare la parte a sinistra dell'uguale da quella a destra. Ad esempio, si può scrivere: int [] vec; vec = new int[10]; dopo la prima di queste istruzioni, una variabile di nome vec viene creata, ma non si conosce ancora la sua dimensione e quindi non viene ancora allocata la memoria. Dopo la seconda istruzione, viene allocata una quantità di memoria per la variabile vec sufficiente a contenere 10 numeri interi. Importante: tra la prima e la seconda di queste istruzioni, non si può utilizzare vec (pena: errore a tempo di compilazione). 43 Accesso agli elementi di un vettore Sia x un vettore definito come segue all'interno di un programma Java: int [] x = new int[5]; la variabile x è, dunque, un vettore di 5 numeri interi. Si può accedere a questi cinque elementi utilizzando le seguenti 5 variabili di tipo int: x[0], x[1], x[2], x[3], x[4] queste variabili vengono create automaticamente al momento della creazione del vettore x. Si noti come gli indici comicino anche in questo caso (come nel caso delle stringhe!) da zero e non da uno: il primo elemento di un vettore x di N elementi è SEMPRE x[0] e l'ultimo elemento è SEMPRE x[N-1]. Scrivere x[N], oppure anche x[K] per qualsiasi K > N comporta un errore a tempo di compilazione. L'indice per accedere ad un elemento di un vettore deve sempre essere un numero intero, ma può anche essere il risultato di un'espressione aritmetica. Quindi, in generale, la sintassi per accedere ad un elemento di un vettore è: <identificatore> [<espressione>] dove <identificatore> è il nome del vettore (lo stesso usato al momento della sua definizione) ed <espressione> è un'espressione aritmetica tale che: • • il suo risultato è un numero intero il suo risultato è compreso tra 0 e N-1 dove N è la lunghezza del vettore (specificata al momento della sua dichiarazione). Esempio: Il seguente frammento di codice int [] x = new int[5]; int i = 3; x[i+1] = 12; assegna all'ultima posizione del vettore x il valore 12. 44 Lunghezza di un vettore Una volta che un vettore x è stato definito, la sua lunghezza può essere ottenuta tramite: x.length Si faccia attenzione a non confondere il metodo length per calcolare la lunghezza di una stringa: s.length() e la variabile intera x.length che contiene la lunghezza del vettore x. La differenza risulterà chiara al momento in cui verranno studiati i metodi di Java (un po' più avanti in questo corso). Per il momento è sufficiente mandare a memoria il fatto che per calcolare la lunghezza di una stringa occorre utilizzare le parentesi (parentesi aperta e parentesi chiusa) dopo la parola length, mentre per calcolare la lunghezza di un vettore le parentesi non vanno usate. 45 Esempio Creare un vettore di interi vec ed assegnare ad ogni suo elemento il valore 5. class Esercizio { public static void main (String[] args) { int [] vec = new int [10]; int i = 0; while (i < 10) { vec[i] = 5; i++; } } } Si noti che avremmo potuto scrivere il while come segue: while (i < vec.length) { ... } ed il programma sarebbe stato assolutamente equivalente. 46 Inizializzazione di un array • Inizializzazione per enumerazione int [] x = {1, 2, 3, 4}; crea un array di lunghezza 4 con: x[0] x[1] x[2] x[3] = = = = 1 2 3 4 Tale dichiarazione è equivalente a: int [] x[0] = x[1] = x[2] = x[3] = • x = new int[4]; 1; 2; 3; 4; Inizializzazione con ciclo int [] y = new int[100]; for (int i = 0; i < y.length; y[i] = <espressione>; i++) 47 Il parametro del metodo main Siamo finalmente in grado di capire il significato del parametro (obbligatorio!) del metodo main: si tratta di un vettore di stringhe! Questo vettore contiene in sequenza tutti i parametri passati dall'utilizzatore al momento dell'esecuzione del programma. Supponiamo di scrivere un programma in Java all'interno di un file chiamato Esempio.java. Si può compilare ed eseguire il programma, ad esempio, in questo modo: javac Esempio.java java Esempio ciao 2 terzo Al momento dell'esecuzione del programma (per l'esattezza prima di cominciare l'esecuzione della prima istruzione del metodo main), il vettore args viene creato automaticamente ed esso contiene nella prima posizione (indice 0) la stringa "first", nella seconda posizione (indice 1) la stringa "2" (attenzione: anche se l'utente scrive dei numeri, questi sono comunque memorizzati in un vettore di stringhe e quindi sono stringhe!) e nella terza posizione (indice 2) la stringa "terzo". Esempio Si consideri il seguente programma Java: class Esempio public static void main (String[] args) { if (args.length != 3) { System.out.println("Errore: occorre inserire 3 argomenti!"); System.exit(-1); } System.out.println(args[0] + args[1] + args[2]); } } Supponiamo di compilare ed eseguire il programma nel seguente modo: javac Esempio.java java Esempio Il risultato sarà che il programma stampa a video: Errore: occorre inserire 3 argomenti! Supponiamo adesso di compilare ed eseguire il programma nel seguente modo: javac Esempio.java java Esempio 1 2 Il risultato sarà ancora una volta la stampa a video di: Errore: occorre inserire 3 argomenti! 48 Supponiamo infine di compilare ed eseguire il programma nel seguente modo: javac Esempio.java java Esempio first second 3 Il risultato sarà che il programma stampa a video: firstsecond3 Si noti che, dato che l'argomento del metodo main è un vettore, il suo nome è un qualsiasi identificatore. Fin qui abbiamo usato il nome args per chiarezza e per semplicità, ma nulla vieta di usare un nome a propria scelta come per ogni altra variabile del programma. Nel prossimo esempio, infatti, abbiamo scelto un nome diverso. Nota: l'istruzione System.exit(-1); serve per terminare forzatamente l'esecuzione di un programma Java in conseguenza del verificarsi di un errore. 49 Trasformazione di stringhe in elementi di altro tipo Come abbiamo detto, il parametro del main è un vettore di stringhe. Come facciamo se l'utilizzatore vuole passare al programma, ad esempio, dei numeri interi? Data una stringa s, Java mette a disposizione le seguenti primitive per trasformare (ove possibile) s in un elemento di un altro tipo: • Integer.parseInt(s) trasforma s in un elemento di tipo int • Double.parseDouble(s) trasforma s in un elemento di tipo double • Float.parseFloat(s) trasforma s in un elemento di tipo float • • Boolean.parseBoolean(s) trasforma s in un elemento di tipo boolean ... (ne esistono molti altri, ma in questo corso utilizzeremo soltanto questi) Esempio Il seguente frammento di codice trasforma una stringa s in un numero double: String s = "42.76"; double d = Double.parseDouble(s); Alla fine dell'esecuzione di questo frammento di codice, il valore di d è il numero reale 42.76. Chiaramente, le primitive messe a disposizione da Java per trasformare le stringhe in elementi di un tipo diverso funzionano correttamente solo se la trasformazione è possibile, ovvero se la stringa da trasformare è la rappresentazione sotto forma di stringa di un elemento di quel tipo. Non andremo molto nei dettagli su questo argomento, ma l'esempio successivo dovrebbe bastare a chiarirsi le idee. Esempio String s = "ciao"; int n = Integer.parseInt(s); questo programma termina con un errore, dato che la stringa ciao non può essere trasformata in un intero. 50 Questi metodi verranno utilizzati in alcuni dei prossimi esempi. Ad esempio, il prossimo esempio richiede che gli argomenti passati dall'utente siano dei numeri interi e quindi fa uso della primitiva Integer.parseInt. Esempio Scrivere un programma Java che: • • • • controlla che l'utente, al momento dell'esecuzione, abbia passato al programma due argomenti. legge i due argomenti scritti dall'utente al momento dell'esecuzione e li memorizza in due variabili intere L ed N. Definisce un vettore di interi vec di lunghezza L. Assegna ad ogni elemento di vec il valore N. class Esercizio { public static void main (String[] buongiornoATutti) { if (buongiornoATutti.length != 2) { System.out.println("Errore: occorre passare 2 argomenti!"); System.exit(-1); } int L = Integer.parseInt(buongiornoATutti[0]); int N = Integer.parseInt(buongiornoATutti[1]); int [] vec = new int[L]; for (int i = 0; i < L; i++) { vec[i] = N; } } } Cercare di capire questo esempio nei minimi dettagli. 51 Esempio Scrivere un programma Java che: • • legge una sequenza di numeri double scritti dall'utente al momento dell'esecuzione del programma li memorizza in un vettore di double class Esercizio { public static void main (String[] unNomeACaso) { double[] vec = new double[unNomeACaso.length]; for (int i = 0; i < vec.length; i++) { vec[i] = Double.parseDouble(unNomeACaso[i]); } } } Supponiamo che l'utilizzatore esegua il programma (dopo averlo compilato!) come segue: java Esercizio 0.25 1.45 3.57 Alla fine dell'esecuzione, il vettore vec conterrà i tre numeri double 0.25, 1.47 e 3.57. Supponiamo adesso che l'utilizzatore esegua il programma come segue: java Esercizio 0.13 0.14 1 1.72 9.47 Alla fine dell'esecuzione, il vettore vec conterrà i cinque numeri double 0.13, 0.14, 1.0, 1.72, 9.47. Si noti, quindi che il programma funziona indipendentemente dalla lunghezza della sequenza di numeri digitati dall'utente. Supponiamo infine che l'utilizzatore esegua il programma come segue: java Esercizio 0.13 buongiorno 9.47 Il programma termina con un errore perché la stringa buongiorno non può essere trasformata in un numero double. 52 Vettori Multidimensionali In Java è consentito l'utilizzo di vettori multidimensionali, dichiarati e creati con istruzioni della forma: <tipo> [] ... [] <nome> = new <tipo> [<lunghezza1>] ... [<lunghezzaN>]; Il caso particolare che utilizzeremo in questo corso è quello dei vettori bidimensionali o matrici. Ad esempio: double [][] M = new double[4][4]; crea una matrice 4x4 di numeri double. Anche gli array multidimensionali possono essere inizializzati con cicli o per enumerazione. Un esempio di inizializzazione per enumerazione è: double[][] M = {1.0, {9.4, {0.0, }; { 2.0, 0.0}, 0.6, 1.7}, 1.0, 4.9} Un esempio di inizializzazione con un ciclo è: double [][] M = new double[5][8]; for (int i = 0; i < M.length; i++) { for (int j = 0; j < M[i].length; j++) { M[i][j] = <espressione>; } } Come si può vedere dall'esempio di inizializzazione con ciclo: • • M.length indica il numero di righe della matrice M M[i].length indica il numero di posizioni della riga i-esima (numero di colonne della matrice se tutte le righe hanno lo stesso numero di elementi). Gli array bidimensionali possono essere visti come array di array; infatti, l'istruzione: int [][] a = new int[3][5]; è equivalente a: int [][] a; a = new int [3][]; a[0] = new int[5]; a[1] = new int[5]; a[2] = new int[5]; In altri termini, la matrice 3x5 a si può vedere come una array i cui 3 elementi sono array di 5 elementi. 53 12. Metodi Si supponga di voler descrivere a un amico l'itinerario per andare dall'Università di Milano-Bicocca all'Università di Losanna. Un modo possibile per fare ciò è quello di descrivere nei minimi dettagli l'itinerario: <Cammino dall'Università di Milano-Bicocca all'Università di Losanna>: • Uscire dal campus dell'Università di Milano-Bicocca • girare a destra • alla rotonda seguire le indicazioni per l'autostrada • ... In questo modo, potreste sicuramente dare una descrizione corretta dell'itinerario, ma il vostro amico potrebbe risultare confuso dalla enorme quantità di dettagli contenuti nella vostra descrizione. Un modo alternativo di dare questa descrizione è quello di scomporre l'itinerario in sotto-itinerari più semplici: <Cammino dall'Università di Milano-Bicocca all'Università di Losanna>: • <Andare dall'Università di Milano-Bicocca a Aosta> • <Attraversare il tunnel del Gran San Bernardo> • <Andare da Martigny all'Università di Losanna> In un momento successivo, potreste spiegare al vostro amico i dettagli di ognuno di questi sottoitinerari, magari suddividendo ulteriormente ognuno di essi. In questo modo, il vostro amico risulterà sicuramente meno confuso perché a prima vista dovrà capire un numero minore di dettagli ed avrà una visione di insieme dell'itinerario da compiere. Inoltre, il vostro amico potrebbe già conoscere alcuni di questi sotto-itinerari, il che vi risparmierebbe la fatica di descriverli nei minimi dettagli. In altri termini, spesso nella vita di tutti i giorni è utile fare delle astrazioni e presentare le cose in modo modulare. Fare delle astrazioni significa non occuparsi, almeno in un primo momento, di alcuni dettagli; presentare una descrizione in modo modulare significa far capire la sua struttura generale. Spesso è utile fare questo suddividendo un problema in sotto-problemi, come nel caso dell'esempio precedente, dando dei nomi a ogni sotto-problema e descrivendone i dettagli in un secondo tempo. Questo è ciò che i metodi permettono di fare all'interno di un programma Java! Programmare utilizzando i metodi significa programmare in modo modulare, ovvero avere la possibilità di scrivere programmi: • • più chiari, ovvero maggiormente leggibili e comprensibili da altre persone senza ridondanze o ripetizioni Un metodo è un insieme di dichiarazioni e di istruzioni, che può eventualmente anche avere dei parametri, cui viene dato un nome. I comandi contenuti in un metodo possono essere eseguiti semplicemente scrivendo il nome del metodo ed eventualmente passandogli dei parametri. 54 In questo modo, se la stessa sequenza di comandi dovesse essere ripetuta più volte all'interno di uno stesso programma, tali comandi non dovranno più essere scritti ripetute volte, ma basterà semplicemente scrivere ripetute volte il nome del metodo. Per il momento, ci dedicheremo soltanto ai cosiddetti metodi statici (parola chiave static). Vedremo nella parte finale del corso il significato della parola chiave static e la differenza tra i metodi statici e quelli dinamici (in particolare, sarà possibile comprendere questa differenza quando verrà affrontato il concetto di classe). Definizione di un metodo statico E' una porzione del codice di un programma nella quale di definisce: • • • Il nome del metodo Eventualmente i suoi parametri ed il tipo del suo risultato Le dichiarazioni e le istruzioni che ne fanno parte (si dice che esse costituiscono ciò che viene chiamato il corpo del metodo). Sintassi (semplificata): static <tipo> <identificatore> (<lista di parametri formali>) <sequenza di dichiarazioni e istruzioni>; } { dove: <identificatore> è il nome del metodo, <tipo> è il tipo del risultato restituito dal metodo, <lista di parametri formali> è una sequenza di dichiarazioni di variabili (senza inizializzazione) che verranno usate nel corpo del metodo, separate da virgole. <lista di parametri formali> può eventualmente essere vuota e <tipo> può eventualmente essere uguale alla parola chiave void, nel caso in cui il metodo non restituisca alcun risultato. Se il metodo restituisce un risultato, l'ultima istruzione di <lista di dichiarazioni e istruzioni> deve essere una istruzione return, che appunto restituisca il risultato. Chiamata di un metodo statico E' una porzione del codice di un programma che permette di eseguire i comandi che fanno parte del metodo. Sintassi (semplificata): <identificatore> (<lista di parametri attuali>) dove: <identificatore> è lo stesso nome usato al momento della definizione del metodo e <lista di parametri attuali> è una lista di espressioni o variabili separate da virgole i cui tipi di dato devono corrispondere uno ad uno (da sinistra a destra in modo ordinato) ai tipi delle variabili dichiarate in <lista di parametri formali>. <lista di parametri attuali> deve essere vuota se e solo se <lista di parametri formali> è vuota. 55 Esempio Il seguente programma Java contiene un metodo che non restituisce alcun risultato, che non riceve alcun parametro e che non contiene dichiarazioni al suo interno. Tutto ciò che fa questo metodo è stampare a video la stringa Ciao! class Esempio { static void ciao() { System.out.println("Ciao!"); } public static void main (String[] args) { ciao(); } } Note: • Se un metodo non restituisce alcun risultato (ma ha il solo effetto di compiere delle azioni, come ad esempio stampare a video qualche informazione) allora <tipo> deve essere sostituito dalla parola chiave void. • Se un metodo non restituisce alcun risultato, allora la lista dei parametri formali e attuali è vuota, ma ciò non toglie che debbano essere usate le parentesi (aperta e chiusa) sia al momento della definizione che a quello della chiamata. Ciò consente al compilatore e all'interprete di Java di "capire" che si tratta di un metodo e non di una variabile. • main è un metodo particolare: è sempre il primo metodo ad essere eseguito quando comincia l'esecuzione di un programma Java ed è l'unico metodo che non deve essere mai chiamato all'interno del programma: la chiamata del metodo main viene eseguita automaticamente dall'interprete di Java all'inizio dell'esecuzione del programma. 56 Esempio Il programma seguente contiene un metodo che ha due parametri di tipo intero e che restituisce un risultato di tipo intero. Il risultato è la somma dei due parametri. class Esempio { static int somma (int a, int b) { int risultato; risultato = a + b; return(risultato); } public static void main (String[] args) { int x = 5; int y = 3; int z; z = somma(x, y); System.out.println(z); } } Per capire il funzionamento di questo programma occorre sapere che: • I valori utilizzati al momento della chiamata del metodo (parametri attuali) vengono copiati nelle variabili utilizzate al momento della definizione del metodo (parametri formali). Questo genere di passaggio dei parametri si chiama passaggio per valore. • Tutte le variabili dichiarate all'interno di un metodo (compresi i parametri formali) sono variabili locali a quel metodo: sono create al momento della chiamata del metodo e distrutte alla fine dell'esecuzione del metodo stesso. Esse non possono in alcun modo essere utilizzate da altri metodi. • Il comando return termina l'esecuzione di un metodo. Nel caso in cui il metodo restituisca un valore, il valore restituito dal metodo (passato come argomento alla primitiva return) viene copiato nella variabile che compare a sinistra del simbolo = al momento della chiamata del metodo. L'istruzione return deve sempre essere l'ultima di un metodo (dato che termina l'esecuzione del metodo, tutte quelle che fossero eventualmente scritte dopo di essa non verrebbero mai eseguite in nessun caso). Si noti, invece, che se un metodo non restituisce alcun risultato (metodo void) allora esso può non contenere l'istruzione return o, al limite, può contenere l'istruzione return senza alcun parametro. In base a queste considerazioni, cerchiamo di spiegare passo per passo il comportamento del metodo precedente. Nel leggere i seguenti punti, si tenga davanti anche il codice del programma. • All'inizio dell'esecuzione del programma, come sempre, viene eseguita la prima istruzione del metodo main. Quindi viene creata una variabile intera x a cui viene assegnato il valore 5. 57 • Viene creata una variabile intera y a cui viene assegnato il valore 7. • Viene creata una variabile intera z a cui non viene assegnato alcun valore. • Il valore di z viene assegnato al risultato restituito dal metodo somma a cui vengono passati come parametri x e y. Per poter conoscere questo valore (da assegnare alla variabile z), quindi, occorre eseguire il metodo somma. Prima di cominciare l'esecuzione del corpo del metodo somma, l'interprete di Java compie le seguenti azioni: • Copia del valore di x ( = 5) nella variabile a e copia del valore di y (= 3) nella variabile b. In altri termini il valore del primo parametro attuale viene copiato nel primo parametro formale e il valore del secondo parametro attuale viene copiato nel secondo parametro formale. Ciò è possibile a patto che il primo parametro attuale abbia lo stesso tipo del primo parametro formale e che il secondo parametro attuale abbia lo stesso tipo del secondo parametro formale (ipotesi verificata in questo caso). • A questo punto, inizia l'esecuzione del metodo somma. Si può immagina che il "controllo del programma" lasci il metodo main per passare al metodo somma. Esso verrà successivamente "restituito" al metodo main alla fine del metodo somma. La prima azione dell'esecuzione del metodo somma è, dunque, la creazione di una variabile intera chiamata risultato a cui non viene, per il momento, assegnato alcun valore. • Alla variabile risultato viene assegnato il valore dell'espressione a+b, ovvero 8. • Viene eseguita l'istruzione return. Essa ha due effetti: il valore passato come parametro all'istruzione return (ovvero 8) viene copiato nella variabile che compariva sulla sinistra del simbolo = al momento della chiamata del metodo somma (ovvero la variabile z nel metodo main) e il metodo somma termina. Prima di "restituire il controllo" al metodo main e finirne l'esecuzione, l'interprete di Java compie le seguenti azioni: • Viene distrutta (ovvero "cancellata" dalla memoria del calcolatore) la variabile a. • Viene distrutta la variabile b. • Viene distrutta la variabile risultato. • A questo punto, l'esecuzione del metodo main continua con l'esecuzione dell'istruzione che segue quella della chiamata al metodo somma, ovvero con l'esecuzione dell'istruzione che stampa a video il valore della variabile z (= 8). In conclusione, questo programma ha come effetto quello di stampare a video la somma, ovvero 8. Una comprensione accurata di questo esempio è di fondamentale importanza per poter comprendere le pagine seguenti. 58 Esempio Il seguente programma Java contiene un metodo di nome scrivereNVolte che riceve due parametri e non restituisce alcun risultato (metodo void). L'effetto di questo metodo è stampare a video N volte una stinga, dove N è il primo parametro e la stringa da stampare è il secondo parametro. Nota: per quanto si tratti di un programma molto semplice, dovrebbe già risultare chiara l'importanza fondamentale di utilizzare i metodi: se ad esempio vogliamo scrivere una stringa (diciamo "ciao") per 50 volte e poi un'altra stringa (diciamo "buongiorno") per 100 volte, non dobbiamo scrivere due comandi iterativi cambiando ogni volta il numero di iterazioni e la stringa da stampare (come saremmo costretti a fare se non potessimo usare i metodi), ma basta semplicemente chiamare due volte il metodo scrivereNVolte con due parametri diversi! Il nostro codice contiene una sola istruzione for (indipendentemente dal numero di volte in cui si vuole eseguire la stampa), nel corpo del metodo scrivereNVolte e la struttura del metodo main (che chiama il metodo scrivereNVolte) è molto semplice! class Esempio { static void scrivereNVolte (String s, int N) { for (int i = 0; i < N; i++) { System.out.println(s); } } public static void main (String[] args) { scrivereNVolte("ciao", 50); scrivereNVolte("buongiorno", 100); /* * * * * */ Non vi sembra carino questo programma? Posso stampare la stringa che voglio, per il numero di volte che voglio, senza dover più scrivere nessun ciclo for !!! } } Nota: i parametri attuali possono anche essere dei valori costanti (come in questo programma), oltre che delle variabili (come nell'esempio precedente) o delle espressioni. La stessa cosa vale (nel caso in cui un metodo debba restituire un risultato e quindi non sia un metodo void) per l'argomento dell'istruzione return. 59 Passaggio dei parametri Come abbiamo già detto, la modalità di passaggio dei parametri semplici in Java è "per valore". Vediamo più in dettaglio che cosa questo significhi, e quali implicazioni abbia, tramite un esempio. Supponiamo di voler scrivere un metodo il cui unico effetto sia quello di sommare una costante numerica (diciamo 4) al parametro (di tipo int) che riceve in ingresso. Ecco come ci si potrebbe aspettare di scrivere questo metodo all'interno di un programma Java: class Esempio { public static void main (String[] args) { int n = 10; piu4(n); System.out.println(n); } static void piu4 (int x) { x = x+4; } } Per quanto possa risultare a prima vista strano, questo programma stampa a video 10 (e non 14 come forse ci si potrebbe aspettare!). Infatti, vengono eseguiti i seguenti passi: • • • • • Viene creata una variabile intera n a cui viene assegnato il valore 10. La chiamata al metodo piu4 ha come effetto quello di creare una nuova variabile intera x (il parametro formale), copiare il valore del parametro attuale (n = 10) nel parametro formale (x) e far cominciare l'esecuzione del metodo piu4. Viene sommato 4 valore di x (il nuovo valore di x adesso è 14). Successivamente il metodo termina e ciò ha come effetto il fatto che venga compiuta la seguente azione: Viene cancellata la variabile x (di fatto l'operazione di somma che è stata eseguita è stata inutile!). Il controllo ritorna al main, che stampa il valore di n (= 10). Passare un parametro "per valore" a un metodo significa copiare il valore del parametro attuale nel parametro formale. Per effetto del passaggio per valore, ciò che si sarebbe dovuto scrivere per ottenere l'effetto di stampare a video 14 (ovvero fare in modo che la somma, fatta nel metodo piu4, sia "visibile" anche nel metodo chiamante) è: 60 class Esempio { public static void main (String[] args) { int n = 10; n = piu4(n); System.out.println(n); } static int piu4 (int x) { x = x+4; return(x); } } Come abbiamo già detto, infatti, oltre a quello di terminare l'esecuzione del metodo, l'istruzione return ha anche il compito di copiare il valore del suo parametro (x = 14) nella variabile che compare a sinistra del simbolo = al momento della chiamata (n). In altre parole, n è l'unico sistema che un metodo possiede per "comunicare" il risultato per proprio calcolo al metodo che lo ha chiamato. Ancora un po' di osservazioni: • n e x sono due variabili (ovvero due regioni di memoria) diverse. Esse sarebbero diverse anche avessero lo stesso nome! In altri termini, i parametri attuali e formali possono anche avere lo stesso nome, ma rimangono sempre due variabili diverse. La visibilità dei parametri formali è limitata al metodo chiamato, mentre la visibilità dei parametri attuali è limitata (a meno di non compiere azioni assai poco consigliabili) al metodo chiamante, quindi, anche se i parametri formali hanno lo stesso nome dei parametri attuali, non vi è alcuna ambiguità tra essi (ovvero in ogni punto del programma in cui un nome compare, è sempre possibile stabilire se si tratta di un parametro formale o di un parametro attuale). • L'ordine in cui scriviamo le definizioni dei metodi nel nostro programma è assolutamente ininfluente: in ogni caso, il programma comincia sempre con l'esecuzione del main e poi è l'ordine con cui i metodi vengono chiamati a stabilire l'ordine in cui vengono eseguiti. Attenzione: il passaggio dei parametri in Java non avviene sempre "per valore". Il prossimo esempio mostra un caso in cui il passaggio dei parametri avviene con una modalità detta "per riferimento": il caso in cui i parametri siano vettori. 61 Esempio class Esempio { public static void main (String[] args) { int [] vet = new int[2]; vet[0] = 10; vet[1] = 15; piu4(vet); System.out.println(vet[0] + " " + vet[1]); } public void piu4 (int [] v) { v[0] = v[0] + 4; v[1] = v[1] + 4; } } Questo programma stampa a video 14 19. Perché? Perché nel caso in cui i parametri siano vettori, non viene copiato il valore di ogni elemento del parametro attuale nel parametro formale (vi immaginate quanto sarebbe dispendioso fare questa copia per vettori con migliaia di elementi?), ma viene copiato l'indirizzo di memoria. In altre parole, se il parametro è un vettore o un altro tipo complesso di Java, dopo la chiamata del metodo il parametro formale e il parametro attuale fanno riferimento alla stessa area di memoria. In altri termini, sono a tutti gli effetti la stessa variabile! (... e questo vale indipendentemente dal fatto che essi abbiano lo stesso nome o nomi diversi!). Questo tipo di passaggio dei parametri si chiama "per riferimento". Quindi, siamo in grado di enunciare la seguente regola generale: il passaggio dei parametri in Java avviene "per valore" se i parametri sono di un tipo semplice (short, int, long, float, double, char, boolean, ...) e "per riferimento" se essi sono di un tipo complesso (vettori, stringhe, ...). Osservando il precedente esempio, si noti inoltre che: al momento della definizione di un metodo che riceve un parametro di tipo vettore, non occorre specificare la lunghezza del vettore, ma solamente il tipo di dato dei suoi elementi e il suo nome. La lunghezza di questo vettore verrà automaticamente settata a quella del parametro attuale corrispondente. Di seguito vengono presi in considerazione alcuni errori tipici che vengono spesso commessi quando si programma usando i metodi. 62 Errori Tipici Esempio 1 static int metodo (int i) { if (i > 0) { System.out.println(i); } else { return(i); } } il compilatore di Java restituisce un errore quando si cerca di compilare un programma che contiene un metodo come questo. L'errore che viene segnalato è "Return required at the end of int metodo(int)". Ciò avviene perché qualsiasi possibile sequenza di esecuzione di un metodo che restituisce un risultato (metodo non-void o con tipo) deve obbligatoriamente terminare con l'istruzione return. In questo caso, il metodo return viene eseguito solo nel caso in cui venga passato al metodo un numero negativo. In tutti gli altri casi, esso non viene eseguito. Esempio 2 static int metodo (int i) { int j = i+5; return(j); j = j+1; } Anche in questo caso, il compilatore di Java termina con un errore. Stavolta l'errore segnalato è "Statement not reached j=j+1;". L'istruzione return deve essere sempre l'ultima istruzione ad essere eseguita in un metodo. 63 Un piccolo riassunto sui metodi statici Metodi void Eseguono delle azioni, ma non restituiscono nessun risultato. Si definiscono con la seguente sintassi (semplificata): static void <identificatore> (<lista di parametri formali>) { <sequenza di dichiarazioni e istruzioni>; } e si chiamano con la seguente sintassi (semplificata): <identificatore> (<lista di parametri attuali>); I parametri attuali devono avere (in modo ordinato da sinistra a destra) lo stesso tipo di quelli formali. Metodi con tipo Eseguono delle azioni e restituiscono un risultato. Si definiscono con la seguente sintassi (semplificata): static <tipo> <identificatore> (<lista di parametri formali>) { <sequenza di dichiarazioni e istruzioni>; return <espressione>; } e si chiamano con la seguente sintassi (semplificata): <identificatore> = <identificatore> (<lista di parametri attuali>); • • • I parametri attuali devono avere (in modo ordinato da sinistra a destra) lo stesso tipo di quelli formali. L'identificatore a sinistra del simbolo = nella chiamata deve essere una variabile dello stesso tipo di quello specificato come tipo del risultato nella definizione. L'espressione passata come argomento all'istruzione return deve avere lo stesso tipo di quello specificato come tipo del risultato nella definizione. 64 Visibilità degli identificatori Fin'ora abbiamo preso in considerazione solo variabili locali, ovvero visibili all'interno del blocco nel quale sono state dichiarate. In Java è possibile utilizzare anche variabili globali, ovvero visibili ed utilizzabili in tutti i metodi di una certa classe: basta dichiararle all'esterno dei metodi. Esempio class Esempio { int n; public static void main (String[] args) { int x; n = 10; x = 4; myMethod(); } static void myMethod() { n = n + 2; x = x + 3; } // // --------------> OK ---------------> ERRORE } In questo programma, x è visibile soltanto all'interno del metodo main, in quanto essa è stata dichiarata all'interno del metodo main. Viceversa, n è visibile sia nel metodo main che nel metodo myMethod perché è stata dichiarata esteriormente a questi due metodi. Una variabile come n, ovvero una variabile visibile da tutti i metodi di una classe, si dice variabile globale. Una variabile come x, ovvero visibile solo in un particolare metodo, si dice locale a quel metodo. 65 12. Ricorsione Prima di iniziare la lettura di questo paragrafo, si tenga presente il fatto che il concetto di ricorsione, in questo documento, viene trattata in modo molto rapido e quindi, talvolta, approssimativo, con il solo scopo di dare allo studente una comprensione generica e superficiale del tema. Per una trattazione più completa e approfondita di questo affascinante argomento, si veda l'apposito capitolo sul libro di testo. Definire una funzione f in modo ricorsivo significa definire f usando f (in altri termini, f è definita in funzione di se stessa). Un tipico esempio di funzione che può essere facilmente definita in modo ricorsivo è la funzione fattoriale. Nella sua definizione non ricorsiva, il fattoriale di un numero interno non negativo N è dato da: fatt(N) = N * (N-1) * (N-2) * ... * 2 * 1 (spesso fatt(N) viene indicato con la notazione N!). La funzione fattoriale può essere definita in modo ricorsivo come segue: fatt(N) = 1 fatt(N) = N * fatt(N-1) se N = 0 se N > 0 Si noti la struttura di questa definizione: • Un caso che NON utilizza la funzione fatt nella sua definizione (detto caso base o caso iniziale). Questo caso vale sul più piccolo dei valori possibili su cui si può definire la funzione fatt (N = 0). • Un caso in cui la funzione fatt di un qualsiasi numero N viene definito in funzione della funzione fatt su un numero più piccolo di N. Grazie alla presenza di questi due casi, questa definizione può essere applicata per ottenere il fattoriale di un qualsiasi numero intero non negativo. Ad esempio, per calcolare il fattoriale di 4, si possono applicare i seguenti passaggi: fatt(4) = 4 * fatt(3) = 4 * 3 * fatt(2) = 4 * 3 * 2 * fatt(1) = 4 * 3 * 2 * 1 * fatt(0) = 4 * 3 * 2 * 1 = 24 In altri termini, il fattoriale di 4 può essere definito in funzione del fattoriale di 3. A sua volta, il fattoriale di 3 può essere definito in funzione del fattoriale di 2, e così via, finché non si arriva a definire il fattoriale del numero di partenza in funzione del fattoriale di 0. Il fattoriale di 0, non essendo definito in funzione della funzione fatt, permette di completare il calcolo. Si può dire che il caso iniziale permette alla definizione di terminare. Se non ci fosse il caso iniziale, la funzione fatt sarebbe sempre definita in funzione della funzione fatt stessa e non si arriverebbe mai al punto in cui il calcolo è composto solo da costanti e quindi può essere completato. La struttura di questa definizione, per quanto semplice, può essere generalizzata. Per definire una funzione f in modo ricorsivo, infatti, occorre: 66 • Stabilire un ordinamento sui possibili oggetti a cui f può essere applicata (se questi oggetti sono numeri, l'ordinamento è semplicemente quello numerico, ma non è detto che essi siano sempre numeri!). • Definire f senza usare f stessa su uno o più elementi, tali che non esistano altri elementi più piccoli nell'ordinamento fissato (casi base o iniziali). • Per tutti gli altri elementi, definire f in funzione del valore di f su elementi più piccoli nell'ordinamento fissato. Un'altro esempio di funzione che si presta in modo molto naturale ad essere definita in modo ricorsivo è la funzione che, dato un numero intero e non negativo N, restituisce l'N-esimo "numero di Fibonacci". La sequenza dei primi numeri di Fibonacci è: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ... la sequenza continua ottenendo ogni numero grazie alla somma del suo precedente e del precedente del suo precedente. La definizione ricorsiva della funzione che restituisce l'N-esimo numero di Fibonacci è: fibo(N) = 0 fibo(N) = 1 fibo(N) = fibo(N-1) + fibo(N-2) se N = 0 se N = 1 altrimenti cercare di capire nei dettagli questa definizione prima di proseguire con la lettura. In particolare, è importante convincersi del fatto che, affinché questa definizione sia corretta, è opportuno utilizzare due casi iniziali. Analogamente a quanto avviene per le funzioni nel mondo matematico, il linguaggio Java consente di definire metodi ricorsivi. Un metodo ricorsivo è un metodo che chiama se stesso nella propria definizione. Come nel caso delle funzioni matematiche, anche nel caso dei metodi ricorsivi, è opportuno che, per alcuni valori dei parametri, la sequenza di esecuzione possa calcolare il risultato senza dover chiamare il metodo stesso (casi iniziali). Tipicamente, ciò si ottiene con delle istruzioni condizionali if all'interno del corpo dei metodi. 67 Esempio Il seguente programma Java contiene la definizione di un metodo ricorsivo che calcola il fattoriale di un numero intero e non negativo digitato dall'utente. class Esempio { public static void main (String[] args) { if (args.length != 1) { System.out.println("Errore: occorre specificare un argomento!"); System.exit(-1); } int n = fatt(Integer.parseInt(args[0]); System.out.println(n); } static int fatt (int x) { if (x < 0) { System.out.println("Errore: numero negativo!"); System.exit(-1); } int risultato; if (x == 0) { risultato = 1; } else { risultato = x * fatt(x-1); } return(risultato); } } 68 Esempio Il seguente programma Java contiene la definizione di un metodo ricorsivo che calcola l'N-esimo numero di Fibonacci, con N numero intero e non negativo digitato dall'utente. class Esempio { public static void main (String[] args) { if (args.length != 1) { System.out.println("Errore: occorre specificare un argomento!"); System.exit(-1); } int num = fib(Integer.parseInt(args[0]); System.out.println(num); } static int fib (int n) { if (n < 0) { System.out.println("Errore: numero negativo!"); System.exit(-1); } int risultato; if (n == 0) { risultato } else if (n == risultato } else { risultato } = 0; 1) { = 1; = fib(n-1) + fib(n-2); return(risultato); } } 69 13. Classi Supponiamo di dover scrivere un programma che gestisce i pazienti di un ospedale, ovvero registra le uscite e entrate di tutti i pazienti e, per ognuno dei pazienti ricoverati, registra varie informazioni di diversa natura, come ad esempio: • • • • • • • • il nome, il cognome, l'indirizzo di residenza, il codice fiscale, il tipo di malattia, le malattie contratte in passato, alcune funzioni che, in base ai risultati di alcuni esami, decidono il tempo di ricovero e la terapia, ecc. Con gli strumenti visti fino a questo momento, è sicuramente possibile scrivere un programma in Java che gestisce tutti questi dati. Ad esempio, per ogni paziente, informazioni come il nome, il cognome, la malattia, ecc. potrebbero essere singole variabili, mentre, sempre per ogni paziente, le funzioni potrebbero essere realizzate tramite metodi. Ma si pensi alla struttura del programma nel caso in cui l'ospedale fosse in grado di ospitare fino a diverse migliaia di pazienti: ci troveremmo di fronte ad una enorme quantità di variabili e di metodi e sarebbe difficile collezionare tutte le informazioni relative a un unico paziente. Non sarebbe, invece, molto più semplice poter raccogliere in un'unica struttura tutte le informazioni relative ad ogni singolo paziente? Questo problema in Java è risolto dal concetto di classe. Esistono svariate definizioni di classe. Probabilmente le più semplici sono: • Una classe è un costrutto che permette di incapsulare informazioni e comportamenti comuni a un insieme di oggetti. • Una classe è ciò che definisce la struttura di un oggetto. Fin'ora, abbiamo preso in considerazione soltanto programmi Java composti da una sola classe. In realtà, normalmente, un programma Java si presenta come un insieme di classi, che possono o meno essere contenute in un insieme di files di testo. La struttura generale di un programma Java può essere rappresentata come segue, dove ogni rettangolo rappresenta un file di testo, il cui nome è scritto al di sotto del rettangolo stesso : 70 public class C1{ ... } public class C2{ ... } ... ... public class CN{ ... public static void main (String[] args) { ... } ... } class A { ... } ... ... ... class B { ... } ... C1.java C2.java CN.java Si noti che: • Ogni file può contenere una o più classi, ma una sola classe può essere dichiarata public all'interno dello stesso file (vedremo tra poco il significato della parola chiave public). • Il nome di ogni file deve obbligatoriamente essere uguale al nome di una delle classi che esso contiene (la class public, se essa esiste), con l'estensione .java • Una ed una sola classe deve contenere il metodo main, che è il primo metodo che viene eseguito al momento in cui il programma viene messo in esecuzione. • La classe che contiene il metodo main deve essere contenuta in un file con lo stesso nome, più l'estensione .java. Prima di poter eseguire un programma con questa struttura, ogni file deve essere separatamente compilato. La fase di compilazione, dunque, prevede stavolta le seguenti chiamate: javac C1.java javac C2.java ... javac CN.java Se non vi sono errori di sintassi in nessuno dei file C1.java, C2.java, ..., CN.java, il compilatore crea automaticamente i file in byte-code C1.class, C2.class, ..., CN.class. Per eseguire il programma, occorre chiamare (soltanto): java CN In altri termini, per eseguire il programma, occorre chiamare il comando java con il nome (senza estensione) del file che contiene il metodo main. 71 Vediamo adesso il nostro primo programma Java composto da più di una classe. Come discusso all'inizio di questo paragrafo, supponiamo di voler gestire i pazienti di un ospedale. Per scrivere questo programma, è opportuno definire una classe (che chiameremo Paziente) che contenga tutte le informazioni relative a un paziente. Ecco un esempio di come potrebbe essere la struttura di un tale programma (notevolmente semplificato): class Paziente { String nome; String cognome; int eta; void stampa() { System.out.println("Il paziente si chiama " + nome + " " + cognome + " e ha " + eta + "anni"); } } public class Ospedale { public static void main (String[] args) { Paziente paz1 = new Paziente(); Paziente paz2 = new Paziente(); paz1.nome = "Giovanni"; paz1.cognome = "Rossi"; paz1.eta = 40; paz2.nome = "Francesco"; paz2.cognome = "Bianchi"; paz2.eta = 35; paz1.stampa(); paz2.stampa(); } } Questo programma stampa a video: Il paziente si chiama Giovanni Rossi e ha 40 anni Il paziente si chiama Francesco Bianchi e ha 35 anni Si noti una cosa di fondamentale importanza: le variabili paz1 e paz2 sono variabili "di tipo Paziente" !! In generale, una volta che il programmatore ha definito una nuova classe, tale classe può essere utilizzata come tutti gli altri tipi di dato. Si dice che paz1 e paz2 sono istanze della classe Paziente o oggetti. Il concetto di oggetto (molto importante nella programmazione in Java, tanto è vero che Java viene definito un linguaggio "Orientato agli Oggetti" o "Object Oriented") è un concetto ben distinto da (seppur collegato a) quello di classe: nell'esempio precedente, infatti, la classe Paziente contiene tutte le informazioni che un paziente deve avere (o, se si preferisce, che un oggetto deve avere per poter essere considerato un paziente): nome, cognome ed età. Viceversa, paz1 (come paz2) è un particolare paziente, con il suo particolare nome, il suo cognome e la sua età. In altri termini, in paz1 (come in paz2) sono stati dati dei valori, dei contenuti ben precisi, a tutte le informazioni contenute nella classe Paziente. Si dice che paz1 (come paz2) è un'istanza della classe 72 Paziente perché, appunto, in paz1 (come in paz2) tutte le informazioni della classe Paziente sono state "istanziate": sono stati dati loro dei valori ben precisi. Alla luce di questa discussione, si riprendano adesso le due definizioni di classe date in precedenza: • Una classe è un costrutto che permette di incapsulare informazioni e comportamenti comuni a un insieme di oggetti. • Una classe è ciò che definisce la struttura di un oggetto. Adesso, esse dovrebbero risultare un po' più chiare. Una possibile definizione di oggetto, invece, può essere semplicemente la seguente: • Un oggetto è un'istanza di una classe. Struttura generale della definizione di una classe Sintassi: <modificatore> class <identificatore> <sequenza di attributi> <sequenza di costruttori> <sequenza di metodi> } { dove: • • • gli attributi sono le informazioni delle istanze di quella classe (variabili), i metodi sono i comportamenti delle istanze di quella classe (funzioni), i costruttori possono essere per il momento considerati come particolari metodi, di cui parleremo tra poco. Ognuna delle tre sequenze che fanno parte di una classe (di attributi, di costruttori, di metodi) può essere vuota. Ad esempio, la classe Paziente dell'esempio precedente conteneva soltanto tre attributi (nome, cognome, età), nessun metodo e nessun costruttore. Un modificatore può essere una tra le seguenti parola chiave di Java: public, private, static, final, ecc. .... Esse verranno definite tra poco. Struttura della definizione di un attributo Sintassi: <modificatore> <tipo> <identificatore> ⎡ = <espressione> ⎤ ; dove, ancora una volta, la presenza di tutto ciò che è compreso tra i simboli ⎡ e ⎤ è da considerarsi opzionale. 73 Esempi: String nome; private String nome = "Francesco"; Struttura della definizione di un metodo Sintassi: <modificatore> <tipo> <identificatore> (<lista di parametri formali>) { <sequenza di dichiarazioni e istruzioni> } Come abbiamo già visto nell'esempio precedente, i metodi e gli attributi di un'istanza di una classe possono essere acceduti, rispettivamente, con la seguente sintassi: <nome dell'oggetto>.<nome dell'attributo>; <nome dell'oggetto>.<nome del metodo> ( <lista dei parametri attuali>); dove <nome dell'oggetto>, <nome dell'attributo> e <nome del metodo> sono identificatori. Esempi: pat1.nome; pat1.stampa(); 74 Costruttore Un costruttore di una classe è un metodo particolare, che viene eseguito al momento della creazione di un'istanza. Esso deve necessariamente avere lo stesso nome della classe che lo contiene e non può mai restituire un risultato. Quindi, non deve essere scritto nessun tipo di dato (neanche void!) nella sua definizione. Per chiamare un costruttore, occorre utilizzare la parola chiave new. Definizione di un Costruttore Sintassi: <identificatore> (<lista parametri formali>) { <sequenza di dichiarazioni e istruzioni> } Esempio Paziente() { ... } Chiamata di un costruttore (ovvero creazione di un oggetto) Sintassi: <nome costruttore> <nome oggetto> = new <nome costruttore> (<lista parametri attuali>); Esempio Paziente pat1 = new Paziente(); Esempio Il programma contiene una possibile realizzazione della classe Paziente dell'esempio precedente contenente un costruttore. class Paziente { String nome; String cognome; int eta; Paziente (String n, String c, int e) { cognome = c; nome = n; if (e > 14) { eta = e; } else { System.out.println("Paziente troppo giovane!"); 75 System.exit(-1); } } } public class Ospedale { public static void main (String[] args) { Paziente pat1 = new Paziente ("Giovanni", "Rossi", 40); Paziente pat2 = new Paziente ("Francesco", "Bianchi", 35); ... } } In questo caso, se viene creato un paziente la cui età è inferiore a 14 anni, il programma viene interrotto con un errore (intendendo che il paziente deve probabilmente rivolgersi a un ospedale pediatrico). Vantaggi di programmare con i costruttori: • codice più semplice, • si possono inserire dei controlli. Una classe può contenere diversi costruttori, purché essi si distinguano per il numero o il tipo dei loro parametri: class Paziente { ... Paziente () {...} Paziente (String cognome, int eta) {...} Paziente (String nome, String cognome, int eta) {...} ... } Quando una classe non possiede alcun costruttore, Java mette a disposizione un costruttore "di default" senza alcun parametro, che inizializza le variabili dell'istanza con valori di default conformemente al loro tipo e non compie alcuna altra azione. I valori di default di alcuni tipi standard sono: int --> 0 double --> 0.0 boolean --> false qualsiasi oggetto --> null 76 Esempio Il seguente programma Java contiene la definizione di una classe Rettangolo che racchiude tutte le informazioni necessarie per definire una figura geometrica di forma rettangolare (base e altezza), un costruttore e un metodo per calcolare l'area. Questa classe viene usata nel metodo main di un'altra classe. class Rettangolo { double base; double altezza; Rettangolo (double b, double h) { if (b <= 0 || h <= 0) { System.out.println("Errore: la base e l'altezza non possono essere negativi!"); System.exit(-1); } base = b; altezza = h; } double area () { return (base * altezza); } } public class Esempio { public static void main (String[] args) { Rettangolo rett = new Rettangolo (10.4, 4.0); double a = rett.area(); System.out.println(a); } } Questo programma stampa a video 40. 77 Parole chiave public e private Supponiamo di voler scrivere un programma Java che contiene una classe (di nome Semaforo) che descrive il comportamento di un semaforo stradale. Una possibile realizzazione della classe Semaforo è la seguente: class Semaforo { String colore; void cambiaColore() { if (colore.equals("rosso")) { colore = "verde"; } else if (colore.equals("verde")) { colore = "giallo"; } else if (colore.equals("giallo")) { colore = "rosso"; } } } In questo modo, il colore del semaforo è rappresentato da un attributo della classe e il metodo cambiaColore regola il funzionamento del semaforo, permettendogli di cambiare colore in funzione del colore corrente. Scriviamo adesso una possibile realizzazione di un metodo main (ad esempio contenuto in un'altra classe di nome Esempio) che utilizza la classe semaforo: public class Esempio { public static void main (String[] args) { Semaforo sem = new Semaforo(); sem.colore = "rosso"; // inizializzazione del semaforo (1) sem.cambiaColore(); System.out.println(sem.colore); } } Se il metodo main è scritto in questo modo, il programma funziona correttamente: il semaforo viene inizializzato al valore "rosso" e ogni volta che viene chiamato il metodo cambiaColore, il colore del semaforo cambia nel modo corretto. Ma si supponga che la classe Esempio e la classe Semaforo vengano scritte da due programmatori diversi (è ciò che accade molto spesso nelle aziende di software!) e che il programmatore che deve scrivere la classe Esempio non conosca nei dettagli ne' il significato ne' l'implementazione della classe Semaforo (è ciò che, purtroppo, accade molto spesso nelle aziende di software!). Chi gli impedisce di inizializzare l'attributo colore con una stringa che non sia ne' "rosso", ne' "giallo", ne' "verde"? Egli potrebbe, ad esempio, inizializzare l'attributo colore come segue (nel punto del programma contrassegnato da (1)): sem.colore = "blu"; oppure l'inizializzazione potrebbe avvenire utilizzando stringhe ancora più lontane da ciò che vorremmo venisse usato: sem.colore = "Buongiorno a tutti!"; 78 Inizializzazioni di questo tipo comporterebbero effetti disastrosi su tutto il comportamento del programma! Ogni volta che verrebbe chiamato il metodo cambiaColore, tale metodo non avrebbe alcun effetto e il semaforo non cambierebbe mai il suo "colore" iniziale! Un buon modo per impedire al programmatore che implementa la classe Esempio di commettere simili disastri, è quello di impedirgli di modificare direttamente l'attributo colore (tramite la parola chiave private) e di consentirgli, come unico sistema per inizializzare il colore del semaforo, di utilizzare un apposito metodo (dichiarato usando la parola chiave public) nel quale sono contenuti dei controlli rigidi sulla stringa che si sta tentando di usare per l'inizializzazione. Ecco il significato delle parole chiave private e public: • Un metodo o un attributo di una classe C dichiarato con la parola chiave private non può essere acceduto direttamente da metodi appartenenti a classi diverse da C. • Un metodo o un attributo dichiarato con la parola chiave public può essere acceduto da qualsiasi classe che compone il programma. Ed ecco la versione "più sicura" del precedente programma: class Semaforo { private String colore; public void setColore (String col) { if (!col.equals("rosso") && !col.equals("giallo") && !col.equals("verde)) { System.out.println("Errore. Unici colori ammissibili: rosso, giallo, verde"); System.exit(-1); } colore = col; } public String getColore() { return(colore); } public void cambiaColore() { if (colore.equals("rosso")) { colore = "verde"; } else if (colore.equals("verde")) { colore = "giallo"; } else if (colore.equals("giallo")) { colore = "rosso"; } } } public class Programma { 79 public static void main (String[] args) { Semaforo sem = new Semaforo(); sem.setColore("rosso"); // questo è l'unico modo // possibile per // inizializzare l'attributo // colore! sem.cambiaColore(); System.out.println(sem.getColore()); // questo è // l'unico modo possibile per // ottenere il valore // dell'attributo colore! } } Si noti che, con questa nuova versione del programma: • Nessun metodo della classe Programma può più accedere direttamente l'attributo colore. In altri termini, scrivere cose tipo: sem.colore = ... ; adesso è vietato. L'unico modo di leggere o di modificare il valore dell'attributo colore è tramite i metodi pubblici getColore e setColore. • Se il programmatore che implementa la classe Programma tenta di inizializzare l'attributo colore usando una stringa diversa da "rosso", "giallo" o "verde", ad esempio scrivendo: sem.setColore ("blu"); il metodo setColore termina segnalando un errore. Uno dei grossi vantaggi di usare metodi pubblici "set" e "get" per modificare e leggere il valore di un attributo è che essi consentono di scrivere al loro interno dei controlli, e ciò rende il codice "più sicuro". 80 Parole chiave static e final Si supponga di voler scrivere una classe che raccolga tutte le informazioni necessarie per descrivere una squadra di calcio. Si supponga, ad esempio, che, ai fini del nostro programma, quelle informazioni debbano essere: • • • • Il nome della squadra. Il nome dell'allenatore. Il nome del presidente. Il numero di giocatori. Risulterà subito evidente che c'è una grossa differenza tra alcuni di questi attributi e altri: ogni squadra, infatti ha (con poche eccezioni) un nome diverso da quello delle altre squadre e ogni squadra ha un allenatore ed un presidente diversi da quelli delle altre squadre. Per quanto riguarda il numero dei giocatori, invece (con questo attributo intendiamo il numero dei giocatori che scendono in campo all'inizio di una partita), è chiaro che esso deve necessariamente essere uguale a 11 per tutte le squadre. In altri termini, potremmo dire che attributi come il nome della squadra, il nome dell'allenatore e il nome del presidente sono "relativi a ogni singola istanza", mentre un attributo come il numero di giocatori è "relativo alla classe": dipende dal concetto stesso di squadra di calcio e deve sempre avere lo stesso valore per tutte le squadre di calcio. Per specificare che un attributo dipende dalla classe e non da una particolare istanza, Java mette a disposizione la parola chiave static. In generale: Un attributo o un metodo dichiarati con usando la parola chiave static dipendono dalla classe che li contiene e non da ogni singola istanza. Ecco un esempio di implementazione della classe SquadraDiCalcio: class SquadraDiCalcio { String String String ... static ... nome; allenatore; presidente; int numeroDiGiocatori = 11; SquadraDiCalcio (...) { ... } // costruttore } Adesso supponiamo che, per qualche motivo, un metodo di un'altra classe abbia bisogno di accedere l'attributo numeroDiGiocatori. Ecco come si fa: public class Esempio { public static void main (String[] args) { 81 SquadraDiCalcio team = new SquadraDiCalcio(...); int num = SquadraDiCalcio.numeroDiGiocatori; ... } } Come mostra questo esempio, un attributo static si accede usando alla sinistra del simbolo "." il nome della classe, e non il nome di una istanza (come potrebbe essere, ad esempio, team nel codice qua sopra). Ciò è del tutto ragionevole, dato che, per sua definizione, un attributo static dipende dalla classe e non dalle singole istanze. Supponiamo, infine, di voler imporre che il valore dell'attributo numeroDiGiocatori della classe SquadraDiCalcio non possa mai essere modificato, ma debba necessariamente rimanere uguale a 11 per tutta la durata del programma (è del tutto ragionevole, no?). Java mette a disposizione la parola chiave final per ottenere questo effetto. Se all'interno della classe SquadraDiCalcio, l'attributo numeroDiGiocatori fosse stato dichiarato come segue: static final int numeroDiGiocatori = 11; allora, oltre a dichiarare che questo attributo è statico, avremmo anche imposto che il suo valore non possa più essere modificato dopo la sua inizializzazione (pena: un errore a tempo di compilazione). In generale: ad una variabile dichiarata usando la parola chiave final si può assegnare un valore una ed una sola volta. Se, dopo che si è già assegnato un valore a questa variabile, si tenta di modificare quel valore, il compilatore di Java terminerà segnalando un errore. 82 Il metodo main Siamo finalmente in grado di capire il motivo per cui il metodo main va scritto in quel modo! Ecco (per l'ennesima volta!) come va scritto il metodo main: public static void main (String[] argomenti) { ... } ed eccone una spiegazione parola per parola: • public: il metodo main deve, ovviamente, poter essere chiamato anche esternamente rispetto alla classe in cui compare. In effetti, è sempre l'interprete di Java a far partire l'esecuzione del metodo main quando il programma viene eseguito. • static: c'è un solo metodo main, indipendentemente dall'istanza della classe che stiamo considerando. In altri termini, il metodo main dipende dalla classe e non da una particolare istanza. Questo semplifica enormemente il lavoro dell'interprete di Java (che deve chiamare il metodo main). Infatti, se il main non fosse statico, l'interprete dovrebbe prima creare una istanza della classe e poi invocare il main su tale istanza. Questo, oltre ad essere fortemente inefficiente, porrebbe anche una serie di altri problemi; ad esempio: quale costruttore utilizzerebbe l'interprete per creare l'istanza della classe? Come farebbe a sapere quali costruttori esistono nella classe che contiene il main? Se, invece, il metodo main è statico, allora questi problemi non esistono: l'interprete può chiamarlo semplicemente invocando: <nome della classe>.main (<lista dei parametri attuali>). • void: il metodo main non restituisce nessun risultato (a chi servirebbe questo risultato, visto che nessun altro metodo in un programma Java può chiamare il metodo main?). • main: il nome del metodo main è obbligatorio, come tutte le parole chiave e gli argomenti con cui esso deve essere definito. • argomenti: l'unico parametro di un metodo main è un vettore di stringhe. Questo vettore viene inizializzato automaticamente dall'interprete prima di mettere in esecuzione il programma con i dati digitati dall'utente al momento dell'esecuzione del comando java. Come già visto durante il corso, il tipo di ciascuno di questi argomenti può essere trasformato con appositi metodi messi a disposizione dal linguaggio Java. Si ricorda, infine, che il metodo main non può mai essere invocato all'interno di un programma Java: esso può essere eseguito solo ed unicamente dall'interprete! 83 Metodo equals e operatore == Si consideri il seguente programma Java: class Rettangolo { double base; double altezza; Rettangolo (double b, double h) { if (b <= 0 || h <= 0) { System.out.println("Errore: la base e l'altezza non possono essere negativi!"); System.exit(-1); } base = b; altezza = h; } double area () { return (base * altezza); } } public class Esempio { public static void main (String[] args) { Rettangolo r1 = new Rettangolo (10.4, 4.0); Rettangolo r2 = new Rettangolo (10.4, 4.0); System.out.println(r1 == r2); } } Contrariamente a ciò che si potrebbe immaginare, questo programma stampa a video false. Infatti l'operatore == applicato a due oggetti testa se essi occupano la stessa porzione di memoria (ovvero se sono a tutti gli effetti lo stesso oggetto). In questo caso, anche se r1 e r2 sono due rettangoli uguali, dato che hanno la stessa base e la stessa altezza, essi non sono chiaramente lo stesso oggetto: le due dichiarazioni di r1 e di r2 comportano l'allocazione di due diverse aree di memoria per questi due oggetti. Ogni classe Java possiede un metodo predefinito di nome equals, ma la versione di default di questo metodo contiene semplicemente un'invocazione all'operatore == (e quindi è identico all'operatore ==). Dunque, se vogliamo testare se due istanze di una certa classe sono uguali o meno (ad esempio se i loro attributi hanno gli stessi valori o meno), occorre riscrivere a mano il metodo equals. Ad esempio, la classe Rettangolo potrebbe contenere: public boolean equals (Rettangolo rett) { return ((this.base == rett.base) && (this.altezza == ret.altezza)); } 84 In questo caso, se il metodo main al posto della chiamata System.out.println(r1 == r2); contenesse la chiamata: System.out.println(r1.equals(r2)); il programma stamperebbe a video true. Note Conclusive In questo corso, il concetto di classe è stato affrontato in modo molto rapido e superficiale e molti concetti base della programmazione orientata agli oggetti e del linguaggio Java non sono stati affrontati. Tra essi, è importante ricordare, ad esempio: • • • ereditarietà, polimorfismo, libreria standard di Java. Gli studenti interessati e desiderosi di approfondire lo studio sono fortemente incoraggiati a consultare il libro di testo su questi argomenti e a confrontarsi con il docente in caso di mancata comprensione di alcuni argomenti o di dubbi di qualsiasi genere. 85