Espressioni Lambda in Java
Implementazioni inline concise di interfacce funzionali in Java con espressioni lambda: (parametri) -> corpo.
Un'espressione lambda è la sintassi concisa aggiunta da Java 8 per "un'istanza di un'interfaccia che ha esattamente un metodo astratto." Prima di Java 8, questa operazione si scriveva come classe anonima. Dopo, si scrive come lista di parametri, una freccia e un corpo:
Runnable r = () -> System.out.println("hi");
Comparator<String> byLen = (a, b) -> a.length() - b.length();
Function<String, Integer> length = s -> s.length();Non c'è nessun nuovo tipo di valore qui — r, byLen e length sono ancora riferimenti a oggetti, e a runtime ognuno contiene un'istanza di una classe che implementa l'interfaccia a sinistra. La novità è che il codice che dice "creane uno" è abbastanza corto da stare al call site, il che sblocca ogni altro idioma funzionale della parte: predicati di filtro, costruttori di comparatori, gestori di eventi, pipeline di stream.
Le forme sintattiche
Una lambda ha tre parti: lista di parametri, freccia -> e corpo. Ogni parte ha una forma abbreviata:
// Zero parameters: empty parens are required
Runnable r = () -> System.out.println("tick");
// One parameter: parens optional (idiomatic to omit them)
Function<String, Integer> len = s -> s.length();
Function<String, Integer> len2 = (s) -> s.length(); // same thing
// Two or more: parens required
Comparator<String> cmp = (a, b) -> a.length() - b.length();
// Explicit types: rare but legal
BinaryOperator<Integer> add = (Integer a, Integer b) -> a + b;
// Expression body: the value of the expression is the return value
Predicate<Integer> positive = n -> n > 0;
// Block body: explicit `return` required if the interface method returns a value
Function<Integer, String> describe = n -> {
if (n == 0) return "zero";
if (n < 0) return "negative";
return "positive";
};Tre regole collegano tutto:
- I tipi dei parametri vengono solitamente inferiti dal tipo target (l'interfaccia dichiarata al call site). Scriviamoli solo quando il compilatore non riesce a determinarne uno o quando migliorano la leggibilità.
- Il corpo espressione restituisce il suo valore implicitamente. Niente
return, niente punto e virgola. L'espressione è il risultato. - Il corpo blocco richiede
returnquando il metodo dell'interfaccia ha un tipo di ritorno. Dimenticarlo è un errore di compilazione, non unnullsilenzioso.
Target typing — dove possono apparire le lambda
Una lambda non ha un tipo intrinseco. Il compilatore ne determina il tipo dal target — il contesto in cui viene utilizzata:
Runnable r1 = () -> doWork(); // target: Runnable
Callable<Integer> c1 = () -> 42; // target: Callable<Integer>
Supplier<Integer> s1 = () -> 42; // target: Supplier<Integer>() -> 42 è la stessa sorgente in tutti e tre i casi, ma compila in tre diverse istanze di interfaccia. Per questo motivo una lambda non può essere assegnata direttamente a Object — Object o = () -> 42; è ambiguo e il compilatore lo rifiuta. Si fa un cast per disambiguare: Object o = (Supplier<Integer>) () -> 42;.
I target più frequenti:
- Un parametro di metodo tipizzato come interfaccia funzionale:
list.removeIf(s -> s.isEmpty()). - Un campo o variabile locale di tipo interfaccia funzionale:
Predicate<String> empty = String::isEmpty;. - Un tipo di ritorno:
public Supplier<Date> now() { return Date::new; }.
Se non c'è un target, non può esserci una lambda. var f = s -> s.length(); non compila — var non riesce a inferire un tipo target.
Cattura di variabili: "effectively final"
Una lambda può leggere variabili locali dal metodo che la contiene, ma solo se tali variabili sono effectively final — mai riassegnate dopo il valore iniziale:
int multiplier = 3;
IntFunction<Integer> scale = n -> n * multiplier; // OK — `multiplier` never reassigned
multiplier = 4; // <-- this line would make the lambda not compileLa regola è la stessa che le classi anonime interne hanno sempre avuto, e il motivo è lo stesso: una lambda può sopravvivere al metodo in cui è stata definita (potresti salvarla in un campo o passarla a un altro thread), e Java non ha chiusure che catturano la variabile — cattura il valore al momento della costruzione. Consentire la riassegnazione creerebbe un'illusione confusa.
I campi sono un discorso diverso. Una lambda può leggere e modificare liberamente campi di istanza e campi statici:
class Counter {
private int n = 0;
Runnable inc = () -> n++; // legal — `n` is a field, not a local
}Questa è una frequente fonte di bug nel codice con gli stream — una lambda che modifica un campo condiviso sembra innocua, ma va in race condition con se stessa quando lo stream diventa parallelo. Le lambda pure sono più sicure.
this, return e break dentro una lambda
Una lambda non è un nuovo scope per this. All'interno di una lambda, this fa riferimento all'istanza che la contiene — come nel codice circostante:
class Greeter {
String prefix = "Hello, ";
Function<String, String> greet = name -> this.prefix + name; // `this` is the Greeter
}Questa è una delle più importanti differenze pratiche rispetto alle classi anonime, dove this si riferiva all'istanza anonima stessa.
return dentro una lambda restituisce dalla lambda, non dal metodo che la contiene. break e continue non funzionano in una lambda — appartengono al ciclo a cui si riferiscono, e il corpo della lambda non fa parte del ciclo circostante.
Lambda vs classe anonima — quando usare ciascuna
Per le interfacce funzionali, le lambda sono quasi sempre più brevi e chiare. Generano un bytecode leggermente diverso (invokedynamic) e non creano un nuovo file di classe per ogni sito d'uso, quindi di solito sono più leggere anche a runtime.
Usa una classe anonima quando:
- L'interfaccia ha più di un metodo astratto (non è funzionale).
- Hai bisogno di un campo locale al metodo (
int seen = 0;accessibile tra le chiamate). - Hai bisogno che
thissi riferisca all'istanza che stai creando, non all'istanza che la contiene. - Devi sovrascrivere un metodo default per specializzarne il comportamento.
In tutti gli altri casi vince la lambda.
Un esempio completo: cattura, target typing e i quattro call site
Il programma qui sotto illustra i quattro posti più comuni in cui appare una lambda — forEach su collection, removeIf, sort e filter su stream — insieme alle regole di cattura e al target typing.
Cosa osservare dall'esecuzione:
() -> \"hi\"ha funzionato sia comeCallable<String>che comeSupplier<String>— stessa sorgente, target type diversi, istanze di interfaccia diverse. Ecco perché una lambda non ha tipo finché il contesto non ne fornisce uno.times = n -> n * factorha catturatofactorper valore. Il compilatore l'ha accettato perchéfactornon è mai stato riassegnato. Decommentarefactor = 11renderebbefactoruna variabile non effectively-final e farebbe fallire la compilazione della lambda.forEach,removeIfesortaccettano ciascuno una interfaccia funzionale diversa (Consumer,Predicate,Comparator), e la forma della lambda — numero di parametri, presenza di un valore di ritorno — corrispondeva al singolo metodo astratto di ciascuna interfaccia. Il compilatore fa la corrispondenza tramite il target typing.- La lambda
describecon corpo blocco richiedeva istruzionireturnesplicite perché il suo target (Function<Integer, String>) ha un tipo di ritorno nonvoid. Le lambda con corpo espressione sopra di essa restituivano la propria espressione implicitamente.
Cosa c'è dopo
Conosci la sintassi e le regole di cattura. La domanda successiva è: a quale interfaccia, esattamente, compila una lambda? Interfacce funzionali Java introduce la regola del singolo metodo astratto (SAM), l'annotazione @FunctionalInterface e come scrivere la propria interfaccia funzionale per i casi non coperti dalla libreria standard.