Dichiarazione di metodi (ripasso...)

Sintassi della dichiarazione di un metodo:
 
    <modificatori> <tipo> <nome> (<lista_parametri_formali>) {
        ...
    }

Esempio: dichiarazione del metodo min(int,int) della classe Math.
    public static int min (int a, int b) { 
        if ( a<b )
            return a;
        else
            return b;
    }

  • <modificatori>: i modificatori descrivono proprietà del metodo come

    • visibilità    (private, protected, public),
    • modificabilità    (final),
    • appartenenza ad una classe o alle istanze    (static).

  • <tipo>: Il tipo del metodo è il tipo del valore restituito dal metodo, oppure void.

  • <nome>: Il nome del metodo è un identificatore qualunque, che individua il metodo all'interno della classe.

  • <lista_parametri_formali>: I parametri formali consistono di una lista (anche vuota) di coppie <tipo> <parametro> separate da virgole, dove <tipo> è un tipo di dati e <parametro> è una variabile (identificatore).

  • <corpo>: Il corpo del metodo è un blocco che contiene istruzioni fra cui una o più istruzioni return (secondo le regole che vedremo dopo).



Firma di un metodo e overloading

Java supporta un meccanismo di overloading (sovraccarico) che consente di chiamare più metodi della stessa classe con lo stesso nome, purché ogni metodo abbia una diversa firma (signature).

Dato un generico metodo:
 

<modificatori> <tipo> <nome> ( <tipo1> <p1> , ... , <tipon> <pn>) {
    ...
}

la firma corrispondente è data dalla sequenza:

<nome>(<tipo1>, ..., <tipon>)

Attenzione: la firma non comprende né il tipo del metodo, né i nomi dei parametri formali.



Esempi di overloading

Alcuni esempi di overloading già visti:

Stampa di dati sullo standard output.
    System.out.println(345);    // stampa un intero

    System.out.println(Math.PI);    // stampa un double

    System.out.println("Pippo");    // stampa una stringa

    java.awt.Rectangle r = new java.awt.Rectangle(5, 10, 20, 30);

    System.out.println(r);    // stampa un oggetto

L'oggetto System.out (che rappresenta lo standard output) è un'istanza della classe PrintStream, che definisce i metodi aventi le seguenti firme (oltre a tanti altri):

    println(int)
    println(double)
    println(Object)

Analogamente, abbiamo visto:

I metodi print della classe ArrayUtils
    // Stampa tutti gli elementi dell'array di interi
    // passato per argomento
    public static void print (int[] array) {
       ...
    }

    // Stampa tutti gli elementi dell'array di stringhe
    // passato per argomento
    public static void print (String[] array) {
       ...
    }

    // Le firme sono:     print (int[])     print (String[])

I metodi appena visti non restituiscono un risultato. In generale, due metodi con lo stesso nome ma firma diversa possono restituire risultati diversi.

Ad esempio, nella classe java.lang.Math [locale, Medialab, Sun] ci sono diversi metodi min che restituiscono il minore tra due numeri, tra i quali:

   public static int min(int a, int b) {
        ...
   };

   public static double min(double a, double b) {
        ...
   }

    // Le firme sono:     min (intint)     min (doubledouble)

Si noti che:

  • È naturale chiamarli con lo stesso nome perché implementano la medesima funzionalità.

  • Anche se ogni int è un double, non possiamo avere solo il secondo metodo, perchè il minore di due interi sarebbe un numero decimale!



Invocazione di un metodo: compilazione

I metodi possono essere invocati all'interno di espressioni, come in
double i = Math.sqrt(2) * MyRecursiveMethods.fib(3);
oppure come istruzioni (tipicamente sono metodi di tipo void)
System.out.println("ciao");


Quando il compilatore incontra una invocazione di un metodo come

MyClass.name(exp1,...,expn)
esegue i seguenti passi:
  • determina il tipo di ciascuna delle espressioni exp1,...,expn: siano questi type1,...,typen
  • controlla che nella classe MyClass esista una dichiarazione di un metodo con firma:

    name(type1, ..., typen)

    e compila l'invocazione in modo da chiamare il metodo individuato.

Se questa dichiarazione non esiste, la compilazione non ha successo e viene stampato un messaggio di errore. Ad esempio,

public class prova {  
    public static void main(String[] args) {
        System.out.println(Math.min(2, 3, 7));  // min con tre argomenti
    }
}

