Riferimenti a Metodi in Java

Concetti Chiave
  • I riferimenti a metodi in Java forniscono un modo per fare riferimento a metodi senza eseguirli, creando un'istanza di un'interfaccia funzionale compatibile.
  • I riferimenti a metodi possono essere statici o di istanza, e la sintassi per crearli varia a seconda del tipo.
  • I riferimenti a metodi statici utilizzano la sintassi NomeClasse::nomeMetodo, mentre i riferimenti a metodi di istanza utilizzano riferimentoOggetto::nomeMetodo.
  • I riferimenti a metodi possono essere utilizzati in contesti compatibili con le interfacce funzionali, migliorando la leggibilità e la manutenibilità del codice.

Riferimenti a Metodi

Esiste una caratteristica importante correlata alle espressioni lambda chiamata Riferimenti a Metodi.

Un riferimento ad un metodo fornisce un modo per fare riferimento a un metodo senza eseguirlo. Si relaziona alle espressioni lambda perché anch'esso richiede un contesto di tipo target che consiste in un'interfaccia funzionale compatibile. Quando valutato, un riferimento al metodo crea anche un'istanza dell'interfaccia funzionale.

Esistono diversi tipi di riferimenti ai metodi. Inizieremo con i riferimenti ai metodi ai metodi static.

Riferimenti a Metodi Statici

Per creare un riferimento a un metodo statico, si usa questa sintassi generale:

NomeClasse::nomeMetodo

Si noti che il nome della classe è separato dal nome del metodo da un doppio due punti. Il :: è un separatore che è stato aggiunto a Java dal JDK 8 espressamente per questo scopo. Questo riferimento al metodo può essere utilizzato ovunque in cui sia compatibile con il suo tipo di destinazione.

Il seguente programma dimostra un riferimento al metodo static:

// Dimostra un riferimento a metodo per un metodo static.

// Un'interfaccia funzionale per operazioni su stringhe.
interface FunzioneStringa {
    String funzione(String n);
}

// Questa classe definisce un metodo static chiamato invertiStr().
class MieOperazioniStringa {

    // Un metodo static che inverte una stringa.
    static String invertiStr(String str) {
        String risultato = "";
        int i;

        for(i = str.length()-1; i >= 0; i--)
            risultato += str.charAt(i);
        return risultato;
    }

}

class DemoRiferimentoMetodo {

    // Questo metodo ha un'interfaccia funzionale come tipo del
    // suo primo parametro. Quindi, può essere passata qualsiasi istanza
    // di quell'interfaccia, incluso un riferimento al metodo.
    static String operazioneStringa(FunzioneStringa fs, String s) {
        return fs.funzione(s);
    }

    // Il metodo main() dimostra l'uso di operazioneStringa().
    public static void main(String[] args)
    {
        String strIngresso = "Le lambda aggiungono potenza a Java";
        String strUscita;

    // Qui, un riferimento al metodo invertiStr
    // viene passato a operazioneStringa().
    strUscita = operazioneStringa(MieOperazioniStringa::invertiStr,
                                  strIngresso);

    // Stampa la stringa originale e quella invertita.
    System.out.println("Stringa originale: " + strIngresso);
    System.out.println("Stringa invertita: " + strUscita);
    }

}

L'output è mostrato qui:

Stringa originale: Le lambda aggiungono potenza a Java
Stringa invertita: avaJ a aznetop onognuigga adbmal eL

Nel programma, prestare particolare attenzione a questa riga:

strUscita = operazioneStringa(MieOperazioniStringa::invertiStr,
                              strIngresso);

Qui, un riferimento al metodo static invertiStr(), dichiarato all'interno di MieOperazioniStringa, viene passato come primo argomento a operazioneStringa(). Questo funziona perché invertiStr è compatibile con l'interfaccia funzionale FunzioneStringa. Quindi, l'espressione MieOperazioniStringa::invertiStr valuta a un riferimento a un oggetto in cui invertiStr fornisce l'implementazione di func() in FunzioneStringa.

