MokaByte 68 - 9mbre 2002 
Corso introduttivo su Java
VIII parte: i metodi parametrizzati
di
Andrea Gini
L'uso dei metodi può essere potenziato notevolmente con il ricorso ai parametri. Utilizzando i parametri, è possibile generalizzare i metodi, permettendo di estenderne l'uso a diversi contesti a seconda del valore dei parametri stessi. L'uso di metodi con parametri richiede una certa attenzione sia in fase di dichiarazione che in fase di chiamata. Inoltre, l'uso di metodi parametrizzati introduce nuove problematiche relativamente alla visibilità delle variabili.

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

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it