Corso di Java per la 3 E Sia - 8a Lezione 20 febbraio 2016 ore 11,00

Corso di Java per la 3 E Sia - 
8a Lezione 20 febbraio 2016 ore 11,00

Procedure e funzioni

Istruzioni che si ripetono
Funzioni e procedure consentono di scrivere una sola volta delle istruzioni che normalmente andrebbero ripetute più volte nel programma. Un esempio è la valutazione della funzione di cui si vuole trovare un intervallo con uno zero nel programma Nullo.java. Nel programma ci sono ben cinque istruzioni che calcolano la stessa identica funzione x2-5x-2. Sarebbe comodo poter scrivere questa funzione una volta sola all'inizio del programma: a parte la semplificazione del lavoro di scrittura, questo riduce la probabilità di commettere errori nella scrittura della funzione, e permette di modificare più facilmente il programma se si vuole analizzare una funzione diversa.
Usare funzioni sul programma di ricerca degli zeri è piuttosto complicato da spiegare, dal momento che si utilizzano già tutte le caratteristiche delle funzioni. Per questa ragione, cominciamo con esempî più facili, e rimandiamo la modifica di Nullo.java a un momento successivo.



Supponiamo di voler modificare il programma Espressioni.java in modo che i risultati delle espressioni siano separati fra di loro da una linea composta di asterischi, preceduta e seguita da linee vuote. In altre parole, vogliamo una cosa del tipo:
Risultato della prima espressione: 1.8371900826446281

***********************************************************

Risultato della seconda espressione: 34.460000000000001

***********************************************************

Risultato della terza espressione: 0.01484119916889285

***********************************************************

Risultato della quarta espressione: 7.0408025110778389
Per stampare una linea vuota basta dare un comando del tipo System.out.println(" "), che corrisponde a stampare una linea in cui c'è solo uno spazio (la linea appare quindi vuota). Con questo trucco, è possibile scrivere il programma EspressioniSeparate.java che risolve il problema.
/*
  Alcuni esempi di espressioni
*/

class EspressioniSeparate {
  public static void main(String[] args) {
    double a=12.23,b=0.1e+2,c;

    c=(a+b)/12.1;
    System.out.println("Risultato della prima espressione: "+c);

    System.out.println(" ");
    System.out.println("***********************************************************");
    System.out.println(" ");

    c=a*2+b;
    System.out.println("Risultato della seconda espressione: "+c);

    System.out.println(" ");
    System.out.println("***********************************************************");
    System.out.println(" ");

    c=1/(a-1)/(b-4);
    System.out.println("Risultato della terza espressione: "+c);

    System.out.println(" ");
    System.out.println("***********************************************************");
    System.out.println(" ");

    c=Math.sqrt(a*a - b*b);
    System.out.println("Risultato della quarta espressione: "+c);
  }
}

Il programma funziona perfettamente. D'altra parte, è stato necessario riscrivere tre volte la stessa sequenza di istruzioni:
    System.out.println(" ");
    System.out.println("***********************************************************");
    System.out.println(" ");
Sarebbe comodo poter evitare di ripetere la scrittura di queste tre istruzioni ogni volta. Le procedure servono a questo: scrivere una sequenza di istruzioni una volta sola, anche se poi vanno ripetute più volte nel programma. In particolare, una procedura è un nome che viene assegnato a una sequenza di istruzioni. In questo modo, quando nel programma è necessario eseguire quelle istruzioni, si può utilizzare il nome invece di scrivere le istruzioni per esteso.
Per fare questo è necessario:
  1. dare un nome a una sequenza di istruzioni;
  2. scrivere il nome al posto della sequenza nei punti del programma in cui vanno eseguite le istruzioni.
Il passo 1, chiè la definizione di un nome a una sequenza di istruzioni, si chiama dichiarazione di procedura. Il formato è il seguente:
static void nome() {
  istruzione1;
  istruzione2;
  ...
}
Le istruzioni fra parentesi graffe sono quelle che si vuole ripetere più volte all'interno del programma. A questa sequenza viene assegnato il nome dato. Il nome serve a permettere di richiamarequeste istruzioni: in altre parole, si usa ogni volta che è necessario eseguire la sequenza di istruzioni.
Le procedure vanno messa prima della line public stati void main.... Ogni volta che si vogliono eseguire le istruzioni che si trovano in una certa procedura, si scrive nome();. Scrivere il nome (che equivale a eseguire le istruzioni associate al nome) si dice chiamata di procedura.
Il programma che stampa il risultato di espressioni si può quindi riscrivere come segue: EspressioniSeparateProc.java
/*
  Alcuni esempi di espressioni
*/

class EspressioniSeparateProc {
  static void separa() {
    System.out.println(" ");
    System.out.println("***********************************************************");
    System.out.println(" ");
  }

  public static void main(String[] args) {
    double a=12.23,b=0.1e+2,c;

    c=(a+b)/12.1;
    System.out.println("Risultato della prima espressione: "+c);

    separa();

    c=a*2+b;
    System.out.println("Risultato della seconda espressione: "+c);

    separa();

    c=1/(a-1)/(b-4);
    System.out.println("Risultato della terza espressione: "+c);

    separa();

    c=Math.sqrt(a*a - b*b);
    System.out.println("Risultato della quarta espressione: "+c);
  }
}