Riferimenti ai Metodi per Metodi di Istanza

Per passare un riferimento a un metodo di istanza su un oggetto specifico, utilizziamo questa sintassi di base:

riferimentoOggetto::nomeMetodo

Come possiamo vedere, la sintassi è simile a quella utilizzata per un metodo static, eccetto che viene utilizzato un riferimento all'oggetto invece di un nome di classe

Ecco il programma precedente riscritto per utilizzare un riferimento al metodo di istanza:

// Dimostra un riferimento al metodo per un metodo di istanza

// Un'interfaccia funzionale per operazioni su stringhe.
interface FunzioneStringa {
    String funzione(String n);
}

// Ora, questa classe definisce un metodo di istanza chiamato invertiStr().
class MieOperazioniStringa {

    String invertiStr(String str) {
        String risultato = "";
        int i;

        for(i = str.length()-1; i >= 0; i--)
            risultato += str.charAt(i);
        return risultato;
    }

}

class DemoRifMetodo2 {

    // Questo metodo ha un'interfaccia funzionale come tipo del
    // suo primo parametro. Quindi, può essere passata qualsiasi istanza
    // di quella interfaccia, inclusi i riferimenti ai metodi.
    static String operazioneStringa(FunzioneStringa sf, String s) {
        return sf.funzione(s);
    }

    public static void main(String[] args)
    {
        String strIngresso = "Le lambda aggiungono potenza a Java";
        String strUscita;

        // Crea un oggetto MieOperazioniStringa.
        MieOperazioniStringa operStr = new MieOperazioniStringa();

        // Ora, un riferimento al metodo al metodo di istanza invertiStr
        // viene passato a operazioneStringa().
        strUscita = operazioneStringa(operStr::invertiStr, strIngresso);

        // Stampa la stringa originale e quella invertita.
        System.out.println("Stringa originale: " + strIngresso);
        System.out.println("Stringa invertita: " + strUscita);
    }

}

Questo programma produce lo stesso output della versione precedente.

Nel programma, notiamo che invertiStr() è ora un metodo di istanza di MieOperazioniStringa. All'interno di main(), viene creata un'istanza di MieOperazioniStringa chiamata operStr. Questa istanza viene utilizzata per creare il riferimento al metodo a invertiStr nella chiamata a operazioneStringa, come mostrato ancora qui:

strUscita = operazioneStringa(operStr::invertiStr, strIngresso);

In questo esempio, invertiStr() viene chiamato sull'oggetto operStr.

È anche possibile gestire una situazione in cui vogliamo specificare un metodo di istanza che può essere utilizzato con qualsiasi oggetto di una data classe, non solo un oggetto specificato. In questo caso, creeremo un riferimento al metodo come mostrato qui:

NomeClasse::nomeMetodoIstanza

Qui, il nome della classe viene utilizzato invece di un oggetto specifico, anche se viene specificato un metodo di istanza. Con questa forma, il primo parametro dell'interfaccia funzionale corrisponde all'oggetto che invoca e il secondo parametro corrisponde al parametro specificato dal metodo. Ecco un esempio. Definisce un metodo chiamato contatore() che conta il numero di oggetti in un array che soddisfano la condizione definita dal metodo funzione() dell'interfaccia funzionale MiaFunc. In questo caso, conta le istanze della classe TempAlta.

// Usa un riferimento a metodo di istanza con oggetti diversi.

// Un'interfaccia funzionale che prende due argomenti di riferimento
// e restituisce un risultato boolean.
interface MiaFunc<T> {
    boolean func(T v1, T v2);
}

// Una classe che memorizza la temperatura massima di un giorno.
class TempAlta {

    private int tempMassima;

    TempAlta(int tm) {
        tempMassima = tm;
    }

    // Restituisce true se l'oggetto TempAlta che invoca ha la stessa
    // temperatura di tm2.
    boolean stessaTemp(TempAlta tm2) {
        return tempMassima == tm2.tempMassima;
    }

