Programmazione e Linguaggio Java

Transcript

Programmazione e Linguaggio Java
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