Spiegazione riassuntiva: occorre ripetere una sequenza di tre istruzioni diverse volte nel codice; è quindi vantaggioso usare una procedura in cui si scrivono queste istruzioni una volta sola. La prima cosa da fare è scrivere la procedura stessa, ossia si scrive static void separa(), in cui ``separa'' è il nome che si è scelto per la procedura, seguito dalle tre istruzioni racchiuse da parentesi graffe. A questo punto esiste una procedura che si chiama separa(). Ogni volta che nel resto del programma appare la istruzione separa(), questo è equivalente ad eseguire le istruzioni che sono state messe nella procedura.

Variabili e procedure

A prima vista, può sembrare che le procedure siano semplicemente un modo per raccogliere insieme una sequenza di istruzioni, in modo da avere un nome per questa sequenza. Le cose non stanno esattamente cosí. La principale differenza è che all'interno di una procedura non si possono usare le variabili che sono dichiarate all'interno del programma principale. In altre parole, il seguente programma non viene compilato:
class Zero {
  static annulla() {
    a=0;
  }

  public static void main(String[] args) {
    int a;

    anulla();
    System.out.println(a);
  }
}
A prima vista, potrebbe sembrare che scrivere a=0 nel programma oppure annulla() sia esattamente la stessa cosa. Il problema è che le variabili del programma non possono venire usate in nessun modo nelle istruzioni all'interno della procedura.
D'altra parte, è possibile dichiarare ed usare delle variabili all'interno della procedura. Queste variabili non saranno però visibili al programma principale, ossia non potranno venire usate dalle istruzioni del programma principale. Queste variabili sono dette variabili locali della procedura.
Supponiamo per esempio di voler realizzare una procedura che stampa venti linee bianche. Possiamo ripetere la stampa di una linea bianca per venti volte, usando un ciclo for. Per poter usare il ciclo for occorre una variabile di tipo intero che vada da 1 a 20. Non possiamo dichiarare questa variabile nel programma principale (cioè dopo il public static void main...) perché in questo modo la procedura non potrebbe usarla. È quindi necessario dichiarare questa variabile all'interno della procedura, nel modo seguente:
class LineeBianche {
  static void diecilinee() {
    int i;

    for(i=1; i<=20; i=i+1) {
      System.out.println(" ");
    }
  }

  public static void main(String[] args) {
    ....

    diecilinee();

    ...

  }
}
È possibile avere più procedure, oltre al programma principale. In oguna di queste procedure si possono definire delle variabili. La regola in questo caso è
le variabili definite all'interno di una procedura si possono usare solo dentro la procedura stessa
Prendiamo in considerazione il seguente programma StessoNome.java:
/*
  Due variabili con lo stesso nome: una nella procedura
  e una nel programma.
*/

class StessoNome {
  static void esempio() {
    int a;

    a=12;
  }

  public static void main(String[] args) {
    int a;

    a=90;

    esempio();

    System.out.println(a);
  }
}

Si analizzi il programma e si provi a capire che cosa viene stampato. A prima vista può sembrare che si stampa il valore 12: infatti, la variabile a viene assegnata a 90, poi si chiama la procedura che ci mette il valore 12, e poi si stampa il contenuto. In realtà, il programma stampa il numero 90.
Per capire il perchè, occorre capire come viene organizzata la memoria. La situazione che si viene a creare nella memoria è la seguente:
esempio
a
    
      
 
 
main
a
    
      
 
A parole: la procedura esempio ha una sua zona di memoria, che è un contenitore in cui vengono messe le variabili locali alla procedura. La variabile a dichiarata all'intero di esempio è una casella all'interno di questo contenitore.
D'altra parte, il programma principale (main) ha una sua zona di memoria, che è il contenitore in cui vengono create le sue variabili Quindi, la variabile a dichiarata all'interno di main è una casella all'interno di questa zona.
È quindi chiaro che la due variabili, anche se hanno lo stesso nome, corrispondono a due zone di memoria diverse. È quindi possibile che il contenuto sia diverso; più precisamente, mettendo un valore in una delle due variabili, l'altra rimane inalterata.
Vediamo ora come viene creata questa situazione, ossia eseguiamo il programma passo per passo. Inizialmente, la memoria è vuota.
esempio
 
    
      
 
 
main
 
    
      
 
Per prima cosa viene creata la variabile a del programma:
esempio
 
    
      
 
 
main
a
    
      
 
In questa variabile viene messo il valore 90:
esempio
 
    
      
 
 
main
a
  90
      
 
A questo punto si chiama la procedura esempio. Questa procedura crea una sua variabile locale (nel suo contenitore/zona di memoria) che ha ancora nome a. Quindi la situazione che si ha a questo punto è la seguente:
esempio
a
    
      
 
 
main
a
  90
      
 
La procedura esempio contiene una istruzione a=12. Questo significa: metti il valore 12 nella variabile a. Abbiamo però due variabili a in questo momento. Dal momento che questa istruzione si trova nella procedura esempio, la variabile a è quella della zona di memoria di memoria di esempio, che nel grafico è il contenitore di sopra. Quindi a=12 mette il valore 12 nella variabile a locale della procedura esempio:
esempio
a
  12
      
 
 