javac  prova.java prova.java:5: cannot resolve symbol symbol  : method min (int,int,int// non esiste un metodo con questa firma location: class java.lang.Math         System.out.println(Math.min(2, 3, 7));                                ^ Compilation exited abnormally with code 1




Invocazione di un metodo: esecuzione

Quando l'interprete incontra una invocazione di un metodo come
MyClass.name(exp1,...,expn)
esegue i seguenti passi:
  • valuta ciascuna delle espressioni exp1,...,expn, ottenendo altrettanti valori val1,...,valn, (di tipo type1,...,typen)
  • Supponiamo che la dichiarazione di metodo associata dal compilatore a questa invocazione sia:

    <modificatori> type name (type1 par1 , ... , tipon parn) { 
        <istruzioni>
    }

  • esegue la fase denominata passaggio dei parametri:  viene allocato sulla pila il record di attivazione comprendente anche le variabili par1,...,parn che vengono inizializzate con i parametri attuali (nello specifico, i valori val1,...,valn)

  • esegue le <istruzioni> che formano il corpo del metodo fino a trovare il primo return, oppure fino alla fine se non c'è nessun return (solo se il metodo ha tipo void)

  • se viene eseguito un comando del tipo:

    return <expr>;
    l'espressione <expr> viene valutata ed il valore risultante viene restituito al metodo chiamante (come indicato dall'indirizzo di ritorno del record di attivazione).



Passaggio dei parametri

Il passaggio dei parametri è il meccanismo che lega i parametri attuali di una specifica invocazione di un metodo ai parametri formali della corrispondente dichiarazione.

Esistono varie modalità di passaggio dei parametri: per valore, per riferimento, per valore/risultato, per costante,... Lo studio di tali modalità e delle differenze tra di loro sarà argomento di corsi successivi.

Il linguaggio Java adotta il passaggio di parametri per valore, descritto di seguito.

Se abbiamo la chiamata

nome(val1,...,valn)

e la corrispondente dichiarazione

    <modificatori> type name (type1 par1 , ... , typen parn) { 
        <istruzioni>
    }

il passaggio di parametri seguito dall'esecuzione del corpo del metodo è equivalente all'esecuzione del seguente blocco di istruzioni:

{
    type1 par1 = val1;
    ...
    typen parn = valn;
    <istruzioni>
}

Si noti che il meccanismo con cui il compilatore individua il metodo associato ad una specifica invocazione (basato sulla firma) garantisce che

  • il numero di parametri attuali e di parametri formali coincide;

  • il tipo di ogni parametro attuale è compatibile con quello del corrispondente parametro formale.




Passaggio di valori di tipi elementari

Se un parametro è di tipo elementare (es. int, boolean, double, char, ...), durante il passaggio dei parametri al parametro formale viene assegnato il valore risultante dalla valutazione del parametro attuale.

Eventuali modifiche del parametro formale non si ripercuotono all'esterno del metodo.

 public class PassaggioValore { 

    public static void main (String[] args) {
        int n = 30;
        System.out.println("n vale " + n); // Stampa 30 
        nonModifica(n); 
        System.out.println("n vale ancora " + n); // Stampa 30 
    }
 
    public static void nonModifica(int i) { 
        System.out.println("i vale " + i); // Stampa 30 
        i = 0;
        System.out.println("adesso i vale " + i); // Stampa 0 
    }
}

Vediamo perché:
 
Istruzione eseguita
Stato
  int n = 30; // assegnamento
n
30
  nonModifica(n); // il passaggio di parametri
                  // equivale a
                  // int i = n;
i
n
30
30
      i = 0; // assegnamento a variabile locale
             // eseguito nel corpo del metodo
i
n
0
30
  System.out.println("n vale ancora " + n); // Stampa 30 
n
30




Passaggio di (riferimenti ad) oggetti

Se il tipo di un parametro è un tipo riferimento (un array o una classe, come String, Rectangle, ...), durante il passaggio dei parametri al parametro formale viene assegnato un riferimento all'oggetto o array risultante dalla valutazione del parametro attuale.

Eventuali modifiche fatte a tale oggetto nel metodo invocato saranno visibili dal metodo chiamante dopo la fine dell'esecuzione.

