20. Programmazione Funzionale in Java
Indice
La programmazione funzionale è un paradigma di programmazione che si concentra sul descrivere cosa deve essere fatto, piuttosto che come deve essere fatto.
A partire da Java 8, il linguaggio ha aggiunto diverse funzionalità che abilitano uno stile di programmazione “funzionale”: lambda expressions, functional interfaces e method references.
Queste funzionalità permettono agli sviluppatori di scrivere codice più espressivo, conciso e riutilizzabile, soprattutto quando si lavora con collezioni, API di concorrenza e sistemi event-driven.
20.1 Interfacce Funzionali
In Java, un’interfaccia funzionale è un’interfaccia che contiene esattamente un solo metodo astratto.
Le interfacce funzionali abilitano Lambda Expressions e Method References, formando il nucleo del modello di programmazione funzionale di Java.
Note
Java tratta automaticamente come interfaccia funzionale qualsiasi interfaccia con un solo metodo astratto. L’annotazione @FunctionalInterface è opzionale ma consigliata.
20.1.1 Regole per le Interfacce Funzionali
- Esattamente un metodo astratto (SAM = Single Abstract Method).
- Le interfacce possono dichiarare un numero qualsiasi di metodi default, static o private.
- Possono fare override dei metodi di
Object(toString(),equals(Object),hashCode()) senza influenzare il conteggio SAM. - Il metodo funzionale può provenire da una superinterfaccia.
Esempio:
@FunctionalInterface
interface Adder {
int add(int a, int b); // single abstract method
static void info() {}
default void log() {}
}
20.1.2 Interfacce Funzionali Comuni (java.util.function)
Di seguito un riepilogo delle interfacce funzionali più importanti.
| Functional Interface | Returns | Method | Parameters |
|---|---|---|---|
Supplier<T> |
T | get() | 0 |
Consumer<T> |
void | accept(T) | 1 |
BiConsumer<T,U> |
void | accept(T,U) | 2 |
Function<T,R> |
R | apply(T) | 1 |
BiFunction<T,U,R> |
R | apply(T,U) | 2 |
UnaryOperator<T> |
T | apply(T) | 1 (stessi tipi) |
BinaryOperator<T> |
T | apply(T,T) | 2 (stessi tipi) |
Predicate<T> |
boolean | test(T) | 1 |
BiPredicate<T,U> |
boolean | test(T,U) | 2 |
- Esempi
Supplier<String> sup = () -> "Hello!";
Consumer<String> printer = s -> System.out.println(s);
Function<String, Integer> length = s -> s.length();
UnaryOperator<Integer> square = x -> x * x;
Predicate<Integer> positive = x -> x > 0;
20.1.3 Metodi di Comodità nelle Interfacce Funzionali
Molte interfacce funzionali includono metodi di supporto che consentono chaining e composizione.
| Interface | Method | Description |
|---|---|---|
| Function | andThen() | applica la funzione, poi l’altra specificata in questo metodo addizionale |
| Function | compose() | applica la funzione specificata nel metodo addizionale, poi la funzione |
| Function | identity() | restituisce una funzione x -> x |
| Predicate | and() | AND logico |
| Predicate | or() | OR logico |
| Predicate | negate() | NOT logico |
| Consumer | andThen() | concatena consumer |
| BinaryOperator | minBy() | minimo basato su comparator |
| BinaryOperator | maxBy() | massimo basato su comparator |
- Esempi
Function<Integer, Integer> times2 = x -> x * 2;
Function<Integer, Integer> plus3 = x -> x + 3;
var result1 = times2.andThen(plus3).apply(5); // (5*2)+3 = 13
var result2 = times2.compose(plus3).apply(5); // (5+3)*2 = 16
Predicate<String> longString = s -> s.length() > 5;
Predicate<String> startsWithA = s -> s.startsWith("A");
boolean ok = longString.and(startsWithA).test("Amazing"); // true
20.1.4 Interfacce Funzionali Primitive
Java fornisce versioni specializzate delle interfacce funzionali per i tipi primitivi, per evitare overhead di boxing/unboxing.
| Functional Interface | Return Type | Single Abstract Method | # Parameters |
|---|---|---|---|
| IntSupplier | int | getAsInt() | 0 |
| LongSupplier | long | getAsLong() | 0 |
| DoubleSupplier | double | getAsDouble() | 0 |
| BooleanSupplier | boolean | getAsBoolean() | 0 |
| IntConsumer | void | accept(int) | 1 (int) |
| LongConsumer | void | accept(long) | 1 (long) |
| DoubleConsumer | void | accept(double) | 1 (double) |
| IntPredicate | boolean | test(int) | 1 (int) |
| LongPredicate | boolean | test(long) | 1 (long) |
| DoublePredicate | boolean | test(double) | 1 (double) |
| IntUnaryOperator | int | applyAsInt(int) | 1 (int) |
| LongUnaryOperator | long | applyAsLong(long) | 1 (long) |
| DoubleUnaryOperator | double | applyAsDouble(double) | 1 (double) |
| IntBinaryOperator | int | applyAsInt(int, int) | 2 (int,int) |
| LongBinaryOperator | long | applyAsLong(long, long) | 2 (long,long) |
| DoubleBinaryOperator | double | applyAsDouble(double,double) | 2 |
| IntFunction |
R | apply(int) | 1 (int) |
| LongFunction |
R | apply(long) | 1 (long) |
| DoubleFunction |
R | apply(double) | 1 (double) |
| ToIntFunction |
int | applyAsInt(T) | 1 (T) |
| ToLongFunction |
long | applyAsLong(T) | 1 (T) |
| ToDoubleFunction |
double | applyAsDouble(T) | 1 (T) |
| ToIntBiFunction |
int | applyAsInt(T,U) | 2 (T,U) |
| ToLongBiFunction |
long | applyAsLong(T,U) | 2 (T,U) |
| ToDoubleBiFunction |
double | applyAsDouble(T,U) | 2 (T,U) |
| ObjIntConsumer |
void | accept(T,int) | 2 (T,int) |
| ObjLongConsumer |
void | accept(T,long) | 2 (T,long) |
| ObjDoubleConsumer |
void | accept(T,double) | 2 (T,double) |
| DoubleToIntFunction | int | applyAsInt(double) | 1 |
| DoubleToLongFunction | long | applyAsLong(double) | 1 |
| IntToDoubleFunction | double | applyAsDouble(int) | 1 |
| IntToLongFunction | long | applyAsLong(int) | 1 |
| LongToDoubleFunction | double | applyAsDouble(long) | 1 |
| LongToIntFunction | int | applyAsInt(long) | 1 |
- Esempio
IntSupplier dice = () -> (int)(Math.random() * 6) + 1;
IntPredicate even = x -> x % 2 == 0;
IntUnaryOperator doubleIt = x -> x * 2;
20.1.5 Riepilogo
- Le interfacce funzionali contengono esattamente un metodo astratto (SAM).
- Sono alla base di Lambda e Method References.
- Java offre molte FI built-in in
java.util.function. - Le varianti primitive migliorano le performance rimuovendo il boxing.
20.2 Espressioni Lambda
Una lambda expression è un modo compatto di scrivere una funzione.
Le lambda expressions offrono un modo conciso per definire implementazioni di interfacce funzionali.
Una lambda è essenzialmente un piccolo blocco di codice che accetta parametri e restituisce un valore, senza richiedere una dichiarazione completa di metodo.
Rappresentano il comportamento come dato e sono un elemento chiave del modello di programmazione funzionale in Java.
20.2.1 Sintassi delle Espressioni Lambda
La sintassi generale è:
(parameters) -> expression
oppure
(parameters) -> { statements }
Important
Un’espressione lambda non introduce un nuovo scope per le variabili. Di conseguenza, i nomi di variabili già esistenti nel contesto circostante non possono essere ridefiniti come parametri dell’espressione lambda.
20.2.2 Esempi di Sintassi Lambda
Zero parametri
Runnable r = () -> System.out.println("Hello");
Un parametro (parentesi opzionali)
Consumer<String> c = s -> System.out.println(s);
Più parametri
BinaryOperator<Integer> add = (a, b) -> a + b;
Con block body
Function<Integer, String> f = (x) -> {
int doubled = x * 2;
return "Value: " + doubled;
};
20.2.3 Regole per le Espressioni Lambda
- I tipi dei parametri possono essere omessi (type inference).
- Se un parametro ha un tipo, allora tutti i parametri devono specificare il tipo.
- Un singolo parametro non richiede parentesi.
- Più parametri richiedono le parentesi.
- Se il corpo è una singola espressione (senza
{ }),returnnon è consentito; l’espressione stessa è il valore di ritorno. - Se il corpo usa
{ }(un blocco),returndeve comparire se viene restituito un valore. - Le lambda possono essere assegnate solo a interfacce funzionali (tipi SAM).
20.2.4 Inferenza di Tipo
Il compilatore deduce il tipo della lambda dal contesto dell’interfaccia funzionale target.
Predicate<String> p = s -> s.isEmpty(); // s inferito come String
Se il compilatore non riesce a inferire il tipo, devi specificarlo esplicitamente.
BiFunction<Integer, Integer, Integer> f = (Integer a, Integer b) -> a * b;
20.2.5 Restrizioni nei Corpi delle Lambda
Le lambda possono catturare solo variabili locali che sono final o effectively final (non riassegnate).
int x = 10;
Runnable r = () -> {
// x++; // ❌ errore di compilazione — x deve essere effectively final
System.out.println(x);
};
Possono invece modificare lo stato di un oggetto (solo i riferimenti devono essere effectively final).
var list = new ArrayList<>();
Runnable r2 = () -> list.add("OK"); // consentito
20.2.6 Regole sul Tipo di Ritorno
Se il corpo è un’espressione: l’espressione è il valore di ritorno.
Function<Integer, Integer> f = x -> x * 2;
Se il corpo è un blocco: devi includere return.
Function<Integer, Integer> g = x -> {
return x * 2;
};
20.2.7 Lambda vs Classi Anonime
- Le lambda NON creano un nuovo scope: condividono lo scope contenitore.
thisdentro una lambda si riferisce all’oggetto contenitore, non alla lambda.
class Test {
void run() {
Runnable r = () -> System.out.println(this.toString());
}
}
Nelle classi anonime, this si riferisce all’istanza della classe anonima.
20.2.8 Errori Comuni nelle Lambda
Tipi di ritorno incoerenti
x -> { if (x > 0) return 1; } // ❌ manca return per il caso negativo
Mescolare parametri tipizzati e non tipizzati
(a, int b) -> a + b // ❌ illegale
Restituire un valore da una lambda con target void
Runnable r = () -> 5; // ❌ Runnable.run() restituisce void
Risoluzione di overload ambigua
void m(IntFunction<Integer> f) {}
void m(Function<Integer, Integer> f) {}
m(x -> x + 1); // ❌ ambiguo
20.3 Riferimenti a Metodi
I riferimenti a metodi (method references) forniscono una sintassi abbreviata per usare un metodo esistente come implementazione di un’interfaccia funzionale.
Sono equivalenti alle lambda expressions, ma più concisi, leggibili e spesso preferibili quando il metodo target esiste già.
Esistono quattro categorie di method references in Java:
- Riferimento a un metodo statico (
ClassName::staticMethod) - Riferimento a un metodo d’istanza di un oggetto specifico (
instance::method) - Riferimento a un metodo d’istanza di un oggetto arbitrario di un dato tipo (
ClassName::instanceMethod) - Riferimento a un costruttore (
ClassName::new)
20.3.1 Riferimento a un Metodo Statico
Un method reference statico sostituisce una lambda che invoca un metodo statico.
class Utils {
static int square(int x) { return x * x; }
}
Function<Integer, Integer> f1 = x -> Utils.square(x);
Function<Integer, Integer> f2 = Utils::square; // method reference
Sia f1 che f2 si comportano in modo identico.
20.3.2 Riferimento a un Metodo d’Istanza di un Oggetto Specifico
Usato quando hai già un’istanza di un oggetto e vuoi riferirti a uno dei suoi metodi.
String prefix = "Hello, ";
UnaryOperator<String> op1 = s -> prefix.concat(s);
UnaryOperator<String> op2 = prefix::concat; // method reference
System.out.println(op2.apply("World"));
Il riferimento prefix::concat lega concat a quell’oggetto specifico.
20.3.3 Riferimento a un Metodo d’Istanza di un Oggetto Arbitrario di un Dato Tipo
Questa è la forma più “insidiosa”.
Il primo parametro dell’interfaccia funzionale diventa il receiver del metodo (this).
BiPredicate<String, String> p1 = (s1, s2) -> s1.equals(s2);
BiPredicate<String, String> p2 = String::equals; // method reference
System.out.println(p2.test("abc", "abc")); // true
Note
Questa forma applica il metodo al primo argomento della lambda.
20.3.4 Riferimento a un Costruttore
I constructor references sostituiscono lambda che invocano new.
Supplier<ArrayList<String>> sup1 = () -> new ArrayList<>();
Supplier<ArrayList<String>> sup2 = ArrayList::new; // method reference
Function<Integer, ArrayList<String>> sup3 = ArrayList::new;
// invoca il costruttore ArrayList(int capacity)
20.3.5 Tabella Riassuntiva dei Tipi di Method Reference
La tabella seguente riassume tutte le categorie di method reference.
| Type | Syntax Example | Equivalent Lambda |
|---|---|---|
| Static method | Class::staticMethod | x -> Class.staticMethod(x) |
| Instance method of specific object | instance::method | x -> instance.method(x) |
| Instance method of arbitrary object | Class::method | (obj, x) -> obj.method(x) |
| Constructor | Class::new | () -> new Class() |
20.3.6 Errori Comuni
- Un method reference deve combaciare esattamente con la signature dell’interfaccia funzionale.
- Gli overload possono rendere i method references ambigui.
- Il riferimento a metodo d’istanza (
Class::method) sposta il receiver al parametro 1. - Un constructor reference fallisce se non esiste un costruttore compatibile.
// ❌ Ambiguo: quale println()? (println(int), println(String)...)
Consumer<String> c = System.out::println; // OK solo perché il parametro FI è String
// ❌ Costruttore non compatibile: interfaccia funzionale errata
Supplier<Integer> s = Integer::new; // ✔ OK: invoca Integer()
Function<String, Long> f = Integer::new; // ❌ ERRORE: il costruttore restituisce Integer, non Long
In caso di dubbio, riscrivi il method reference come una lambda: se la lambda funziona ma il method reference no, il problema è quasi sempre il matching della signature.