main
a
  90
      
 
A questo punto la procedura termina e si ritorna ad eseguire il programma principale dal punto in cui lo si era lasciato. In questo caso, l'istruzione subito dopo la chiamata di procedura è System.out.println(a), che stampa il valore della variabile a. Anche in questo caso abbiamo il dubbio di quale sia la variabile a, se quella del programma o quella della procedura esempio. Dal momento che la istruzione di stampa si trova all'interno del programma, la variabile da usare è quella del programma, ossia quella che attualmente contiene il valore 90. Il risultato dell'esecuzione di questo programma è quindi la stampa del numero 90.

Procedure con argomenti

Le procedure viste nelle pagine precedenti erano semplicemente un modo per scrivere una volta sola una sequenza di istruzioni che andava eseguita in più punti del programma. In più esisteva la limitazione che le variabili del programma non potevano venire usate dalla procedura. Ci sono dei casi in cui questa limitazione è un problema. Si pensi al caso in cui, in più punti del programma, occorre stampare un certo numero di linee bianche. Il caso in cui occorre stampare sempre lo stesso numero di linee (per esempio, 20) è stato già analizzato nella pagina sulle variabili e procedure. Consideriamo ora il caso in cui in un certo punto del programma occorre stampare 10 linee bianche, in un altro 20, in un altro 2, ecc. Chiaramente il programma già visto non va bene, dal momento che la procedura lineebianche() stampa sempre lo stesso numero di linee bianche (20).
Se le variabili del programma principali fossero visibili (=utilizzabili) nella procedura, sarebbe facile: la procedura dovrebbe effettuare un ciclo partendo da 1 fino al valore di una certa variabile che rappresenta il numero di linee da stampare. In altre parole si potrebbe (ma non si può) fare cosí:
class Linee {
  static void lineebianche() {  /* NON FUNZIONA */
    int i;

    for(i=1; i<=lineedalasciare; i=i+1) {
      System.out.println(" ");
    }
  }