Abbiamo già incontrato esempi di effetti collaterali dovuti al passaggio di riferimenti (es. con il metodo sort della classe ArrayUtils). Vediamo di capire meglio quale è il motivo con un esempio più semplice.

public class PassaggioRiferimento { 

    public static void main (String[] args) {
        int[] array = new int[1]; 
        array[0] = 30;
        System.out.println("array[0] vale " 
                           + array[0]); // Stampa 30 
        modifica(array); 
        System.out.println("adesso array[0] vale " 
                           + array[0]); // Stampa 0 
    }

    public static void modifica(int[] a) { 
        System.out.println("a[0] vale " 
                           + a[0]); // Stampa 30 
        a[0] = 0;
        System.out.println("adesso a[0] vale " 
                           + a[0]); // Stampa 0 
    }
}

Vediamo perché:
 
Istruzione eseguita
Stato
int[] array = new int[1];
array[0] = 30;
modifica(array);
// il passaggio di parametri
// equivale a
// int[] a = array;
    a[0] = 0;
    // eseguito nel corpo del metodo
System.out.println("adesso array[0] vale " 
                   + array[0]); // Stampa 0 




Oggetti non modificabili: String

Ci sono oggetti il cui stato non può essere cambiato: questi vengono chiamati oggetti non modificabili.

Il tipico esempio è dato dalle stringhe: poichè la classe String non mette a disposizione alcun metodo per modificare lo stato di una sua istanza (cioè la sequenza di caratteri in essa contenuta), le stringhe sono non modificabili.

Ovviamente possiamo assegnare una nuova stringa a una variabile s di tipo String, ma questo corrisponde a creare un nuovo oggetto e quindi un nuovo riferimento che viene assegnato a s, senza modificare la vecchia stringa.

Più in generale, un errore comune consiste nel tentare di modificare lo stato dell'oggetto passato come parametro assegnando un nuovo oggetto al parametro formale: è importante ricordarsi che assegnamenti di questo tipo non hanno effetto all'esterno del metodo.

public class PassaggioString { 

    public static void main (String[] args) {

        System.out.print("Dammi una stringa: "); 
        String str = Input.readLine();
        System.out.println("\nstr e' \"" + str + "\""); 
        nonModifica(str); 
        System.out.println("str e' ancora \"" + str + "\""); 
    }
 
    public static void nonModifica(String s) { 
        System.out.println("s prima \"" + s + "\""); 
        System.out.print("Dammi una stringa diversa: "); 
        s = Input.readLine(); 
        System.out.println("s dopo \"" + s + "\""); 
    }
}

Vediamo cosa succede:
 
Istruzione eseguita
Stato
String str = Input.readLine();
nonModifica(str);
// il passaggio di parametri
// equivale a
// String s = str;
    s = Input.readLine();
    // eseguito nel corpo del metodo
System.out.println("str e' ancora \""
                   + str + "\"");




Il comando return

Ricordiamo che il tipo di un metodo è il tipo del dato restituito dal metodo, oppure void.

Se il tipo è diverso da void, allora si dice che il metodo è tipizzato e deve forzatamente restituire un valore del tipo indicato. Conseguentemente per ogni possibile ramo di esecuzione del corpo deve comparire un comando return  seguito da un'espressione che abbia lo stesso tipo del metodo.

public class EsempioReturn {
    public static String happy() {
        if (true)  
            return "I'm happy!!";
    }
}
> javac EsempioReturn.java
EsempioReturn.java:2: missing return statement
    public static String happy() {
                                 ^
1 error
Compilation exited abnormally with code 1
public class EsempioReturnOK {
    public static String happy() {
        if (true)  
            return "I'm happy!!";
        else 
            return "I'm not happy!";
    }
}
> javac EsempioReturnOK.java
 
Compilation finished
 
Se invece il tipo è void, allora il metodo si chiama non tipizzato e non restituisce niente. I metodi non tipizzati normalmente non contengono alcun comando return. Possono comunque contenere dei comandi return, purché non siano seguiti da espressioni. In tal caso l'esecuzione del metodo termina quando si incontra un return oppure quando si sono eseguite tutte le istruzioni.



return e variabili riferimento

Il tipo di un metodo può anche essere una classe o un tipo array (es. il metodo substring della classe String restituisce una stringa; i metodi fill_int, fill_String e clone di ArrayUtils restituiscono array di interi). In questo caso non viene restituito un valore, ma il riferimento ad un oggetto.

È anche possibile che venga restituita la costante speciale null: se una variabile di tipo riferimento vale null significa che non riferisce alcun oggetto.

Durante l'esecuzione di un programma, se viene invocato un metodo d'istanza su di una variabile di tipo riferimento che in quel momento ha come valore la costante null il programma viene interrotto segnalando un'anomalia (lanciando l'eccezione NullPointerException).

