Skip to content

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;

  • 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 { }), return non è consentito; l’espressione stessa è il valore di ritorno.
  • Se il corpo usa { } (un blocco), return deve 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.
  • this dentro 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.