  public static void main(String[] args) {
    int lineedalasciare;

    /* stampa 10 linee */
    lineedalasciare=10;
    lineebianche();

    ...

    /* stampa 20 linee */
    lineedalasciare=20;
    lineebianche();
  }
}
Questo funzionerebbe se la procedura potesse utilizzare le variabili del programma. Sfortunatamente questo non è possibile. Esiste però un meccanismo che consente ad una procedura di ricevere dei dati dal programma. Usando questo metodo, il programma può ``comunicare'' alla procedura il numero di linee bianche che vanno lasciate. Questo meccanismo si chiama passaggio dei parametri.
Si può pensare al passaggio dei parametri come a un tipo di ``trasmissione'', in cui il programma trasmette dei dati alla procedura (nell'esempio quante linee bianche stampare); la procedura riceve questi dati e li può usare.
Per poter effettuare questa trasmissione, è necessario che la procedura sia definita in modo tale da poter ricevere questi parametri. La definizione della procedura viene modificata in questo modo: dopo il nome, viene aggiunta la lista dei dati che la procedura può ricevere:
static void nome(lista dei parametri)
I parametri sono appunto i dati che il programma trasmette alla procedura. La lista dei parametri deve contenere il tipo di questi dati. In altre parole, per ogni dato che la procedura può ricevere occorre specificare quale è il suo tipo, se intdouble, ecc. Oltre al tipo, per ogni parametro è necessario utilizzare un nome, in modo tale che la procedura possa poi utilizzare il dato ricevuto come se fosse una qualsiasi variabile.
Queste cose sono più facili da far vedere su un esempio che da spiegare. Nel caso della procedura che stampa un certo numero di linee bianche, il programma trasmette un unico dato, che è il numero di linee da stampare. Logicamente, la procedura riceve solo questo dato, quindi nella lista dei parametri c'è un unico parametro, che è di tipo intero. La definizione della procedura viene quindi modificata come segue:
  static void lineebianche(int ...) {
    int i;

    for(i=1; i<=lineedalasciare; i=i+1) {
      System.out.println(" ");
    }
  }
A questo punto, occorre un modo per far sì cha la procedura possa usare questo numero. Si ricordi che la procedura è una unità indipendente dal programma, quindi non ``sa'' come è fatto il programma che la usa. Il meccanismo che si usa è quello di associare una variabile al dato che è stato passato. In altre parole, si specifica che il dato che il programma ha passato va messo in una variabile di cui diamo noi il nome. In questo modo, la procedura può accedere al dato che è stato trasmesso dal programma semplicemente usando la variabile. La definizione della procedura, oltre al tipo di ogni dato che viene passato, specifica un nome di variabile, che è il posto dove il dato ricevuto viene memorizzato.
  static void lineebianche(int l) {
    int i;

    for(i=1; i<=l; i=i+1) {
      System.out.println(" ");
    }
  }
Nelle istruzioni che compongono la procedura, l si comporta esattamente come una qualsiasi variabile. Il punto importante è che all'inizio in questa variabile viene memorizzato il dato che il programma ha trasmesso.
Manca ora da specificare in che modo il programma trasmette i parametri alle procedure. Nel caso di procedure senza parametri, per indicare che si voleva la esecuzione di una procedura, si usava la istruzione nome(), in cui ``nome'' è il nome della procedura. Nel caso in cui il programma vuole passare dei parametri, questi vanno semplicemente messi, l'uno dietro l'altro e separati da virgole, fra le due parentesi tonde: nome(dato1, dato2, dato3,...); Nel caso della stampa di linee bianche, il programma finale è fatto come segue.
class Linee {
  static void lineebianche(int l) {
    int i;

    for(i=1; i<=l; i=i+1) {
      System.out.println(" ");
    }
  }

  public static void main(String[] args) {

    /* stampa 10 linee */
    lineebianche(10);

    ...

    /* stampa 20 linee */
    lineebianche(20);
  }
}
Cosa succede quando il programma viene eseguito? La prima istruzione è lineebianche(10); La esecuzione di questa istruzione comporta che:
  1. viene chiamata la procedura lineebianche
  2. a questa procedura viene mandato il dato intero 10.
La procedura ha un parametro di tipo intero: può ricevere un dato intero, e questo viene memorizzato nella variabile l:
  1. si crea la variabile intera l, e ci si mette 10
  2. si eseguono le istruzioni di lineebianche.
Viene quindi creata una variabile di tipo intero l, in cui viene messo il valore 10. A questo punto, si esegue il resto della procedura come al solito: si crea la variabile i e si esegue il ciclo.
La successiva istruzione è lineebianche(20). Qui avviene tutto come prima: il programma passa il valore 20 alla procedura, la quale lo riceve e lo memorizza nella variabile l, al che vengono eseguite le altre istruzioni della procedura.
Graficamente, la situazione è la seguente: inizialmente abbiamo un contenitore (zona di memoria) per il programma principale (main) e un contenitore per la procedura lineebianche. Inizialmente, non sono definite variabili, quindi abbiamo:
lineebianche
 
    
    
 
    
 
 
 
main
 
    
      
 
Viene chiamata lineebianche(10). Questo corrisponde a: primo, si crea una variabile l che è una variabile locale a lineebianche, in cui viene messo il valore passato, cioè 10; secondo, si eseguono le istruzioni di lineebianche.
lineebianche
l
  10
    
    
 
 
 
main
 
    
      
 
La procedura lineebianche contiene una dichiarazione di un'altra variabile intera i che è sempre locale alla procedura. Si ha quindi la situazione seguente.
lineebianche
l
  10
    
i
    
 
 
 
main
 
    
      
 
La procedura contiene anche un ciclo in cui i assume tutti i valori interi da 1 al valore attualmente memorizzato in l. All'interno del ciclo si stampa una linea bianca. Quindi, alla fine verranno stampate tante linee bianche quanto è il valore di l. Questo è esattamente quello che si voleva ottenere.
Le pagine successive mostrano esempi di procedure che ricevono argomenti: stampa di valori solo se positivi e la stampa del grafico di una funzione con istruzioni di testo.

Stampa i valori positivi

Vediamo ora una funzione leggermente più complicata delle precedenti. Supponiamo di avere due variabili intere a e b, a cui assegnamo dei valori. Vogliamo poi calcolare e stampare il valore di alcune espressioni, per esempio a-ba-b*b-a/b e a/b. Vogliamo però stampare solo il valore delle espressioni positive (maggiore oppure uguale a zero); in caso contrario si vuole stampare solo la scritta Valore negativo, senza precisare il valore esatto dell'espressione.
Questo tipo di problema si può risolvere anche senza usare le procedure. Per esempio, per stampare il valore della prima espressione, si potrebbe fare:
  int c;

  c=a-b;

  if( c>=0 ) {
    System.out.println(c);
  }
  else {
    System.out.println("Valore negativo");
  }
Questo andrebbe poi ripetuto anche per le altre espressioni da valutare. Il programma completo contiene quindi quattro volte la intera istruzione condizionale, esattamente identica. Si tratta quindi di una situazione in cui una sequenza di istruzioni si ripete più volte in un programma, per cui conviene ``mettere a fattor comune'' e usare una procedura.
In questo caso, la procedura dovrebbe ricevere un valore intero (il valore di una delle espressioni); questo valore viene stampato oppure no a seconda se è positivo oppure no. Dal momento che la procedura riceve un valore intero, occorre specificare un nome di variabile intero in cui il dato che il programma manda viene memorizzato. L'inizio della procedura quindi è:
  static void stampa(int e)
Si è scelto il nome stampa per questa procedura. Le istruzioni associate devono stampare il valore memorizzato in e, ma soltanto se è positivo. In caso contrario, si stampa Valore negativo. La procedura completa è quindi come segue:
  static void stampa(int e) {
    if( e>=0 ) {
      System.out.println(e);
    }
    else {
      System.out.println("Valore negativo");
    }
  }
Per poter eseguire le istruzioni della procedura è necessario che all'interno del programma principale ci siano delle chiamate di procedura, ossia delle istruzioni stampa(...). Fra parentesi ci va il valore che il programma manda alla procedura. È possibile mettere tra parentesi un numero intero (per esempio 12), una variabile (per esempio a), o anche una espressione. Nel nostro caso, dato che vogliamo stampare il valore di una espressione come a-b possiamo semplicemente scrivere stampa(a-b). Questo equivale a calcolare il valore della espressione a-b, mettere il risultato nella variabile e locale della procedura stampa, ed eseguire le istruzioni della procedura. Questo è esattamente quello che serve (calcolare il valore della espressione e stamparlo se positivo).
La stessa cosa va fatta per le altre espressioni da stampare. Il programma completo StampaSePositivo.java è riportato qui sotto.
/*
  Stampa il valore di alcune espressioni, ma solo
  se e' positivo.
*/

class StampaSePositivo {

  static void stampa(int e) {
    if( e>=0 ) {
      System.out.println(e);
    }
    else {
      System.out.println("Valore negativo");
    }
  }

  public static void main(String[] args) {
    int a=12, b=4;

    stampa(a-b);
    stampa(a-b*b);
    stampa(-a/b);
    stampa(a/b);
  }
}

Grafico di una funzione

Vediamo qui un esempio di procedura: la stampa del grafico di una funzione usando solo caratteri di testo. Sia quindi data una funzione f=|x|/20.0+20sin(x/10)+30 da visualizzare. Vogliamo visualizzare graficamente i valori di questa funzione con x che va da -100 a +100.
Per effettuare questo grafico usando esclusivamente la finestra di testo, usiamo il trucco di visualizzare il grafico ``in verticale'': per ogni valore di x stampiamo una linea, in cui mettiamo un asterisco "*" in posizione tanto più a destra in funzione del valore f(x).
Per stampare l'asterisco a destra, usiamo la funzione System.out.print, che è simile alla println, ma non va a capo una volta stampato. Per stampare l'asterisco a destra, facciamo prima una stampa di tanti spazi quanto vale la funzione, e poi stampiamo ``*" con andata a capo.
Il programma GraficoFunzione.java realizza questa funzione.
/*
  Grafico di una funzione usando solo testo
  (con funzione)
*/

class GraficoFunzione {
  static void linea(int n) {
    int i;

    for(i=1; i<=n; i=i+1) {
      System.out.print(" ");
    }
    System.out.println("*");
  }

  public static void main(String[] args) {
    double f;
    int x;

    for(x=-100; x<=100; x=x+1) {
      f=Math.abs(x)/20.0+20*Math.sin(x/10.0)+30;
      linea(Math.round((float) f));
    }
  }
}

La funzione linea ha un argomento di tipo intero, ossia si aspetta di ricevere un valore di tipo intero. Questo valore viene memorizzato nella variabile n. Usando il valore di questa variabile, è facile stampare la linea voluta: si fa un ciclo for in cui si stampano n spazi bianchi (senza andare a capo), e poi si stampa un singolo asterisco ``*'', andando a capo. La funzione linee stampa quindi una singola linea del grafico.
Per stampare tutto il grafico, occorre chiamare la funzione per ogni valore di x da -100 a +100. Il programma fa esattamente questo: c'è un ciclo for che assegna alla variabile x valori progressivi da -100 a +100. Per ogni valore di x si valuta la funzione, e si assegna il valore risultato alla variabile f. A questo punto, si chiama la funzione usando il valore memorizzato in f.
Un punto da notare nel programma è la presenza della funzione di arrotondamento Math.round. È necessario usare questa funzione perchè il risultato della funzione è un numero reale. Quindi, non si può passare alla procedura linea, che si aspetta un numero intero. La espressione Math.round((float) f) rappresenta un numero intero che è la più vicina approssimazione del numero reale contenuto nella variabile f.

Funzioni che ritornano un valore

Fino ad ora abbiamo visto due tipi di procedure: quelle che eseguono una sequenza di istruzioni senza ricevere dati dal programma, e quelle che ricevono dati dal programma e li usano per fare qualcosa. Manca ora l'ultima parte, ossia: fare in modo che la procedura possa mandare dei dati al programma principale.
Questo è utile per esempio se il programma contiene molte volte la stessa espressione. Consideriamo il programma che trova un intervallo in cui la funzione contiene uno zero Nullo.java. In questo programma, si ripete per cinque volte una istruzione in cui si valuta il valore della funzione per valori diversi dell'argomento. Farebbe comodo poter scrivere la funzione una volta sola. Con le cose che sono state viste fino ad ora, il calcolo non è difficile da inserire in una procedura:
  static void f( double x ) {
    double risultato;

    risultato=x*x-5*x-2;
  }
Questa procedura crea una variabile reale x, in cui viene messo il dato che la procedura principale manda; viene poi creata un'altra variabile reale risultato. Si valuta la espressione (usando il valore della variabile x, che è quello passato dalla variabile principale) e la si mette in risultato. Da un certo punto di vista, siamo effettivamente riusciti a scrivere l'espressione una volta sola, soltanto che ancora ci manca il modo in cui la procedura rimanda il valore del risultato al programma.

Come si scrive una procedura che manda risultati al programma

Si fa in due passi. Per prima cosa, occorre dire quale è il tipo del risultato, ossia se la procedura vuole inviare al programma un intero oppure un reale, ecc. Per fare questo si modifica la prima linea della procedura, come segue:
  static double f( double x ) {
    ...
  }
La parole void è stata rimpiazzata dalla parola double. Questo specifica il tipo di dato che la procedura rimanda al programma principale. Si spiega ora il piccolo mistero dalla parola void usata nelle procedure che non tornano valori al programma: dichiara che il tipo di dati che la procedura rimanda al programma è vuoto, ossia la procedura non rimanda niente.
Il secondo passo è quello di specificare esattamente che cosa la procedura restituisce al programma. Per fare questo si usa la istruzione:
return espressione
Questa istruzione ha due effetti: la prima è che l'esecuzione della procedura termina; la seconda è che la espressione viene valutata, e il suo valore viene mandato indietro al programma.
La procedura di calcolo della funzione va quindi modificata in maniera tale che il contenuto della variabile risultato venga rimandato al programma principale. Per fare questo è sufficiente aggiungere come ultima istruzione della procedura il return:
  static double f( double x ) {
    double risultato;

    risultato=x*x-5*x-2;

    return risultato;
  }
Si noti che quello che segue la parola return può essere sia una variabile che una espressione complessa. Quindi, la procedura si può semplificare mettendo l'espressione da calcolare direttamente dopo la parola return, rendendo inutile la variabile risultato:
  static double f( double x ) {
    return x*x-5*x-2;
  }

Come fa il programma a usare il valore che la procedura ritorna

Questo è l'ultimo passo: abbiamo visto come si fa a specificare che una procedura calcola valori da rimandare al programma; ora dobbiamo dire come si fa a usare questi valori nel programma.
Nel caso in cui una procedura nomeproc non mandava dati al programma, per far eseguire le sue istruzioni si usava una singola istruzione nomeproc(....) in cui si mettevano fra parentesi i dati che il programma mandava. Nel caso in cui la procedura ritorna un valore, si usa invece:
     variabile = nomeproc(...);
Questa istruzione significa: esegui la procedura mandando i dati fra parentesi; il risultato che la procedura manda mettilo nella variabile.
Più in generale, se una procedura f(...) ritorna un dato di un certo tipo, allora si può scrivere f(...) in qualsiasi punto in cui è possibile usare una variabile di quel tipo. Per esempio, se vogliamo assegnare a x la media fra il risultato di f e 5, possiamo anche scrivere: x=(f(...)+5)/2;. In altre parole, se una espressione contiene una o più procedure, queste vengono eseguite, e al loro posto ci si mette il valore che la procedura ha rimandato al programma.
Nota: le procedure che ritornano un valore vengono chiamate funzioni.
Il programma NulloFunzione.java trova un intervallo piccolo in cui la funzione ha uno zero, e questo viene fatto usando una funzione (procedura che ritorna un valore).
/*
  Trova un punto in cui una funzione f(x) ha un
  valore sufficientemente vicino allo zero.
  Siano a e b due valori tali che f(a) ha segno
  opposto a f(b). Si assume che la funzione sia
  continua.
  Variante con chiamata a funzione.
*/

class NulloFunzione {

    /* la funzione di cui si vuole trovare lo zero */

  static double f( double x ) {
    return x*x-5*x-2;
  }


    /* la procedura principale */

  public static void main(String[] args) {
    double a=0,b=10;
    double e=0.01;
    double x=a;

    if( f(a)*f(b) >0 ) {
      System.out.println("La funzione non ha segno diverso negli estremi");
      System.out.println(f(a)+" "+f(b));
      return;
    }

    while( Math.abs(f(x))>e ) {
      x=(a+b)/2;
      System.out.println(a+" "+x+" "+b);

      if( f(x)*f(a) > 0 ) {
        a=x;
      }
      else {
        b=x; 
      }
    }

    System.out.println("Trovato valore "+f(x)+" per x pari a "+x);
  }
}

Nel caso ci fosse un qualche dubbio sull'uso che si può fare delle funzioni, si tenga presente che è sempre possibile usare una istruzione del tipo variabile=nomefunzione(...), che mette il risultato della funzione nella variabile, e poi utilizzare il valore della variabile nel punto in cui serve il risultato calcolato della funzione.

Variabili passate come parametri

Il programma Locali.java chiarisce un punto che può risultare poco chiaro nella programmazione con procedure e funzioni.
/*
  Esempio sull'uso di variabili locali.
*/

class Locali {
  static void uno(int x) {
    x=x+1;
  }

  static int due(int x) {
    x=x+1;
    return x;
  }

  public static void main(String[] args) {
    int x=5;
    int y;

    uno(x);
    System.out.println(x);

    y=due(x);
    System.out.println(x);
    System.out.println(y);
  }
}

Ci poniamo il seguente problema: cosa viene stampato? Questo non è difficile, se si seguono esattamente le regole sul passaggio dei parametri che sono state specificate fino ad ora. A volte però vengono date risposte errate, del tipo ``viene stampato per tre volte il numero 6'', oppure ``viene stampato 5, poi due volte 6''. La risposta esatta è:
viene stampato due volte 5, poi una volta 6
Se il risultato che ci si aspettava è diverso, allora non si sono seguite correttamente le regole di passaggio sulle procedure.
Analizziamo quindi l'esecuzione del programma:
  1. viene creata la variabile x e ci viene messo 5;
  2. si crea la variabile y;
  3. si chiama la procedura uno, passando il valore di x (che in questo caso è 5);
  4. la procedura uno crea una sua variabile che chiama x, e in questa variabile ci mette il valore passato, cioè 5; è importante notare che questa variabile, anche se ha lo stesso nome, non è la stessa di quella che sta nel programma;
  5. la variabile x interna alla procedura uno viene incrementata;
  6. questo termina l'esecuzione di uno;
  7. si esegue System.out.println(x); fino a questo punto, la variabile x non è stata modificata: infatti, la procedura uno ha modificato la sua variabile x, che non è la stessa del programma. Quindi, si stampa il valore 5.
La chiamata alla funzione due è simile: la variabile x che viene incrementata è una variabile locale della funzione, e non è la stessa del programma. La funzione due permette di vedere come l'unico modo che una funzione ha di influenzare il programma è quello di ritornare dei valori: tutte le altre modifiche vengono ignorate dal programma. In questo caso, la variabile x interna alla funzione viene modificata, ma questa modifica non si riflette sulla variabile x del programma, ma soltanto nel fatto che il valore che la procedura rimanda al programma (che è il valore di x aumentato di 1) viene memorizzato nella variabile y.

Variabili globali

Il meccanismo con cui le procedure interagiscono con il programma principale visto fino ad ora è quello del passaggio dei parametri (con cui il programma manda dei dati alle procedure) e il valore di ritorno (return, con cui la funzioni mandano dati al programma principale quando hanno terminato).
Esiste un altro meccanismo che permette al programma e alle procedure di comunicare, ed è quello delle variabili globali.
Come si è visto fino ad ora, le variabili dichiarate all'interno di una procedura e le variabili dichiarate nel programma principale sono diverse, e ogni blocco può accedere alle sole variabili dichiarate all'interno del blocco stesso. Per esempio, nel seguente programma java la variabile x dichiarata nella procedura test è diversa dalla variabile x del programma principale, anche se il nome è lo stesso.
class Prova {
  static void test() {
    int x;

    ...
  }

  public static void main(String[] args) {
    int x;

    ...
  }
}
Infatti, la situazione che si viene a creare nella memoria è la seguente:
test
x
    
      
 
 
main
x
    
      
 
Se per esempio si mette x=10 dentro il main, la variabile in cui viene messo il 10 è la variabile che sta nel contenitore di main. Se si esegue System.out.println(x) dentro il main, viene stampato il contenuto della casella x che sta dentro il contenitore di main, ecc. Lo stesso vale per l'altra variabile x, cioè quella del contenitore della procedura test: solo le istruzioni che stanno dentro testpossono modificare il contenuto di questa variabile, e solo queste possono accedere al contenuto precedentemente memorizzato. Si può dire che le due variabili, anche se hanno lo stesso nome, sono due variabili diverse.
Le variabili globali sono variabili che possono venire usate da tutte le procedure e dal programma principale. Si tratta di variabili che sono definite al di fuori delle procedure, e che sono accessibili da tutti i blocchi del programma. Per dichiarare una variabile globale, si mette la dichiarazione prima di tutte le procedure. Per esempio, se serve una variabile globale intera a si fa cosí:
class Prova {
  static int a;

  static void test() {
    int x;

    ...
  }

  public static void main(String[] args) {
    int x;

    ...
  }
}
In un certo senso, continua a valere la regola che le variabili definite all'interno di una coppia di graffe {...} sono utilizzabili solo all'interno dello stesso paio di graffe. In questo caso però la dichiarazione di a si trova tra class Prova{ e l'ultima }, per cui tutte le istruzioni di tutte le procedure (e del programma principale) possono usare questa variabile, sia per memorizzare un valore che per vedere il suo contenuto.
La situazione che si viene a creare nella memoria si può rappresentare come segue:
a
    
      
 
test
x
    
      
 
 
main
x
    
      
 
Detto a parole, la variabile a è una casella (zona di memoria) che non sta dentro il contenitore di nessuna delle procedure. Per questo tipo di variabili vale la regola che tutte le procedure e funzioni possono usare queste variabili.
Consideriamo il seguente programma Globali.java:
/*
  Esempio di variabili globali.
*/

class Globali {
  static int a;

  static void test() {
    a=a+1;
  }

  public static void main(String[] args) {
    a=10;

    test();
    test();

    System.out.println("La variabile vale "+a);
  }
}

Quello che succede è che la variabile a viene creata come una variabile globale. Quindi, sia test che main possono usarla. In particolare, quando si esegue a=10 dentro main, viene messo 10 nella variabile. Quando si chiama la procedura test, si esegue l'istruzione a=a+1. Questa istruzione incrementa di 1 il valore della variabile a, e questa è ancora la variabile globale. Quando si chiama la seconda volta la procedura, il contenuto della variabile viene incrementato di nuovo. Alla fine, l'istruzione System.out.println("La variabile vale "+a); stampa il valore di a, che ora è 12.
Si noti la differenza dal programma in cui vengono dichiarate due variabili con lo stesso nome a sia dentro test che dentro main: in questo caso, si tratta di due variabili distinte, e ogni blocco di istruzioni può accedere soltanto alla sua variabile, e non a quella dell'altro blocco (anche se le due variabili hanno lo stesso nome).
Questo meccanismo consente di far passare informazioni dal programma alle procedure e viceversa: se per esempio il programma vuole mandare un intero a una procedura, può mettere il valore in una variabile globale e poi chiamare la procedura. La procedura, a sua volta, può leggere il valore che il programma ha scritto in questa variabile. Si veda per esempio il programma che stampa un certo numero di linee bianche fatto usando una variabile globale:
class Linee {
  static int lineedalasciare;

  static void lineebianche() {
    int i;

    for(i=1; i<=lineedalasciare; i=i+1) {
      System.out.println(" ");
    }
  }

  public static void main(String[] args) {

    /* stampa 10 linee */
    lineedalasciare=10;
    lineebianche();

    ...

    /* stampa 20 linee */
    lineedalasciare=10;
    lineebianche();
  }
}
Quello che succede, in questo caso, è che il programma principale mette il valore 10 nella variabile lineedalasciare e poi chiama la procedura lineebianche(). Questa procedura contiene un ciclo in cui la variabile i va da 1 al contenuto della variabile lineedalasciare, che è appunto 10. In questo modo, la procedura riesce a usare un valore che è stato memorizzato dal programma principale.
Nota: il passaggio di parametri e i valori di ritorno sono anch'essi forme di comunicazione di dati fra procedure e programma. In effetti, è possibile non usare affatto gli argomenti, limitandosi a usare sempre variabili globali. Questo modo di procedere è fortemente sconsigliato, a meno che non sia realmente necessario (vedi il caso della reazione al mouse negli applet) perchè in questo modo si genera un codice difficile da leggere, cha spesso genera errori e sempre un abbassamento del voto finale.
Nota: ci sono dei casi in cui una variabile globale non è utilizzabile da una procedura. Questo problema viene analizzato nella pagina dedicata alla visibilità.

Visibilità delle variabili globali

Nella pagine precedente si è detto che le variabili globali si possono usare in tutte le procedure e nel programma principale. In effetti esiste una eccezione molto importante. Non tenerne conto può portare a programmi che compilano senza errori ma non funzionano nel modo dovuto. La regola è:
una variabile globale non è utilizzabile in una procedura se nella procedura viene definita un'altra variabile con lo stesso nome.
Per esempio, se definiamo una variabile globale che si chiama x, e poi in una procedura c'è una variabile locale x, allora la procedura ``vede'' solo la sua variabile locale. Consideriamo questo programma:
class Prova {
  static int x;

  static void test() {
    int x;

    x=10;
  }

  public static void main(String[] args) {
    x=5;
    test();
    System.out.println(x);
  }
}
La situazione che si viene a creare nella memoria è la seguente:
x
    
      
 
test
x
    
      
 
 
main
    
      
 
La assegnazione x=10 mette il valore 10 nella variabile x locale della procedura, e non in quella globale. Come risultato, la operazione di stampa che si trova nel programma principale stampa 5 e non 10. Infatti, la prima istruzione del programma mette 5 nella variabile globale; viene chiamata la procedura, che mette 10 nella sua variabile x locale; si ritorna dalla procedura e si stampa il valore della variabile globale, che contiene ancora 5.
La stessa cosa succede se la variabile locale che ha lo stesso nome è uno dei parametri:
class Prova {
  static int x;

  static void test(int x) {
    x=10;
  }

  public static void main(String[] args) {
    x=5;
    test(24);
    System.out.println(x);
  }
}
In questo caso, la variabile x della procedura test è un parametro, ma è sempre una variabile della procedura (l'unica differenza fra i parametri e le variabili dichiarate dentro la procedura è che il suo valore iniziale è quello inviato dal programma). Quindi, la assegnazione x=10 mette il valore 10 nella variabile x locale di test e non nella variabile globale x. Anche se hanno lo stesso nome, queste due variabili sono distinte.
Il risultato della esecuzione di questo programma è identico a quello del precedente: la prima istruzione x=5 mette 5 nella variabile globale, si chiama la procedura che mette 10 nella sua variabile locale, si ritorna e si stampa il contenuto della variabile globale, che contiene ancora il valore 5.
Il motivo per cui il programma principale vede la variabile globale, mentre la procedura vede quella locale, è che nel programma principale non ci sono variabili locali con lo stesso nome x, mentre nella procedura c'è una variabile locale con lo stesso nome, per cui la procedure vede la sua variabile e non quella globale.
variabili statiche (cancellazione di variabili alla fine delle procedure)
fonte: http://www.dis.uniroma1.it/~liberato/java/Procedure.html

Commenti

Post popolari in questo blog

Simulazioni di reti (con Cisco Packet Tracer)

Esercizi sulla rappresentazione della virgola mobile IEEE 754 (Floating Point)