Quindi è buona norma controllare che una variabile sia diversa da null prima di invocare un metodo su di essa.

public class Ora {
    private int ore;

    public Ora(int h) {
        ore = h;
    }

    public void stampa() {
        System.out.println("Sono le " + ore); 
    } 
}

public class TestOra { 

    public static void main(String[] args) {
        System.out.print("Inserisci l'ora (0-23): "); 
        int h = Input.readInt(); 
        Ora o = crea(h); 
        if (o!= null) 
            o.stampa(); 
        else 
            System.out.print("Ora errata");
            // quando o==null, la chiamata o.stampa() fallisce
    }

    public static Ora crea(int ora) { 
        if ( 0<=ora && ora<24 ) 
            return new Ora(ora); 
        else 
            return null; 
    }
}

 




 Metodi con numero variabile di argomenti

Con le versioni di Java fino alla 1.4, per definire un metodo con un numero variabile di argomenti occorre utilizzare un parametro formale di tipo array.

Ad esempio, abbiamo visto il metodo maxIterative della classe ArrayUtils:

    // Restituisce il massimo di un array di interi,
    // Integer.MIN_VALUE se e' vuoto
    public static int maxIterative(int[] array){        
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < array.length; i++) 
            if (array[i] > max) max = array[i];
        return max;
    }

Per invocare il metodo maxIterative su di un insieme di numeri, occorre creare un array, inserirci i numeri, e passarlo come parametro attuale nella chiamata del metodo.

public class TestMaxIterative{
    public static void main (String [] args){
        // Inserisco i numeri di cui voglio calcolare il max in un array
        // Assegno gli elementi uno ad uno
        int [] arr = new int [5];
        arr[0] = 5;
        arr[1] = -13;
        arr[2] = 74;
        arr[3] = 333;
        arr[4] = 5;
        System.out.println("Massimo dell'array: " + 
                           ArrayUtils.maxIterative(arr));
        System.out.println("====================================");

        // Uso la sintassi apposita per inizializzare l'array
        int [] arr1 = {5, -13, 74, 333, 5};
        System.out.println("Massimo dell'array: " + 
                           ArrayUtils.maxIterative(arr1));
        System.out.println("====================================");

        // Creo l'array e lo inizializzo all'interno della chiamata
        System.out.println("Massimo dell'array: " + 
                           ArrayUtils.maxIterative(new int[]{5, -13, 74, 333, 5}));
    }
}




Dichiarare metodi con Varargs

Java 1.5 (o 5.0) ha introdotto nel linguaggio i varargs, cioè metodi con un numero variabile di argomenti in cui l'uso di un array per il passaggio dei parametri, come visto sopra, è reso implicito.

Nella definizione di un metodo con varargs il parametro formale di tipo array viene dichiarato, invece che con la normale sintassi <tipo> [] par, con la sintassi:

    <tipo>... par

dove i puntini sospensivi danno l'idea che il parametro attuale corrispondente può essere presente zero o più volte. All'interno del corpo del metodo, il parametro formale par avrà comunque tipo <tipo> [].

Attenzione: solo l'ultimo parametro formale di un metodo può avere questa nuova sintassi.

Come esempio, il seguente metodo max è la versione con varargs di maxIterative:

    /**
       Restituisce il massimo di una sequenza di interi,
       Integer.MIN_VALUE se la sequenza e' vuota
    */
    public static int max(int... array) {        
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < array.length; i++) 
            if (array[i] > max) max = array[i];
        return max;
    }
}

Si noti che l'uso di un array in questo metodo può essere reso completamente implicito usando un for generalizzato, come nel metodo seguente della classe VarargsMax:

public class VarargsMax{

    /**
       Restituisce il massimo di una sequenza di interi,
       Integer.MIN_VALUE se la sequenza e' vuota
    */
    public static int max(int... arg){
        int max = Integer.MIN_VALUE;
        for (int next : arg)
            if (next > max) max = next;
        return max;
    }
}