    // Restituisce true se l'oggetto TempAlta che invoca ha una temperatura
    // che è minore di tm2.
    boolean minoreDiTemp(TempAlta tm2) {
        return tempMassima < tm2.tempMassima;
    }

}

class DemoMetodoIstanzaConRifOggetto {

    // Un metodo che restituisce il numero di occorrenze
    // di un oggetto per il quale alcuni criteri, come specificato dal
    // parametro MiaFunc, sono verificati.
    static <T> int contatore(T[] valori,
                             MiaFunc<T> f,
                             T v) {
        int conta = 0;
        for(int i=0; i < valori.length; i++)
            if(f.func(valori[i], v)) conta++;
        return conta;
    }

    public static void main(String[] args)
    {

        int conta;

        // Crea un array di oggetti TempAlta.
        TempAlta[] tempMassimiSettimana = {
            new TempAlta(21),
            new TempAlta(22),
            new TempAlta(24),
            new TempAlta(24),
            new TempAlta(25),
            new TempAlta(24),
            new TempAlta(23),
            new TempAlta(20)
        };

        // Usa contatore() con array della classe TempAlta.
        // Si noti che un riferimento al metodo di istanza
        // stessaTemp() viene passato come secondo argomento.
        conta = contatore(tempMassimiSettimana,
                        TempAlta::stessaTemp,
                        new TempAlta(21));

        System.out.println(conta + " giorni hanno avuto una massima di 21");

        // Ora, crea e usa un altro array di oggetti TempAlta.
        TempAlta[] tempMassimiSettimana2 = {
            new TempAlta(32),
            new TempAlta(12),
            new TempAlta(24),
            new TempAlta(19),
            new TempAlta(18),
            new TempAlta(12),
            new TempAlta(16),
            new TempAlta(13)
        };

        // Usa contatore() con il nuovo array.
        conta = contatore(tempMassimiSettimana2,
                          TempAlta::stessaTemp,
                          new TempAlta(12));

        System.out.println(conta + " giorni hanno avuto una massima di 12");

        // Ora, usa minoreDiTemp() per trovare quei giorni
        // quando la temperatura era minore
        // di un valore specificato.
        conta = contatore(tempMassimiSettimana2,
                          TempAlta::minoreDiTemp,
                          new TempAlta(16));

        System.out.println(conta + " giorni hanno avuto una massima minore di 16");

        // Ora, usa minoreDiTemp() con il secondo array.
        // Si noti che un riferimento al metodo di istanza
        // minoreDiTemp() viene passato come secondo argomento.
        conta = contatore(tempMassimiSettimana2,
                          TempAlta::minoreDiTemp,
                          new TempAlta(19));

        System.out.println(conta + " giorni hanno avuto una massima minore di 19");
    }

}

L'output è mostrato qui:

1 giorni hanno avuto una massima di 21
2 giorni hanno avuto una massima di 12
3 giorni hanno avuto una massima minore di 16
5 giorni hanno avuto una massima minore di 19

Nel programma, notiamo che TempAlta ha due metodi di istanza: stessaTemp() e minoreDiTemp(). Il primo restituisce true se due oggetti TempAlta contengono la stessa temperatura. Il secondo restituisce true se la temperatura dell'oggetto che invoca è minore di quella dell'oggetto passato. Ogni metodo ha un parametro di tipo TempAlta e ogni metodo restituisce un risultato boolean. Quindi, ognuno è compatibile con l'interfaccia funzionale MiaFunc perché il tipo dell'oggetto che invoca può essere mappato al primo parametro di func() e l'argomento mappato al secondo parametro di func(). Quindi, quando l'espressione

TempAlta::stessaTemp

