Introduzione alla programmazione funzionale in Java

Marco Magnarelli

@Contattami

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;
    }
}

Esempio 2: funzione non pura

public class Calcolatrice {

    private int fattore;

    public int moltiplica(int n) {
        return n * fattore;
    }
}

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]
	}
}

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.