W3docs

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:

  1. 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à.
  2. Il corpo espressione restituisce il suo valore implicitamente. Niente return, niente punto e virgola. L'espressione è il risultato.
  3. Il corpo blocco richiede return quando il metodo dell'interfaccia ha un tipo di ritorno. Dimenticarlo è un errore di compilazione, non un null silenzioso.

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 ObjectObject 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 compile

La 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 this si 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.

java— editable, runs on the server

Cosa osservare dall'esecuzione:

  • () -> \"hi\" ha funzionato sia come Callable<String> che come Supplier<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 * factor ha catturato factor per valore. Il compilatore l'ha accettato perché factor non è mai stato riassegnato. Decommentare factor = 11 renderebbe factor una variabile non effectively-final e farebbe fallire la compilazione della lambda.
  • forEach, removeIf e sort accettano 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 describe con corpo blocco richiedeva istruzioni return esplicite perché il suo target (Function<Integer, String>) ha un tipo di ritorno non void. 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.

Pratica

Pratica
Quale di queste espressioni lambda verrà rifiutata dal compilatore Java?
Quale di queste espressioni lambda verrà rifiutata dal compilatore Java?
Was this page helpful?