Invocare metodi con Varargs

La chiamata di un metodo con varargs può avere un numero variabile di parametri attuali: tutti i valori dei parametri attuali in corrispondenza del vararg vengono inseriti automaticamente in un array, che viene legato al parametro formale al momento del passaggio dei parametri: si guardino con attenzione le chiamate del metodo VarargsMax.max nella classe TestVarargsMax. Si noti (nell'ultima chiamata) che è anche possibile passare direttamente un array invece di una lista di valori.

public class TestVarargsMax{
    public static void main (String [] args){

        System.out.print("Massimo di una lista di 0 numeri: ");
        System.out.print("\nRisultato => ");
        System.out.println(VarargsMax.max());
        System.out.print("Massimo di una lista di 1 numero: ");
        System.out.print("5 \nRisultato => ");
        System.out.println(VarargsMax.max(5));
        System.out.print("Massimo di una lista di 2 numeri: ");
        System.out.print("5, 3 \nRisultato => ");
        System.out.println(VarargsMax.max(5, 3));
        System.out.print("Massimo di una lista di 3 numeri: ");
        System.out.print("5, 3, 8 \nRisultato => ");
        System.out.println(VarargsMax.max(5, 3, 8));
        System.out.print("Massimo di una lista di 4 numeri: ");
        System.out.print("5, 3, 8, -6 \nRisultato => ");
        System.out.println(VarargsMax.max(5, 3, 8, -6));
        int [] arr = {1, 2, 3, 6, 7, 5};
        System.out.print("Massimo dell'array: ");
        ArrayUtils.print(arr);
        System.out.print("Risultato => ");
        System.out.println(VarargsMax.max(arr));
    }
}

L'output di questo test è il seguente:

Massimo di una lista di 0 numeri
Risultato => -2147483648
Massimo di una lista di 1 numero: 5
Risultato => 5
Massimo di una lista di 2 numeri: 5, 3
Risultato => 5
Massimo di una lista di 3 numeri: 5, 3, 8
Risultato => 8
Massimo di una lista di 4 numeri: 5, 3, 8, -6
Risultato => 8
Massimo dell'array: [ 1,2,3,6,7,5 ]
Risultato => 7




Stampa formattata con printf

In aggiunta ai classici metodi print e println usati per stampare (e, come vedremo in seguito, per scrivere su file e su altri stream di output), Java 5.0 ha introdotto il metodo printf (ispirandosi al linguaggio C) che permette di specificare in modo semplice il modo in cui valori numerici e stringhe devono essere rappresentati (formattati) in output.

Questo permette, ad esempio, di stampare valori double con due sole cifre decimali, oppure di incolonnare correttamente una lista di interi con un numero variabile di cifre, o ancora di allineare alcune stringhe a sinistra.

Il primo parametro del metodo printf è una stringa di formato, che indica il posto e il modo in cui vanno visualizzati i parametri che seguono. Infatti, la stringa di formato oltre a contenere caratteri da stampare normalmente contiene anche sequenze speciali di caratteri, chiamate specificatori di formato, che iniziano sempre col carattere % e terminano con una lettera che indica il tipo di formato. Alcuni esempi di tipi di formati tra i più comuni sono:

d Intero decimale esempio: 123
x Intero esadecimale esempio: 7B
f Virgola mobile esempio: 12.30
e Virgola mobile (notazione esponenziale) esempio: 1.23e+1
g Virgola mobile generico
(esponenziale solo per valori molto grandi o molto piccoli)
 
s Stringa  
n Fine riga indipendente dalla piattaforma
'\r' + '\n' sotto windows, '\n' sotto altri OS
 

I restanti parametri passati a printf sono i valori da visualizzare in accordo agli specificatori di formato presenti nella stringa di formato.

Esempio TestPrintf (HTML / Java): stampa incolonnata di un array di double con totale.

Gli specificatori di formato possono anche contenere altri caratteri speciali detti modificatori di formato che modificano il formato di default. Esempi di modificatori sono:

- Allineamento a sinistra
0 Mostra 0 iniziali
, Mostra il separatore di migliaia
( Racchiude tra parentesi valori negativi
^ Usa lettere maiuscole

La classe String possiede un metodo format che è simile a printf, però invece di visualizzare i dati in output, restituisce la stringa corrispondente.