Dichiarazione
di metodo con parametri
Per dichiarare un metodo parametrizzato, è necessario
seguire una sintassi un po' diversa da quella vista
in precedenza: dopo il nome del metodo, tra una coppia
di parentesi tonde, bisogna specificare una serie di
coppie tipo-nome, in maniera simile a quanto si fa con
le dichiarazioni di variabile:
static
tipo nome(tipo1 nome1,tipo2 nome2, .... , tipoN nomeN)
{
istruzione1;
istruzione2;
....
instruzioneN;
}
Vediamo
un esempio pratico, per fissare meglio le idee. Proviamo
a definire un metodo min, che prenda in ingresso una
coppia di interi e restituisca il minore tra i due:
static
int min(int n1 , int n2) {
if ( n1 < n2 )
return n1
else
return n2;
}
Si
noti che i parametri n1 ed n2 vengono usati all'interno
del metodo come fossero normali variabili. Si noti anche
che, come sempre avviene comunemente nella programmazione,
è possibile trovare una formulazione equivalente
per lo stesso metodo:
static
int min(int n1 , int n2) {
return n1 < n2 ? n1 : n2;
}
La
possibilità di sostituire un metodo con un altro
più breve o più efficiente può
costituire una valida forma di ottimizzazione: ogni
qual volta noi troviamo una formulazione più
efficiente per un metodo, il miglioramento delle prestazioni
verrà notato ad ogni chiamata del metodo stesso.
D'altra parte si ricordi che è sempre preferibile
un metodo lievemente più lungo, anche se con
qualche inefficienza, piuttosto che un metodo molto
compatto ed efficiente ma scarsamente leggibile: infatti,
malgrado in teoria l'efficienza di un programma tenda
a variare in modo inversamente proporzionale al numero
delle righe di codice, è anche vero che molte
ottimizzazioni di questo tipo hanno poi uno scarsissimo
riscontro pratico, e portano a problemi di manutenzione
del codice che, alla lunga, si rivelano molto più
costosi dell'acquisto di un sistema più potente.
Chiamata di metodo con parametri
Il metodo min definito nel paragrafo precedente, può
ora essere richiamato, come ogni altro metodo, da qualunque
altra sezione del programma. La riga seguente:
m
= min( 10 , 5 );
assegna
alla variabile intera m il valore 5 (il minimo tra 5
e 10). L'aspetto più interessante dei metodi
parametrizzati è che è possibile fornire
come parametri delle variabili, come si vede nell'esempio
seguente:
int
a = 5;
int b = 10;
m = min(a,b);
L'uso
di variabili come veicolo per il passaggio di parametri
solleva immediatamente dei grossi interrogativi. E'
bene soffermarsi con calma ad analizzarli uno per uno.
Parametro attuale e parametro formale
L'uso di variabili come parametri solleva dei grossi
interrogativi: come viene trattata una variabile all'interno
di un metodo? Può un metodo modificare in modo
permanente una delle variabili passate come parametri?
Vediamo il seguente programma:
public
class parametri {
static void metodo(int i) {
i = 10;
}
static void main(String argv[]) {
int i = 5;
metodo(i);
System.out.println(i);
}
}
Si
osservi il metodo main e si cerchi di immaginare come
procederà l'esecuzione. Quale output ci si aspetta?
Qual'è la differenza tra la variabile i presente
nella chiamata e quella che compare nella dichiarazione?
Per poter chiarire queste differenze è necessario
imparare a distinguere tra di parametro formale e parametro
attuale, e tra passaggio di parametri by ref e by value.
Parametro formale
I parametri definiti nella firma di un metodo vengono
detti parametri formali. Tali parametri vengono trattati,
all'interno del metodo, come delle variabili, la cui
visibilità termina alla fine del metodo stesso.
Un parametro formale può avere lo stesso nome
di una variabile globale: come già spiegato nel
paragrafo sulla visibilità, per accedere alla
omonima variabile globale è necessario ricorrere
alla parola riservata this:
public
class Esempio {
int i; // variabile globale
static void metodo(int i) { // parametro
formale
System.out.println(i); // utilizzo
della variabile globale
System.out.println(this.i);
// utilizzo del parametro formale
}
}
Parametro attuale
Se
si esegue una chiamata a metodo passando una variabile
come parametro, tale variabile viene detta parametro
attuale. Si noti che il compilatore richiede che le
variabili fornite come parametri attuali siano dello
stesso tipo di quelle richieste dai parametri formali
presenti nella firma del metodo, nel rispetto delle
regole di casting. Pertanto, se un metodo richiede un
parametro long, non verranno segnalati errori se si
effettua una chiamata con un int. Nel caso contrario
(parametro formale int, parametro attuale long), il
compilatore segnalerà un errore.
public
class Esempio {
static void metodo(int i, float f , boolean
b ) { // i, f e b sono parametri
formali
// corpo del metodo
}
public static void main(String argv[] )
{
int a = 10;
float b = 10.5F;
Boolean c = true;
metodo(a,b,c); // a, b e c sono
parametri attuali
}
}
Passaggio
di parametri by value e by ref
All'atto di chiamare un metodo, l'interprete Java copia
il valore dei parametri attuali nelle variabili corrispondenti
ai rispettivi parametri formali. Tale comportamento
viene detto "Passaggio di parametri by value",
o "per valore". Attraverso il passaggio di
parametri by value, siamo sicuri che qualunque modifica
ai parametri formali non andrà ad alterare il
contenuto delle variabili usate come parametri attuali.
Proviamo ora a riprendere in mano il programma visto
in un precedente paragrafo:
public
class Parametri {
static void metodo(int i) {
i = 10;
}
static
void main(String argv[]) {
int i = 5;
metodo(i);
System.out.println(i);
}
}
Ora
sappiamo che esso darà come output 5, ossia il
valore della variabile i utilizzata come parametro attuale
nella chiamata metodo(i) presente nel metodo main. D'altra
parte, le modifiche alla variabile i sottostante al
parametro formale visibile nella chiamata di metodo
static void metodo(int i) non si rifletteranno nella
omonima variabile i presente nel metodo main stesso.
Come
si comporta un metodo quando come parametro viene utilizzato
un array? Proviamo a riscrivere il precedente esempio,
utilizzando un vettore come parametro:
public
class Parametri2 {
static void metodo(int[] i) {
i[0] = 10;
}
static void main(String argv[]) {
int[] i = new int[10];
i[0] = 5;
metodo(i);
System.out.println(i[0]);
}
}
Quale
output dobbiamo aspettarci? A differenza del programma
precedente, questo esempio restituisce in output il
valore 10, quello assegnato al primo elemento dell'array
all'interno del metodo. Cosa è successo? Al contrario
di quanto avviene con le variabili dei tipi primitivi
(int, long ecc), quando si passa ad un metodo un array
(o, come vedremo, qualunque altro oggetto), esso viene
passato "by ref", o "per riferimento":
in altre parole il parametro formale i presente nel
metodo non punta ad una copia dell'array definito nel
metodo main, ma punta allo stesso identico array, e
qualunque modifica effettuata su di esso all'interno
del metodo andrà ad agire direttamente sull'array
stesso.
Alcuni
linguaggi, come il C, il C++, il Pascal e il Visual
Basic, permettono di specificare per ogni singolo parametro
se si desidera che venga passato per riferimento o per
valore. Questa possibilità, sebbene utile in
teoria, finisce inevitabilmente per causare confusione.
Per questo motivo, durante la progettazione di Java
si è deciso di imporre per default il passaggio
by value per tutti i tipi primitivi e il passaggio by
ref di array ed oggetti.
Ricorsione
Ogni metodo può chiamare altri metodi. Cosa succede
nel caso limite in cui un metodo richiama sé
stesso? In casi come questo otteniamo un'esecuzione
ricorsiva. Per capire l'uso e l'utlilità della
ricorsione, vediamo un esempio divenuto ormai classico:
la funzione fattoriale.
La funzione fattoriale viene tipicamente definita per
ricorsione, dicendo che il fattoriale di 0 è
1 e che il fattoriale di un qualunque numero n intero
è pari ad n moltiplicato per il fattoriale del
numero n - 1. Pertanto il fattoriale di 0 è 1,
il fattoriale di 3 è 6 (3*2*1), il fattoriale
di 4 è 24 (1*2*3*4) e così via. Grazie
alla ricorsione possiamo realizzare un metodo che calcola
il fattoriale applicando esattamente la definizione
standard:
static
int fattoriale(int n) {
if(n=0)
return 1;
else
return n * fattoriale(n - 1);
}
Per
eseguire correttamente le procedure ricorsive, l'interprete
Java ricorre una speciale memoria a pila (Stack in inglese),
che ha la particolarità di comportarsi come la
pila di piatti di un ristorante: il primo piatto che
viene preso (tipicamente quello in altro) è anche
l'ultimo che vi era stato posto. A causa di questo particolare
comportamento, si dice che una pila è una struttura
dati di tipo LIFO, acronimo inglese che si traduce con
"Last In First Out", ossia "l'ultimo
ad entrare è il primo ad uscire".
La ricorsione permette di fornire una soluzione elegante
ad una serie di problemi, ma proprio a causa del ricorso
allo Stack, si paga questa eleganza con una penalità,
spesso non grave, sui tempi di esecuzione. Si noti che
qualunque funzione ricorsiva può essere sostituita
da una equivalente funzione iterativa: vediamo ad esempio
una versione iterativa del metodo fattoriale:
static
int fattoriale(int n) {
f = 1;
while(n>0) {
f = f * n;
n = n--;
}
return f;
}
In
questo caso le due formulazioni presentano una complessità
di lettura molto simile, mentre in altri casi la soluzione
ricorsiva risulta decisamente più elegante di
quella iterativa. Ancora una volta si raccomanda al
programmatore di preferire sempre la chiarezza all'efficienza,
specie in virtù del fatto che i moderni compilatori
sono in grado di ottimizzare automaticamente i casi
più comuni di ricorsione, al punto da rendere
superfluo qualsiasi sforzo di ottimizzazione manuale
del codice.
Conclusioni
Questo mese abbiamo analizzato in profondità
l'uso dei metodi con parametro. Le problematiche che
comunemente creano dei dubbi al momento dell'uso di
simili parametri sono state analizzate in profondità
una per una. Il mese prossimo faremo una veloce panoramica
sulla programmazione strutturata, un passo necessario,
specie per chi si avvicina alla programmazione per la
prima volta, prima di introdurre la programmazione Object
Oriented caratteristica del linguaggio Java.
Risorse
Scarica
qui i sorgenti presentati nell'articolo
|