viene passata al metodo contatore(), viene creata un'istanza dell'interfaccia funzionale MiaFunc in cui il tipo di parametro del primo parametro è quello dell'oggetto che invoca il metodo di istanza, che è TempAlta. Il tipo del secondo parametro è anche TempAlta perché quello è il tipo del parametro per stessaTemp(). Lo stesso vale per il metodo minoreDiTemp().

Un altro punto: ci si può riferire alla versione della superclasse di un metodo usando super, come mostrato qui:

super::nomeMetodo

Il nome del metodo è specificato da nomeMetodo. Un'altra forma è

NomeTipo.super::nomeMetodo

dove NomeTipo si riferisce a una classe contenitore o super-classe.

Riferimenti ai Metodi con i Generics

Possiamo utilizzare i riferimenti ai metodi con classi generiche e/o metodi generici. Ad esempio, consideriamo il seguente programma:

// Dimostra un riferimento a un metodo generico
// dichiarato all'interno di una classe non generica.

// Un'interfaccia funzionale che opera su un array
// e un valore, e restituisce un risultato int.
interface MiaFunc<T> {
    int funzione(T[] valori, T v);
}

// Questa classe definisce un metodo chiamato contaCorrispondenze() che
// restituisce il numero di elementi in un array che sono uguali
// a un valore specificato. Notare che contaCorrispondenze()
// è generico, ma MieOperazioniArray non lo è.
class MieOperazioniArray {

    // Questo metodo conta le corrispondenze tra gli elementi
    // di un array e un valore specificato. È generico e può
    // essere utilizzato con qualsiasi tipo di array.
    static <T> int contaCorrispondenze(T[] valori, T v) {
        int conta = 0;
        for (int i = 0; i < valori.length; i++)
            if (valori[i] == v)
                conta++;
        return conta;
    }

}

class DemoRifMetodoGenerico {

    // Questo metodo ha l'interfaccia funzionale MiaFunc come
    // tipo del suo primo parametro. Gli altri due parametri
    // ricevono un array e un valore, entrambi di tipo T.
    static <T> int miaOp(MiaFunc<T> f,
                         T[] valori,
                         T v) {
        return f.funzione(valori, v);
    }

    public static void main(String[] args) {
        Integer[] valori = { 1, 2, 3, 4, 2, 3, 4, 4, 5 };
        String[] stringhe = { "Uno", "Due", "Tre", "Due" };
        int conta;

        // Qui, passiamo il metodo contaCorrispondenze come
        // riferimento a un metodo. Notare che il tipo di
        // contaCorrispondenze() è compatibile con l'interfaccia
        // funzionale MiaFunc, quindi possiamo usarlo come
        // riferimento a un metodo.
        // Il compilatore deduce il tipo di T come Integer.
        conta = miaOp(MieOperazioniArray::<Integer>contaCorrispondenze, valori, 4);
        System.out.println("valori contiene " + conta + " 4");

        // Qui, passiamo lo stesso metodo come riferimento a un
        // metodo, ma questa volta con un array di stringhe.
        // Notare che il tipo di contaCorrispondenze() è ancora
        // compatibile con l'interfaccia funzionale MiaFunc,
        // quindi possiamo usarlo come riferimento a un metodo.
        // Il compilatore deduce il tipo di T come String.
        conta = miaOp(MieOperazioniArray::<String>contaCorrispondenze, stringhe, "Due");
        System.out.println("stringhe contiene " + conta + " Due");
    }

}

L'output è mostrato qui:

valori contiene 3 4
stringhe contiene 2 Due

Nel programma, MieOperazioniArray è una classe non generica che contiene un metodo generico chiamato contaCorrispondenze(). Il metodo restituisce un conteggio degli elementi in un array che corrispondono a un valore specificato. Notiamo come viene specificato l'argomento del tipo generico. Ad esempio, la sua prima chiamata in main(), mostrata qui:

conta = miaOp(MieOperazioniArray::<Integer>contaCorrispondenze, valori, 4);

