Stato mutabile e immutabilità: confronto tra OOP e approccio funzionale
La programmazione funzionale è un paradigma in cui i programmi vengono espressi come composizione di funzioni. Questo modello privilegia l’uso di funzioni pure e la trasformazione dei dati, riducendo al minimo la dipendenza da stato condiviso.
Una funzione pura restituisce sempre lo stesso risultato a parità di input e non produce effetti collaterali osservabili. Non legge né modifica variabili esterne e non altera strutture dati esistenti. I dati, anziché essere aggiornati, vengono trasformati: ogni operazione genera un nuovo valore, lasciando invariato quello di partenza.
Esempio 1: funzione pura
public class Calcolatrice {
public int quadrato(int n) {
return n * n;
}
}
Il metodo quadrato può essere considerato puro poiché non legge né modifica campi della classe, non interagisce con risorse esterne e il risultato dipende esclusivamente dal parametro in ingresso n.
Esempio 2: funzione non pura
public class Calcolatrice {
private int fattore;
public int moltiplica(int n) {
return n * fattore;
}
}
Il metodo perde la sua purezza poiché dipende dallo stato interno della classe (fattore) e il risultato può variare a parità di input se tale stato viene modificato.
Una differenza centrale rispetto alla programmazione orientata agli oggetti riguarda la gestione dello stato. Nell’OOP gli oggetti incapsulano uno stato interno e i metodi operano modificandolo nel tempo. Il comportamento di un oggetto può quindi dipendere dallo stato proprio o da quello di altri oggetti. Nell’ approccio funzionale, al contrario, le funzioni non mantengono stato e agiscono come trasformazioni indipendenti, riducendo le dipendenze implicite e rendendo il flusso logico più prevedibile.
Le funzioni sono considerate cittadini di prima classe: possono essere assegnate a variabili, passate come argomento e restituite come risultato. Questo consente un’elevata composizione del comportamento e favorisce la costruzione di pipeline di elaborazione.
Anche lo stile di scrittura del codice differisce. Nei linguaggi orientati agli oggetti il codice è spesso imperativo, focalizzato sulle istruzioni necessarie a ottenere un risultato. La programmazione funzionale predilige invece uno stile dichiarativo, in cui si descrive cosa deve essere fatto, delegando al linguaggio o al runtime il controllo del flusso di esecuzione.
Esempio 3: filtrare numeri pari da una collezione di interi – Approccio imperativo
public class EsempioImperativo {
public static void main(String[] args) {
List<Integer> numeri = List.of(1,2,3,4);
List<Integer> pari = new ArrayList<>();
for (int n: numeri) {
if(n%2 == 0) pari.add(n);
}
System.out.println(pari); // [2, 4]
}
}
Esempio 4: filtrare numeri pari da una collezione di interi – Approccio funzionale
public class EsempioFunzionale {
public static void main(String[] args) {
List<Integer> numeri = List.of(1,2,3,4);
List<Integer> pari = numeri
.stream()
.filter(n -> n%2 == 0)
.toList();
System.out.println(pari); // [2, 4]
}
}
In questi ultimi due esempi si osserva come, nell’approccio imperativo, la logica sia espressa in termini di come eseguire l’operazione, utilizzando un flusso di controllo esplicito per filtrare i dati. Nell’approccio funzionale, invece, le operazioni vengono composte in pipeline e il codice descrive cosa deve essere fatto.
Java 8 e l’introduzione dei principi funzionali
Con la diffusione dei processori multi-core e l’aumento dei contesti concorrenti, è emersa l’esigenza di modelli di programmazione più adatti a gestire il parallelismo e a limitare i problemi legati allo stato condiviso. In questo contesto, a partire da Java 8, il linguaggio ha introdotto elementi ispirati alla programmazione funzionale.
L’obiettivo non era trasformare Java in un linguaggio funzionale puro, ma affiancare al modello orientato agli oggetti strumenti capaci di ridurre la complessità del codice e migliorare la sicurezza in presenza di concorrenza. L’uso estensivo di oggetti mutabili, tipico dell’OOP, può infatti rendere più difficile garantire correttezza e manutenibilità; per questo motivo Java incentiva pratiche come la limitazione degli effetti collaterali e l’adozione dell’ immutabilità ove possibile.
Questi principi trovano applicazione concreta nelle Stream API, che permettono di trattare le collezioni come flussi di dati su cui applicare operazioni di trasformazione e aggregazione senza modificare la struttura originale. Gli stream favoriscono uno stile dichiarativo e consentono, in modo trasparente, anche l’esecuzione parallela.
Esempio 4: elaborazione con Stream e lambda
List<String> nomi = List.of("Leonardo", "Francesco", "Sofia");
// Trasformazione dichiarativa con Stream
List<String> maiuscolo = nomi.stream()
.map(String::toUpperCase)
.filter(s -> s.startsWith("S"))
.toList();
System.out.println(maiuscolo); // [SOFIA]
Esempio 5: elaborazione parallela
List<Integer> numeri = List.of(1, 2, 3, 4, 5, 6);
// Calcolo parallelo della somma dei quadrati
int somma = numeri.parallelStream()
.mapToInt(n -> n * n)
.sum();
System.out.println(somma); // 91
La programmazione funzionale in Java non sostituisce l’approccio orientato agli oggetti, ma lo integra. Comprendere le differenze tra i due paradigmi e i contesti in cui ciascuno risulta più efficace consente di adottare soluzioni più chiare, robuste e manutenibili.
Nel prossimo articolo verranno introdotte le Stream API, mostrando come gli stream rappresentino il principale strumento con cui Java applica i principi funzionali nella pratica quotidiana.