Introduzione
I lambda, noti anche come funzioni e chiusure anonime, sono blocchi di codice che possono essere passati ed eseguiti successivamente. I lambda sono presenti in Java e nella maggior parte dei linguaggi di programmazione moderni per consentire la scrittura di codice più conciso con meno standard.
In questo tutorial imparerai a scrivere le tue espressioni lambda. Imparerai anche a utilizzare alcuni Lambda integrati disponibili nel pacchetto java.util.function
. Coprono casi d'uso comuni e semplificano l'avvio con lambda.
Prerequisiti
Per seguire questo tutorial avrai bisogno di:
Java (versione 11 o successiva) installato sul tuo computer, con il compilatore fornito dal Java Development Kit (JDK).
Creare la tua Lambda
Per creare la tua espressione lambda, devi prima comprenderne la struttura. Può essere descritto come segue:
(argument(s)) -> {body}
Ci sono tre parti in un'espressione lambda:
- Un elenco di zero o più argomenti separati da virgole, racchiusi tra parentesi. Il loro tipo (
String
,Integer
, ecc.) può essere dedotto automaticamente dal compilatore in modo da non doverlo dichiarare. - La freccia
->
. Viene utilizzato per collegare i parametri e il corpo dell'espressione lambda. - Un corpo che contiene il codice da eseguire. Se sono presenti argomenti, vengono utilizzati nel corpo per eseguire operazioni con essi.
Informazioni: per seguire il codice di esempio in questo tutorial, apri lo strumento Java Shell sul tuo sistema locale eseguendo il comando jshell
. Quindi puoi copiare, incollare o modificare gli esempi aggiungendoli dopo la richiesta jshell>
e premendo ENTER
. Per uscire jshell
digitare /exit
.
Ecco un esempio di espressione lambda:
(x) -> System.out.println(x)
In questa lambda, x
è il parametro che viene stampato con System.out.println()
metodo. Un buon utilizzo di tale lambda è stampare il contenuto di un elenco fornito dal metodo foreach. Per testarlo, jshell
crea un elenco di animali domestici come questo:
List<String> pets = Arrays.asList("Dog", "Cat")
Vedrai una conferma che la lista è stata creata:
pets ==> [Dog, Cat]
Ora puoi stampare il contenuto della lista utilizzando il metodo foreach
e l'espressione lambda:
pets.forEach((x) -> System.out.println(x))
Vedrai ogni animale domestico stampato su una nuova riga:
Dog
Cat
La lambda (x) -> System.out.println(x)
può essere ulteriormente semplificata perché ha un solo parametro. In questo caso, puoi:
- rimuovere le parentesi attorno al parametro
x
. - rimuovere
x
del tutto il parametro e utilizzare solo il riferimento al metodoSystem.out::println
. I riferimenti ai metodi sono una notazione abbreviata per un'espressione lambda per chiamare un metodo. Sono considerati più leggibili e consigliati dalle migliori pratiche del codice pulito.
Pertanto, lo stesso codice può essere riscritto come:
pets.forEach(System.out::println)
Tuttavia, non tutte le lambda sono così semplici e possono essere montate su un'unica linea. Probabilmente dovrai utilizzare lambda con più parametri e il blocco di esecuzione sarà su più di una sola riga. È allora che la sintassi diventa più complessa. Ad esempio, aggiungiamo anche il numero di animali durante la stampa:
pets.forEach(x -> {
System.out.println("Pet name: " + x);
System.out.println("Pet number: " + pets.indexOf(x));
});
Nota come l'espressione lambda è ora racchiusa tra parentesi graffe aggiuntive {}
perché si trova su più di una riga. Inoltre, per ottenere l'indice dell'animale nell'elenco, devi utilizzare il metodo indexOf
dell'elenco e passargli il nome dell'animale come parametro pets.indexOf(x)
.
Il codice sopra produrrà il seguente output:
Pet name: Dog
Pet number: 0
Pet name: Cat
Pet number: 1
Ecco come puoi scrivere le tue espressioni lambda in Java. Nella sezione successiva imparerai come utilizzare le lambda integrate disponibili nel pacchetto java.util.function
.
Utilizzo di Lambda integrati
Il pacchetto java.util.function
contiene una serie di interfacce funzionali che coprono casi d'uso comuni. Per utilizzare una di queste interfacce è necessario fornire l'implementazione del suo metodo astratto. Vediamo come è possibile farlo per alcune delle interfacce più utili.
Predicati
predicate
è un lambda per testare una condizione. Ha un metodo test
che restituisce un valore booleano: vero se la condizione è soddisfatta e falso in caso contrario. Ad esempio, puoi verificare se i valori di un elenco di stringhe iniziano con una lettera specifica. Usiamo nuovamente l'elenco degli animali domestici e stampiamo il nome di ciascun animale domestico che inizia con la lettera "D":
Predicate<String> filterPets = x -> x.startsWith("D");
pets.stream().filter(t -> filterPets.test(t)).forEach(System.out::println);
Il codice può essere scomposto come segue:
- Nella prima riga crei un tipo di predicato per i valori stringa chiamato
filterPets
. Il tipo di stringa è definito all'interno dell'operatore diamante<>
. - Con il segno uguale il predicato viene assegnato a un'espressione lambda che controlla se l'argomento passato
x
inizia con la lettera D. - Nella seconda riga inizi iterando l'elenco degli animali domestici utilizzando il suo
stream()
metodo. - Successivamente nella seconda riga segue il
filter
metodo utilizzato per filtrare i valori in base a un predicato. Il definito in precedenzafilterPets
viene passato a questo metodo per questo scopo. - Infine, utilizzi il
forEach
metodo per scorrere i valori filtrati e stamparli ciascuno utilizzando il metodo referenceSystem.out::println
.
L'output jshell
sarà simile al seguente:
filterPets ==> $Lambda$32/0x0000...
Dog
La prima riga può essere ignorata. Conferma che un riferimento filterPets
è stato creato e viene fornita la sua posizione di memoria. Vedrai tali righe jshell
quando dichiari lambda. Puoi ignorarli per ora perché sono utili solo per il debug.
La seconda riga stampa il nome dell'unico animale domestico che inizia con la lettera D - Cane.
L'esempio esplicito sopra è utile per apprendere e comprendere come funziona predicate
. Tuttavia, in pratica, può essere scritto in modo molto più semplice. Una volta definito predicate
, puoi usarlo direttamente senza passargli esplicitamente argomenti in un lambda. Questo perché ogni predicato, proprio come ogni lambda, ha un solo metodo. Java può capire automaticamente che questo metodo deve essere chiamato con l'unico parametro disponibile. Quindi lo stesso codice può essere riscritto come:
Predicate<String> filterPets = x -> x.startsWith("D");
pets.stream().filter(filterPets).forEach(System.out::println);
Definire il predicato separatamente e assegnarlo a un oggetto ha senso filterPets
se si intende utilizzarlo più di una volta nel codice. Tuttavia, se filtrerai gli animali domestici in base alla loro prima lettera solo una volta, avrà più senso scrivere il predicato direttamente nel metodo di filtro in questo modo:
pets.stream().filter(x -> x.startsWith("D")).forEach(System.out::println);
In tutti i casi, l'output sarà lo stesso. La differenza sta nel quanto esplicito e riutilizzabile vuoi che sia il tuo codice. Questo sarà valido per il resto degli esempi in questo tutorial.
Consumatori
consumer
viene utilizzato per consumare un valore. Ha un metodo accept
che restituisce void, cioè non restituisce nulla. Senza rendertene conto, quando hai creato la tua prima lambda per stampare l'elenco degli animali domestici, hai implementato un file consumer
.
Definiamo un consumatore in modo più esplicito in questo modo:
Consumer<String> printPet = x -> System.out.println(x);
pets.forEach(printPet);
Come previsto, l'output elencherà i due animali domestici in questo modo:
Dog
Cat
Proprio come nel predicato lambda, quanto sopra è il modo più esplicito per definire e utilizzare un consumatore in modo da poterlo comprendere al meglio.
Funzioni
function
viene utilizzato per trasformare un valore passato come argomento. Ha un metodo apply
che restituisce il valore trasformato. Ad esempio, puoi utilizzare una funzione per trasformare ciascuno degli animali domestici e renderlo in maiuscolo:
Function<String, String> toUpperCase = x -> x.toUpperCase();
pets.stream().map(x -> toUpperCase.apply(x)).forEach(System.out::println);
Quando si definisce a function
è necessario specificare i tipi di input e output. Nel nostro esempio, entrambe sono stringhe. Ecco perché la prima riga inizia con Function<String, String>
per dichiarare il riferimento toUpperCase
. Dopo il segno di uguale segue l'espressione lambda in cui il metodo stringa toUpperCase
gioca il ruolo chiave modificando la stringa di input in maiuscolo.
Sulla seconda riga viene trasmesso in streaming l'elenco degli animali domestici e map
viene richiamato il metodo. Questo metodo viene utilizzato per trasformare ogni animale domestico in maiuscolo con la lambda toUpperCase
. Infine, usi il metodo forEach
per stampare ciascun animale domestico. L' jshell
output sarà:
DOG
CAT
Fornitori
supplier
viene utilizzato per fornire un valore al chiamante e non accetta argomenti. Ha un metodo get
che restituisce il valore desiderato. Ecco un supplier
esempio che fornisce l'ora corrente:
Supplier<java.time.LocalTime> timeSupplier = () -> java.time.LocalTime.now();
System.out.println(timeSupplier.get());
Nella prima riga si definisce un fornitore per <java.time.LocalTime>
i tipi denominati timeSupplier
. È assegnato a un lambda che non ha parametri e il suo codice di esecuzione restituisce l'ora corrente fornita dal metodo integrato java.time.LocalTime.now
. Infine, utilizzi il get
metodo per ottenere il valore del lambda timeSupplier e stamparlo con System.out.println
.
Quando esegui il codice sopra jshell
vedrai l'ora corrente stampata in questo modo:
21:52:38.384278
Operatori unari
Analogamente function
, unaryOperator
anche a viene utilizzato per trasformare un valore passato come argomento. Tuttavia, con a unaryOperator
i tipi di input e output sono sempre gli stessi. Viene chiamato anche il suo metodo apply
e restituisce il valore trasformato. Ecco come puoi utilizzare unaryOperator
per trasformare una stringa come "dog" in maiuscolo:
UnaryOperator<String> toUpperCase = x -> x.toUpperCase();
System.out.println(toUpperCase.apply("dog"));
Nell'esempio precedente, definisci una unaryOperator
chiamata toUpperCase
che accetta una stringa e restituisce un'altra stringa in maiuscolo. Quindi usi il metodo apply
per trasformare la stringa "dog" in "DOG" e stamparla con System.out.println
. Quando esegui il codice sopra jshell
vedrai la stringa "DOG" stampata in questo modo:
DOG
Operatori binari
binaryOperator
viene utilizzato per combinare due valori. Ha anche un metodo apply
che restituisce il valore combinato. Ad esempio, puoi creare una calcolatrice che somma due numeri come questo:
BinaryOperator<Integer> addition = (x, y) -> x + y;
System.out.println(add.apply(2, 3));
La prima riga definisce una binaryOperator
chiamata addition
che accetta argomenti interi e restituisce la loro somma. Quindi usi il apply
metodo per sommare i numeri 2 e 3 e stampare il risultato con System.out.println
. Quando esegui il codice sopra jshell
vedrai:
add ==> $Lambda$29/0x000...
5
In questo modo puoi utilizzare le interfacce lambda integrate disponibili nel pacchetto java.util.function
per realizzare utili implementazioni e creare utili snippet di codice.
Conclusione
In questo tutorial hai imparato come scrivere le tue espressioni lambda in Java. Hai anche imparato come utilizzare le funzioni lambda integrate disponibili nel pacchetto java.util.function
che copre scenari comuni. Ora puoi utilizzare questi lambda per rendere il tuo codice più pulito e leggibile.