passa l'argomento di tipo Integer. Notiamo che esso appare dopo il ::. Questa sintassi può essere generalizzata: Quando un metodo generico viene specificato come riferimento a un metodo, il suo argomento di tipo viene dopo il :: e prima del nome del metodo. È importante sottolineare, tuttavia, che specificare esplicitamente l'argomento di tipo non è richiesto in questa situazione (e in molte altre) perché l'argomento di tipo sarebbe stato automaticamente dedotto. Nei casi in cui viene specificata una classe generica, l'argomento di tipo segue il nome della classe e precede il ::.

Sebbene gli esempi precedenti mostrino la meccanica dell'uso dei riferimenti ai metodi, non mostrano i loro veri vantaggi. Un posto dove i riferimenti ai metodi possono essere molto utili è in congiunzione con il Collections Framework, che studieremo nelle prossime lezioni. Tuttavia, per completezza, è incluso qui un esempio breve ma efficace che usa un riferimento a un metodo per aiutare a determinare l'elemento più grande in una collezione.

Un modo per trovare l'elemento più grande in una collezione è usare il metodo max() definito dalla classe Collections. Per la versione di max() usata qui, dovete passare un riferimento alla collezione e un'istanza di un oggetto che implementa l'interfaccia Comparator<T>. Questa interfaccia specifica come due oggetti vengono confrontati. Definisce solo un metodo astratto, chiamato compare(), che prende due argomenti, ciascuno del tipo degli oggetti che vengono confrontati. Deve restituire maggiore di zero se il primo argomento è maggiore del secondo, zero se i due argomenti sono uguali, e minore di zero se il primo oggetto è minore del secondo.

In passato, per usare max() con oggetti definiti dall'utente, un'istanza di Comparator<T> doveva essere ottenuta prima implementandola esplicitamente con una classe, e poi creando un'istanza di quella classe. Questa istanza veniva poi passata come comparatore a max(). A partire da JDK 8, ora è possibile semplicemente passare un riferimento a un metodo di confronto a max() perché farlo implementa automaticamente il comparatore. Il seguente esempio semplice mostra il processo creando un ArrayList di oggetti MiaClasse e poi trovando quello nella lista che ha il valore più alto (come definito dal metodo di confronto).

// Usa un riferimento a un metodo per aiutare a trovare il valore massimo in una collezione.
import java.util.*;

// Questa classe rappresenta un oggetto con un valore intero.
// La classe ha un metodo getVal() per ottenere il valore.
class MiaClasse {
    private int val;

    MiaClasse(int v) {
        val = v;
    }

    int getVal() {
        return val;
    }
}

class UsaRifMetodo {

    // Un metodo compare() compatibile con quello definito da Comparator<T>.
    static int confrontaMC(MiaClasse a, MiaClasse b) {
        return a.getVal() - b.getVal();
    }

    public static void main(String[] args) {
        // Crea una collezione di oggetti MiaClasse.
        ArrayList<MiaClasse> al = new ArrayList<MiaClasse>();

        // Aggiunge alcuni oggetti MiaClasse alla collezione.
        al.add(new MiaClasse(1));
        al.add(new MiaClasse(4));
        al.add(new MiaClasse(2));
        al.add(new MiaClasse(9));
        al.add(new MiaClasse(3));
        al.add(new MiaClasse(7));

        // Trova il valore massimo in al usando il metodo confrontaMC().
        MiaClasse objValMax = Collections.max(al, UsaRifMetodo::confrontaMC);
        System.out.println("Il valore massimo è: " + objValMax.getVal());
    }

}

L'output è mostrato qui:

Il valore massimo è: 9

Nel programma, notiamo che MiaClasse non definisce alcun metodo di confronto proprio, né implementa Comparator.

Tuttavia, il valore massimo di una lista di elementi MiaClasse può ancora essere ottenuto chiamando max() perché UsaRifMetodo definisce il metodo statico confrontaMC(), che è compatibile con il metodo compare() definito da Comparator. Pertanto, non c'è bisogno di implementare esplicitamente e creare un'istanza di Comparator.