Inizia con le espressioni lambda in Java

Prima di Java SE 8, le classi anonime venivano generalmente utilizzate per passare la funzionalità a un metodo. Questa pratica ha offuscato il codice sorgente, rendendolo più difficile da capire. Java 8 ha eliminato questo problema introducendo lambda. Questo tutorial introduce prima la funzionalità del linguaggio lambda, quindi fornisce un'introduzione più dettagliata alla programmazione funzionale con espressioni lambda insieme ai tipi di destinazione. Potrai anche imparare lambda interagiscono con gli ambiti, le variabili locali, le thise superle parole chiave, e le eccezioni Java. 

Nota che gli esempi di codice in questo tutorial sono compatibili con JDK 12.

Alla scoperta dei tipi per te stesso

Non introdurrò funzionalità del linguaggio non lambda in questo tutorial che non hai imparato in precedenza, ma dimostrerò lambda tramite tipi che non ho discusso in precedenza in questa serie. Un esempio è la java.lang.Mathclasse. Introdurrò questi tipi nei futuri tutorial di Java 101. Per ora, suggerisco di leggere la documentazione dell'API JDK 12 per saperne di più su di loro.

download Ottieni il codice Scarica il codice sorgente per applicazioni di esempio in questo tutorial. Creato da Jeff Friesen per JavaWorld.

Lambdas: un primer

Un'espressione lambda (lambda) descrive un blocco di codice (una funzione anonima) che può essere passato ai costruttori o metodi per la successiva esecuzione. Il costruttore o il metodo riceve lambda come argomento. Considera il seguente esempio:

() -> System.out.println("Hello")

Questo esempio identifica un lambda per inviare un messaggio al flusso di output standard. Da sinistra a destra, ()identifica l'elenco di parametri formali di lambda (non ci sono parametri nell'esempio), ->indica che l'espressione è un lambda ed System.out.println("Hello")è il codice da eseguire.

Lambda semplifica l'uso delle interfacce funzionali , che sono interfacce annotate che dichiarano ciascuna esattamente un metodo astratto (sebbene possano anche dichiarare qualsiasi combinazione di metodi predefiniti, statici e privati). Ad esempio, la libreria di classi standard fornisce java.lang.Runnableun'interfaccia con un unico void run()metodo astratto . La dichiarazione di questa interfaccia funzionale appare di seguito:

@FunctionalInterface public interface Runnable { public abstract void run(); }

La libreria di classi annota Runnablecon @FunctionalInterface, che è un'istanza del java.lang.FunctionalInterfacetipo di annotazione. FunctionalInterfaceviene utilizzato per annotare quelle interfacce che devono essere utilizzate nei contesti lambda.

Un lambda non ha un tipo di interfaccia esplicito. Invece, il compilatore usa il contesto circostante per dedurre quale interfaccia funzionale istanziare quando viene specificato un lambda: il lambda è associato a tale interfaccia. Ad esempio, supponiamo di aver specificato il seguente frammento di codice, che passa il lambda precedente come argomento al costruttore java.lang.Threaddella classe Thread(Runnable target):

new Thread(() -> System.out.println("Hello"));

Il compilatore determina che il lambda viene passato Thread(Runnable r)perché questo è l'unico costruttore che soddisfa il lambda: Runnableè un'interfaccia funzionale, l'elenco di parametri formali vuoto di lambda ()corrisponde run()all'elenco di parametri vuoto e anche i tipi restituiti ( void) sono d'accordo. Il lambda è vincolato a Runnable.

Il listato 1 presenta il codice sorgente a una piccola applicazione che ti consente di giocare con questo esempio.

Listato 1. LambdaDemo.java (versione 1)

public class LambdaDemo { public static void main(String[] args) { new Thread(() -> System.out.println("Hello")).start(); } }

Compila il Listato 1 ( javac LambdaDemo.java) ed esegui application ( java LambdaDemo). Dovresti osservare il seguente output:

