Introduzione alle Espressioni Lambda in Java
- Le espressioni lambda in Java sono metodi anonimi che implementano interfacce funzionali.
- Un'interfaccia funzionale è un'interfaccia con un solo metodo astratto, che definisce il tipo target di un'espressione lambda.
- Le espressioni lambda possono avere uno o più parametri, e il loro tipo può essere inferito dal contesto.
- Un'espressione lambda può essere utilizzata in contesti come assegnazioni, ritorni di metodi e argomenti di metodi.
Introduzione alle Espressioni Lambda
La chiave per comprendere l'implementazione Java delle espressioni lambda sono due costrutti. Il primo è l'espressione lambda stessa. Il secondo è l'interfaccia funzionale. Iniziamo con una semplice definizione di ciascuno.
Un'espressione lambda è, essenzialmente, un metodo anonimo (cioè, senza nome). Tuttavia, questo metodo non viene eseguito da solo. Invece, viene utilizzato per implementare un metodo definito da un'interfaccia funzionale. Quindi, un'espressione lambda risulta in una forma di classe anonima. Le espressioni lambda sono anche comunemente chiamate closures.
Un'interfaccia funzionale è un'interfaccia che contiene uno e un solo metodo astratto. Normalmente, questo metodo specifica lo scopo previsto dell'interfaccia. Quindi, un'interfaccia funzionale rappresenta tipicamente una singola azione. Per esempio, l'interfaccia standard Runnable
è un'interfaccia funzionale perché definisce solo un metodo: run()
. Pertanto, run()
definisce l'azione di Runnable
.
Inoltre, un'interfaccia funzionale definisce il tipo target di un'espressione lambda. Ecco un punto chiave: un'espressione lambda può essere utilizzata solo in un contesto in cui il suo tipo target è specificato. Un'altra cosa: un'interfaccia funzionale è talvolta chiamata tipo SAM, dove SAM sta per Single Abstract Method.
Interfacce Funzionali e Metodi di Object
Un'interfaccia funzionale può specificare qualsiasi metodo pubblico definito da Object
, come equals()
, senza influenzare il suo stato di "interfaccia funzionale". I metodi pubblici di Object
sono considerati membri impliciti di un'interfaccia funzionale perché sono automaticamente implementati da un'istanza di un'interfaccia funzionale.
Ora esaminiamo più da vicino sia le espressioni lambda che le interfacce funzionali.
Fondamenti delle Espressioni Lambda
L'espressione lambda ha introdotto un nuovo elemento di sintassi e un operatore nel linguaggio Java.
Il nuovo operatore, a volte chiamato operatore lambda o operatore freccia, è −>
. Divide un'espressione lambda in due parti:
- Il lato sinistro specifica tutti i parametri richiesti dall'espressione lambda. Se non sono necessari parametri, viene utilizzata una lista di parametri vuota.
- Sul lato destro c'è il corpo lambda, che specifica le azioni dell'espressione lambda. Il
−>
può essere verbalizzato come diventa o implementa.
Java definisce due tipi di corpi lambda:
- Uno consiste in una singola espressione;
- l'altro tipo consiste in un blocco di codice.
Inizieremo con le lambda che definiscono una singola espressione. Le lambda con corpi a blocco sono discusse nelle prossime lezioni.
A questo punto, sarà utile guardare alcuni esempi di espressioni lambda prima di continuare. Iniziamo con quello che è probabilmente il tipo più semplice di espressione lambda che si possa scrivere. Valuta a un valore costante ed è mostrato qui:
() -> 123.45
Questa espressione lambda non prende parametri, quindi la lista dei parametri è vuota. Restituisce il valore costante 123.45. Pertanto, è simile al seguente metodo:
double mioMetodo() {
return 123.45;
}
Naturalmente, il metodo definito da un'espressione lambda non ha un nome.
Un'espressione lambda leggermente più interessante è mostrata qui:
() -> Math.random() * 100
Questa espressione lambda ottiene un valore pseudo-casuale da Math.random()
, lo moltiplica per 100 e restituisce il risultato. Anch'essa non richiede un parametro.
Quando un'espressione lambda richiede un parametro, viene specificato nella lista dei parametri sul lato sinistro dell'operatore lambda. Ecco un semplice esempio:
(n) -> (n % 2)==0
Questa espressione lambda restituisce true
se il valore del parametro n
è pari. Benché sia possibile specificare esplicitamente il tipo di un parametro, come n
in questo caso, spesso non sarà necessario farlo perché in molti casi il suo tipo può essere inferito. Come un metodo con nome, un'espressione lambda può specificare tutti i parametri necessari.
Interfacce Funzionali
Come detto, un'interfaccia funzionale è un'interfaccia che specifica solo un metodo astratto.
Se si lavora in Java da qualche tempo, si potrebbe inizialmente pensare che tutti i metodi delle interfacce siano implicitamente astratti.
Anche se questo era vero prima di JDK 8, la situazione è cambiata. Come spiegato nelle lezioni sulle interfacce, a partire da JDK 8, è possibile specificare un'implementazione predefinita per un metodo dichiarato in un'interfaccia. Anche i metodi di interfaccia privati e statici forniscono un'implementazione. Di conseguenza, oggi, un metodo di interfaccia è astratto solo se non specifica un'implementazione.
Poiché i metodi di interfaccia non default
, non static
e non private
sono implicitamente astratti, non c'è bisogno di usare il modificatore abstract
(anche se si può specificarlo all'occorrenza).
Ecco un esempio di un'interfaccia funzionale:
interface MioNumero {
double ottieniValore();
}
In questo caso, il metodo ottieniValore()
è implicitamente astratto, ed è l'unico metodo definito da MioNumero
. Quindi, MioNumero
è un'interfaccia funzionale, e la sua funzione è definita da ottieniValore()
.
Come menzionato in precedenza, un'espressione lambda non viene eseguita da sola. Piuttosto, forma l'implementazione del metodo astratto definito dall'interfaccia funzionale che specifica il suo tipo di destinazione. Di conseguenza, un'espressione lambda può essere specificata solo in un contesto in cui è definito un tipo di destinazione. Uno di questi contesti viene creato quando un'espressione lambda viene assegnata a un riferimento di interfaccia funzionale. Altri contesti di tipo di destinazione includono l'inizializzazione di variabili, le istruzioni return
, e gli argomenti dei metodi, per citarne alcuni.
Analizziamo un esempio che mostra come un'espressione lambda può essere usata in un contesto di assegnazione. Prima, viene dichiarato un riferimento all'interfaccia funzionale MioNumero
:
// Crea un riferimento a un'istanza di MioNumero.
MioNumero mioNum;
Successivamente, un'espressione lambda viene assegnata a quel riferimento di interfaccia:
// Assegna un'espressione lambda a mioNum.
mioNum = () -> 123.45;
Quando un'espressione lambda viene usata in un'espressione di assegnazione, viene automaticamente creata un'istanza di una classe che implementa l'interfaccia funzionale, con l'espressione lambda che definisce il comportamento del metodo astratto dichiarato dall'interfaccia funzionale. Questo è il punto chiave.
Quando quel metodo viene chiamato attraverso il riferimento, l'espressione lambda viene eseguita. Quindi, un'espressione lambda ci dà un modo per trasformare un segmento di codice in un oggetto.
Nell'esempio precedente, l'espressione lambda diventa l'implementazione per il metodo ottieniValore()
. Di conseguenza, il seguente codice visualizza il valore 123.45:
// Chiama ottieniValore(),
// che è implementato dall'espressione lambda
// precedentemente assegnata.
System.out.println(mioNum.ottieniValore());
Poiché l'espressione lambda assegnata a mioNum
restituisce il valore 123.45, quello è il valore ottenuto quando ottieniValore()
viene chiamato.
Affinché un'espressione lambda possa essere usata in un assegnamento, il tipo del metodo astratto e il tipo dell'espressione lambda devono essere compatibili. Ad esempio, se il metodo astratto specifica due parametri int
, allora la lambda deve specificare due parametri il cui tipo sia esplicitamente int
o possa essere implicitamente inferito come int
dal contesto. In generale, il tipo e il numero dei parametri dell'espressione lambda devono essere compatibili con i parametri del metodo; i tipi di ritorno devono essere compatibili; e qualsiasi eccezione lanciata dall'espressione lambda deve essere accettabile per il metodo.
Esempi di Espressioni Lambda
Tenendo presente la discussione precedente, esaminiamo alcuni esempi semplici che illustrano i concetti di base delle espressioni lambda.
Il primo esempio mette insieme i pezzi mostrati nella sezione precedente.
// Dimostra una semplice espressione lambda.
// Definiamo prima un'interfaccia funzionale.
interface MioNumero {
double ottieniValore();
}
// Ora, usiamo un'espressione lambda per implementare
// il metodo ottieniValore() di MioNumero.
class DemoLambda {
public static void main(String[] args)
{
// Crea un riferimento a un'istanza di MioNumero.
MioNumero mioNum;
// Qui, l'espressione lambda è semplicemente un'espressione costante.
// Quando viene assegnata a mioNum,
// viene costruita un'istanza di una classe
// in cui l'espressione lambda implementa
// il metodo ottieniValore() in MioNumero.
mioNum = () -> 123.45;
// Chiama ottieniValore(), che è fornito dall'espressione lambda
// precedentemente assegnata.
System.out.println("Un valore fisso: " +
mioNum.ottieniValore());
// Qui, viene utilizzata un'espressione più complessa.
mioNum = () -> Math.random() * 100;
// Queste istruzioni chiamano
// l'espressione lambda nella riga precedente.
System.out.println("Un valore casuale: " + mioNum.ottieniValore());
System.out.println("Un altro valore casuale: " + mioNum.ottieniValore());
// Un'espressione lambda deve essere compatibile con il metodo
// definito dall'interfaccia funzionale.
// Pertanto, questa istruzione non funzionerà:
// mioNum = () -> "123.03"; // Errore!
}
}
L'output di esempio del programma è mostrato qui:
Un valore fisso: 123.45
Un valore casuale: 41.232539913446985
Un altro valore casuale: 73.44894370989081
Come menzionato, l'espressione lambda deve essere compatibile con il metodo astratto che è destinata a implementare. Per questa ragione, la riga commentata alla fine del programma precedente è illegale perché un valore di tipo String
non è compatibile con double
, che è il tipo di ritorno richiesto da ottieniValore()
.
L'esempio successivo mostra l'uso di un parametro con un'espressione lambda:
// Dimostra un'espressione lambda che prende un parametro.
// Un'altra interfaccia funzionale.
interface TestNumerico {
boolean test(int n);
}
class DemoLambda2 {
public static void main(String[] args)
{
// Un'espressione lambda che testa se un numero è pari.
TestNumerico pari = (n) -> (n % 2)==0;
if(pari.test(10))
System.out.println("10 è pari");
if(!pari.test(9))
System.out.println("9 non è pari");
// Ora, usa un'espressione lambda che testa se un numero
// non è negativo.
// Questa espressione lambda è compatibile con test().
// Quindi, può essere assegnata a un riferimento TestNumerico.
TestNumerico nonNegativo = (n) -> n >= 0;
if(nonNegativo.test(1))
System.out.println("1 non è negativo");
if(!nonNegativo.test(-1))
System.out.println("-1 è negativo");
}
}
L'output di questo programma è mostrato qui:
10 è pari
9 non è pari
1 non è negativo
-1 è negativo
Questo programma dimostra un fatto chiave sulle espressioni lambda che merita un esame attento. Si presti particolare attenzione all'espressione lambda che esegue il test per la parità.
È mostrata di nuovo qui:
(n) -> (n % 2)==0
Si noti che il tipo di n
non è specificato. Piuttosto, il suo tipo è inferito dal contesto. In questo caso, il suo tipo è inferito dal tipo di parametro di test()
come definito dall'interfaccia TestNumerico
, che è int
. È anche possibile specificare esplicitamente il tipo di un parametro in un'espressione lambda. Per esempio, questo è anche un modo valido per scrivere la precedente espressione lambda:
(int n) -> (n % 2)==0
Qui, n
è specificato esplicitamente come int
. Solitamente non è necessario specificare esplicitamente il tipo, ma si può farlo in quelle situazioni che lo richiedono. A partire da JDK 11, si può anche usare var
per indicare esplicitamente l'inferenza del tipo di variabile locale per un parametro di espressione lambda.
Questo programma dimostra un altro punto importante sulle espressioni lambda: Un riferimento di interfaccia funzionale può essere usato per eseguire qualsiasi espressione lambda che sia compatibile con essa.
Nota che il programma definisce due espressioni lambda diverse che sono compatibili con il metodo test()
dell'interfaccia funzionale TestNumerico
. La prima, chiamata pari
, determina se un valore è pari. La seconda, chiamata nonNegativo
, controlla se un valore non è negativo. In ogni caso, il valore del parametro n
è testato. Poiché ogni espressione lambda è compatibile con test()
, ognuna può essere eseguita attraverso un riferimento TestNumerico
.
Un altro punto prima di procedere. Quando un'espressione lambda ha solo un parametro, non è necessario circondare il nome del parametro con parentesi quando è specificato sul lato sinistro dell'operatore lambda. Per esempio, questo è anche un modo valido per scrivere l'espressione lambda usata nel programma:
n -> (n % 2)==0
Per consistenza, in queste lezioni circonderemo tutte le liste di parametri delle espressioni lambda con parentesi, anche quelle contenenti solo un parametro. Naturalmente, si può adottare uno stile diverso.
Il programma successivo dimostra un'espressione lambda che prende due parametri. In questo caso, l'espressione lambda testa se un numero è un fattore di un altro.
// Dimostra un'espressione lambda che prende due parametri.
// Un'interfaccia funzionale che specifica due parametri.
interface TestNumerico2 {
boolean test(int n, int d);
}
class DemoLambda3 {
public static void main(String[] args)
{
// Questa espressione lambda determina se un numero è
// un fattore di un altro.
TestNumerico2 fattore = (n, d) -> (n % d) == 0;
if(fattore.test(10, 2))
System.out.println("2 è un fattore di 10");
if(!fattore.test(10, 3))
System.out.println("3 non è un fattore di 10");
}
}
L'output è mostrato qui:
2 è un fattore di 10
3 non è un fattore di 10
In questo programma, l'interfaccia funzionale TestNumerico2
definisce il metodo test()
:
boolean test(int n, int d);
In questa versione, test()
specifica due parametri. Quindi, affinché un'espressione lambda sia compatibile con test()
, l'espressione lambda deve anche specificare due parametri. Si noti come sono specificati:
(n, d) -> (n % d) == 0
I due parametri, n
e d
, sono specificati nella lista dei parametri, separati da virgole. Questo esempio può essere generalizzato. Ogni volta che è richiesto più di un parametro, i parametri sono specificati, separati da virgole, in una lista tra parentesi sul lato sinistro dell'operatore lambda.
Ecco un punto importante sui parametri multipli in un'espressione lambda: Se si ha bisogno di dichiarare esplicitamente il tipo di un parametro, allora tutti i parametri devono avere tipi dichiarati. Per esempio, questo è legale:
(int n, int d) -> (n % d) == 0
Ma questo non lo è:
// Errore! Tutti i parametri devono avere tipi dichiarati.
(int n, d) -> (n % d) == 0