Hello

Lambdas può semplificare notevolmente la quantità di codice sorgente che è necessario scrivere e può anche rendere il codice sorgente molto più facile da capire. Ad esempio, senza lambda, probabilmente specifichi il codice più dettagliato del Listato 2, che si basa su un'istanza di una classe anonima che implementa Runnable.

Listato 2. LambdaDemo.java (versione 2)

public class LambdaDemo { public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { System.out.println("Hello"); } }; new Thread(r).start(); } }

Dopo aver compilato questo codice sorgente, esegui l'applicazione. Scoprirai lo stesso output mostrato in precedenza.

Lambdas e l'API Streams

Oltre a semplificare il codice sorgente, le lambda svolgono un ruolo importante nell'API Streams orientata alla funzionalità di Java. Descrivono unità di funzionalità passate a vari metodi API.

Java lambda in profondità

Per utilizzare le espressioni lambda in modo efficace, è necessario comprendere la sintassi delle espressioni lambda insieme alla nozione di tipo di destinazione. È inoltre necessario capire come lambda interagire con gli ambiti, le variabili locali, le thise superle parole chiave, e le eccezioni. Tratterò tutti questi argomenti nelle sezioni che seguono.

Come vengono implementati i lambda

I Lambda sono implementati in termini di invokedynamicistruzioni della macchina virtuale Java e java.lang.invokeAPI. Guarda il video Lambda: A Peek Under the Hood per conoscere l'architettura lambda.

Sintassi Lambda

Ogni lambda è conforme alla seguente sintassi:

( formal-parameter-list ) -> { expression-or-statements }

La formal-parameter-listè un elenco separato da virgole di parametri formali, che devono corrispondere i parametri del singolo metodo astratto di un'interfaccia funzionale in fase di esecuzione. Se ometti i loro tipi, il compilatore deduce questi tipi dal contesto in cui viene utilizzato lambda. Considera i seguenti esempi:

(double a, double b) // types explicitly specified (a, b) // types inferred by compiler

Lambdas e var

A partire da Java SE 11, puoi sostituire il nome di un tipo con var. Ad esempio, potresti specificare (var a, var b).

È necessario specificare le parentesi per più parametri formali o non. Tuttavia, puoi omettere le parentesi (sebbene non sia necessario) quando specifichi un singolo parametro formale. (Questo vale solo per il nome del parametro: le parentesi sono obbligatorie quando viene specificato anche il tipo.) Considera i seguenti esempi aggiuntivi:

x // parentheses omitted due to single formal parameter (double x) // parentheses required because type is also present () // parentheses required when no formal parameters (x, y) // parentheses required because of multiple formal parameters

Il formal-parameter-listè seguito da un ->token, che è seguito da expression-or-statements- un'espressione o un blocco di istruzioni (noto come corpo lambda). A differenza dei corpi basati su espressioni, i corpi basati su istruzioni devono essere posizionati tra i caratteri parentesi graffe open ( {) e close ( }):

(double radius) -> Math.PI * radius * radius radius -> { return Math.PI * radius * radius; } radius -> { System.out.println(radius); return Math.PI * radius * radius; }

Il corpo lambda basato su espressioni del primo esempio non deve essere posizionato tra parentesi graffe. Il secondo esempio converte il corpo basato su espressioni in un corpo basato su istruzioni, in cui returndeve essere specificato per restituire il valore dell'espressione. L'esempio finale mostra più istruzioni e non può essere espresso senza le parentesi graffe.

Corpi lambda e punto e virgola

Notare l'assenza o la presenza di punto e virgola ( ;) negli esempi precedenti. In ogni caso, il corpo lambda non termina con un punto e virgola perché lambda non è un'istruzione. Tuttavia, all'interno di un corpo lambda basato su istruzioni, ogni istruzione deve essere terminata con un punto e virgola.

Il Listato 3 presenta una semplice applicazione che dimostra la sintassi lambda; nota che questo elenco si basa sui due esempi di codice precedenti.

Listato 3. LambdaDemo.java (versione 3)

@FunctionalInterface interface BinaryCalculator { double calculate(double value1, double value2); } @FunctionalInterface interface UnaryCalculator { double calculate(double value); } public class LambdaDemo { public static void main(String[] args) { System.out.printf("18 + 36.5 = %f%n", calculate((double v1, double v2) -> v1 + v2, 18, 36.5)); System.out.printf("89 / 2.9 = %f%n", calculate((v1, v2) -> v1 / v2, 89, 2.9)); System.out.printf("-89 = %f%n", calculate(v -> -v, 89)); System.out.printf("18 * 18 = %f%n", calculate((double v) -> v * v, 18)); } static double calculate(BinaryCalculator calc, double v1, double v2) { return calc.calculate(v1, v2); } static double calculate(UnaryCalculator calc, double v) { return calc.calculate(v); } }

Il Listato 3 introduce innanzitutto le interfacce funzionali BinaryCalculatore i UnaryCalculatorcui calculate()metodi eseguono calcoli su due argomenti di input o su un singolo argomento di input, rispettivamente. Questo elenco introduce anche una LambdaDemoclasse il cui main()metodo dimostra queste interfacce funzionali.

The functional interfaces are demonstrated in the static double calculate(BinaryCalculator calc, double v1, double v2) and static double calculate(UnaryCalculator calc, double v) methods. The lambdas pass code as data to these methods, which are received as BinaryCalculator or UnaryCalculator instances.

Compile Listing 3 and run the application. You should observe the following output:

18 + 36.5 = 54.500000 89 / 2.9 = 30.689655 -89 = -89.000000 18 * 18 = 324.000000

Target types

A lambda is associated with an implicit target type, which identifies the type of object to which a lambda is bound. The target type must be a functional interface that's inferred from the context, which limits lambdas to appearing in the following contexts:

  • Variable declaration
  • Assignment
  • Return statement
  • Array initializer
  • Method or constructor arguments
  • Lambda body
  • Ternary conditional expression
  • Cast expression

Listing 4 presents an application that demonstrates these target type contexts.

Listato 4. LambdaDemo.java (versione 4)

import java.io.File; import java.io.FileFilter; import java.nio.file.Files; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.FileVisitor; import java.nio.file.FileVisitResult; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.Callable; public class LambdaDemo { public static void main(String[] args) throws Exception { // Target type #1: variable declaration Runnable r = () -> { System.out.println("running"); }; r.run(); // Target type #2: assignment r = () -> System.out.println("running"); r.run(); // Target type #3: return statement (in getFilter()) File[] files = new File(".").listFiles(getFilter("txt")); for (int i = 0; i  path.toString().endsWith("txt"), (path) -> path.toString().endsWith("java") }; FileVisitor visitor; visitor = new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attribs) { Path name = file.getFileName(); for (int i = 0; i  System.out.println("running")).start(); // Target type #6: lambda body (a nested lambda) Callable callable = () -> () -> System.out.println("called"); callable.call().run(); // Target type #7: ternary conditional expression boolean ascendingSort = false; Comparator cmp; cmp = (ascendingSort) ? (s1, s2) -> s1.compareTo(s2) : (s1, s2) -> s2.compareTo(s1); List cities = Arrays.asList("Washington", "London", "Rome", "Berlin", "Jerusalem", "Ottawa", "Sydney", "Moscow"); Collections.sort(cities, cmp); for (int i = 0; i < cities.size(); i++) System.out.println(cities.get(i)); // Target type #8: cast expression String user = AccessController.doPrivileged((PrivilegedAction) () -> System.getProperty("user.name")); System.out.println(user); } static FileFilter getFilter(String ext) { return (pathname) -> pathname.toString().endsWith(ext); } }