Programmazione Funzionale in Java
A partire da Java 8, il linguaggio supporta la programmazione funzionale attraverso lambda expressions, method references, la Stream API e Optional. Questi strumenti permettono codice più conciso, leggibile e dichiarativo.
Cosa Imparerai
- Lambda expressions e sintassi
- Interfacce funzionali
- Method references
- Stream API: operazioni intermedie e terminali
- Collectors e raggruppamento
- Optional per gestire null
Lambda Expressions
Una lambda è una funzione anonima che può essere passata come parametro o assegnata a una variabile.
// Prima di Java 8 - classe anonima
Comparator<String> comparatorVecchio = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
};
// Con Lambda - Java 8+
Comparator<String> comparatorLambda = (s1, s2) -> s1.length() - s2.length();
// Varianti sintattiche
// Un solo parametro: parentesi opzionali
Consumer<String> stampa = s -> System.out.println(s);
// Nessun parametro: parentesi vuote obbligatorie
Runnable azione = () -> System.out.println("Eseguito!");
// Corpo con più istruzioni: usare blocco {}
Comparator<String> conBlocco = (s1, s2) -> {
int diff = s1.length() - s2.length();
return diff != 0 ? diff : s1.compareTo(s2);
};
Interfacce Funzionali
Interfacce Funzionali Principali
| Interfaccia | Metodo | Uso |
|---|---|---|
| Predicate<T> | boolean test(T) | Condizioni, filtri |
| Function<T,R> | R apply(T) | Trasformazioni |
| Consumer<T> | void accept(T) | Azioni su elementi |
| Supplier<T> | T get() | Generazione valori |
| BiFunction<T,U,R> | R apply(T,U) | Due input, un output |
| UnaryOperator<T> | T apply(T) | Stesso tipo in/out |
import java.util.function.*;
public class EsempiFunzionali {
public static void main(String[] args) {
// Predicate: condizione
Predicate<Integer> isSufficiente = voto -> voto >= 18;
System.out.println(isSufficiente.test(25)); // true
System.out.println(isSufficiente.test(15)); // false
// Function: trasformazione
Function<String, Integer> lunghezza = s -> s.length();
System.out.println(lunghezza.apply("Java")); // 4
// Consumer: azione
Consumer<String> saluta = nome -> System.out.println("Ciao " + nome);
saluta.accept("Mario"); // Ciao Mario
// Supplier: generazione
Supplier<Double> randomVoto = () -> 18 + Math.random() * 13;
System.out.println(randomVoto.get()); // voto casuale 18-30
// Composizione
Predicate<Integer> isEccellente = voto -> voto >= 28;
Predicate<Integer> isBuono = isSufficiente.and(isEccellente.negate());
Function<String, String> maiuscolo = String::toUpperCase;
Function<String, String> conPrefisso = s -> "Prof. " + s;
Function<String, String> titolo = maiuscolo.andThen(conPrefisso);
System.out.println(titolo.apply("rossi")); // Prof. ROSSI
}
}
Method References
import java.util.*;
public class MethodReferences {
public static void main(String[] args) {
List<String> nomi = Arrays.asList("Mario", "Laura", "Giuseppe");
// 1. Riferimento a metodo statico
// Lambda: s -> System.out.println(s)
nomi.forEach(System.out::println);
// 2. Riferimento a metodo di istanza (oggetto specifico)
String prefisso = "Studente: ";
// Lambda: s -> prefisso.concat(s)
nomi.stream()
.map(prefisso::concat)
.forEach(System.out::println);
// 3. Riferimento a metodo di istanza (tipo)
// Lambda: s -> s.toUpperCase()
nomi.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
// 4. Riferimento a costruttore
// Lambda: s -> new StringBuilder(s)
nomi.stream()
.map(StringBuilder::new)
.forEach(sb -> System.out.println(sb.reverse()));
}
}
Stream API
Gli Stream permettono di elaborare sequenze di elementi in modo dichiarativo, con supporto per operazioni parallele.
import java.util.*;
import java.util.stream.*;
public class CreazioneStream {
public static void main(String[] args) {
// Da Collection
List<String> lista = Arrays.asList("A", "B", "C");
Stream<String> streamLista = lista.stream();
// Da array
String[] array = {"A", "B", "C"};
Stream<String> streamArray = Arrays.stream(array);
// Stream.of()
Stream<String> streamOf = Stream.of("A", "B", "C");
// Stream.generate() - infinito
Stream<Double> casuali = Stream.generate(Math::random).limit(5);
// Stream.iterate() - sequenza
Stream<Integer> pari = Stream.iterate(0, n -> n + 2).limit(10);
// IntStream, LongStream, DoubleStream - primitivi
IntStream interi = IntStream.range(1, 11); // 1-10
IntStream interiChiuso = IntStream.rangeClosed(1, 10); // 1-10
// Da file (righe)
// Stream<String> righe = Files.lines(Path.of("file.txt"));
}
}
Operazioni Intermedie
import java.util.*;
import java.util.stream.*;
public class OperazioniIntermedie {
public static void main(String[] args) {
List<Studente> studenti = Arrays.asList(
new Studente("Mario", 28, "Informatica"),
new Studente("Laura", 30, "Matematica"),
new Studente("Giuseppe", 25, "Informatica"),
new Studente("Anna", 27, "Fisica")
);
// FILTER: seleziona elementi
List<Studente> promossi = studenti.stream()
.filter(s -> s.getVoto() >= 18)
.collect(Collectors.toList());
// MAP: trasforma elementi
List<String> nomi = studenti.stream()
.map(Studente::getNome)
.collect(Collectors.toList());
// MAP con trasformazione
List<String> nomiMaiuscoli = studenti.stream()
.map(s -> s.getNome().toUpperCase())
.collect(Collectors.toList());
// FLATMAP: appiattisce stream di stream
List<List<String>> corsiPerStudente = Arrays.asList(
Arrays.asList("Java", "Python"),
Arrays.asList("SQL", "MongoDB"),
Arrays.asList("Java", "JavaScript")
);
List<String> tuttiCorsi = corsiPerStudente.stream()
.flatMap(List::stream)
.distinct()
.collect(Collectors.toList());
// [Java, Python, SQL, MongoDB, JavaScript]
// SORTED: ordina
List<Studente> perVoto = studenti.stream()
.sorted(Comparator.comparing(Studente::getVoto).reversed())
.collect(Collectors.toList());
// DISTINCT: rimuove duplicati
List<String> corsiUnici = studenti.stream()
.map(Studente::getCorso)
.distinct()
.collect(Collectors.toList());
// PEEK: debug (non modifica lo stream)
studenti.stream()
.peek(s -> System.out.println("Elaborando: " + s.getNome()))
.filter(s -> s.getVoto() >= 28)
.forEach(System.out::println);
// LIMIT e SKIP
List<Studente> primi2 = studenti.stream()
.limit(2)
.collect(Collectors.toList());
List<Studente> dopoPrimi2 = studenti.stream()
.skip(2)
.collect(Collectors.toList());
}
}
class Studente {
private String nome;
private int voto;
private String corso;
public Studente(String nome, int voto, String corso) {
this.nome = nome;
this.voto = voto;
this.corso = corso;
}
public String getNome() { return nome; }
public int getVoto() { return voto; }
public String getCorso() { return corso; }
}
Operazioni Terminali
import java.util.*;
import java.util.stream.*;
public class OperazioniTerminali {
public static void main(String[] args) {
List<Integer> voti = Arrays.asList(28, 30, 25, 27, 30, 24);
// FOREACH: azione su ogni elemento
voti.stream().forEach(System.out::println);
// COUNT: conta elementi
long quanti = voti.stream()
.filter(v -> v >= 28)
.count(); // 3
// SUM, AVERAGE, MIN, MAX (IntStream)
int somma = voti.stream()
.mapToInt(Integer::intValue)
.sum();
OptionalDouble media = voti.stream()
.mapToInt(Integer::intValue)
.average();
OptionalInt massimo = voti.stream()
.mapToInt(Integer::intValue)
.max();
// REDUCE: riduce a singolo valore
int sommaReduce = voti.stream()
.reduce(0, (a, b) -> a + b);
Optional<Integer> maxReduce = voti.stream()
.reduce(Integer::max);
// MATCH: verifica condizioni
boolean tuttiSufficienti = voti.stream()
.allMatch(v -> v >= 18); // true
boolean almenoUn30 = voti.stream()
.anyMatch(v -> v == 30); // true
boolean nessunoInsuff = voti.stream()
.noneMatch(v -> v < 18); // true
// FIND: trova elementi
Optional<Integer> primo = voti.stream()
.filter(v -> v >= 28)
.findFirst();
Optional<Integer> qualsiasi = voti.stream()
.filter(v -> v >= 28)
.findAny(); // Utile con parallelStream
// TOARRAY
Integer[] arrayVoti = voti.stream()
.filter(v -> v >= 25)
.toArray(Integer[]::new);
}
}
Collectors Avanzati
import java.util.*;
import java.util.stream.*;
public class CollectorsAvanzati {
public static void main(String[] args) {
List<Studente> studenti = Arrays.asList(
new Studente("Mario", 28, "Informatica"),
new Studente("Laura", 30, "Matematica"),
new Studente("Giuseppe", 25, "Informatica"),
new Studente("Anna", 27, "Fisica"),
new Studente("Marco", 30, "Informatica")
);
// GROUPING BY: raggruppa per chiave
Map<String, List<Studente>> perCorso = studenti.stream()
.collect(Collectors.groupingBy(Studente::getCorso));
// {Informatica=[Mario, Giuseppe, Marco], Matematica=[Laura], Fisica=[Anna]}
// GroupingBy con downstream collector
Map<String, Long> contaPerCorso = studenti.stream()
.collect(Collectors.groupingBy(
Studente::getCorso,
Collectors.counting()
));
// {Informatica=3, Matematica=1, Fisica=1}
Map<String, Double> mediaPerCorso = studenti.stream()
.collect(Collectors.groupingBy(
Studente::getCorso,
Collectors.averagingInt(Studente::getVoto)
));
Map<String, Optional<Studente>> migliorPerCorso = studenti.stream()
.collect(Collectors.groupingBy(
Studente::getCorso,
Collectors.maxBy(Comparator.comparing(Studente::getVoto))
));
// PARTITIONING BY: divide in true/false
Map<Boolean, List<Studente>> promossiVsNon = studenti.stream()
.collect(Collectors.partitioningBy(s -> s.getVoto() >= 18));
// {true=[tutti], false=[]}
Map<Boolean, Long> contaPromossi = studenti.stream()
.collect(Collectors.partitioningBy(
s -> s.getVoto() >= 28,
Collectors.counting()
));
// JOINING: concatena stringhe
String nomiConcatenati = studenti.stream()
.map(Studente::getNome)
.collect(Collectors.joining(", "));
// "Mario, Laura, Giuseppe, Anna, Marco"
String conPrefissoSuffisso = studenti.stream()
.map(Studente::getNome)
.collect(Collectors.joining(", ", "Studenti: [", "]"));
// "Studenti: [Mario, Laura, Giuseppe, Anna, Marco]"
// TO MAP
Map<String, Integer> nomeVoto = studenti.stream()
.collect(Collectors.toMap(
Studente::getNome,
Studente::getVoto
));
// SUMMARIZING
IntSummaryStatistics stats = studenti.stream()
.collect(Collectors.summarizingInt(Studente::getVoto));
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
System.out.println("Media: " + stats.getAverage());
System.out.println("Somma: " + stats.getSum());
System.out.println("Count: " + stats.getCount());
}
}
Optional
Optional è un container che può contenere o meno un valore,
evitando NullPointerException.
import java.util.*;
public class EsempiOptional {
public static Optional<Studente> trovaStudente(List<Studente> studenti,
String nome) {
return studenti.stream()
.filter(s -> s.getNome().equals(nome))
.findFirst();
}
public static void main(String[] args) {
List<Studente> studenti = Arrays.asList(
new Studente("Mario", 28, "Informatica"),
new Studente("Laura", 30, "Matematica")
);
// Creare Optional
Optional<String> pieno = Optional.of("Valore");
Optional<String> vuoto = Optional.empty();
Optional<String> nullable = Optional.ofNullable(null);
// isPresent e isEmpty
Optional<Studente> mario = trovaStudente(studenti, "Mario");
if (mario.isPresent()) {
System.out.println("Trovato: " + mario.get().getVoto());
}
// ifPresent: esegue azione se presente
mario.ifPresent(s -> System.out.println("Voto: " + s.getVoto()));
// ifPresentOrElse (Java 9+)
Optional<Studente> paolo = trovaStudente(studenti, "Paolo");
paolo.ifPresentOrElse(
s -> System.out.println("Trovato: " + s.getNome()),
() -> System.out.println("Studente non trovato")
);
// orElse: valore default
Studente result = paolo.orElse(new Studente("Default", 0, "N/A"));
// orElseGet: supplier per default (lazy)
Studente resultLazy = paolo.orElseGet(
() -> new Studente("Default", 0, "N/A")
);
// orElseThrow: eccezione se vuoto
Studente obbligatorio = mario.orElseThrow(
() -> new NoSuchElementException("Studente richiesto!")
);
// map: trasforma se presente
Optional<String> nomeStudente = mario.map(Studente::getNome);
// flatMap: per Optional nested
Optional<String> corsoOpt = mario.flatMap(s ->
Optional.ofNullable(s.getCorso())
);
// filter: filtra valore
Optional<Studente> eccellente = mario
.filter(s -> s.getVoto() >= 28);
// Chaining completo
String risultato = trovaStudente(studenti, "Mario")
.filter(s -> s.getVoto() >= 25)
.map(Studente::getNome)
.map(String::toUpperCase)
.orElse("NON TROVATO");
System.out.println(risultato); // MARIO
}
}
Esempio Completo: Analisi Registro Studenti
import java.util.*;
import java.util.stream.*;
public class AnalisiRegistro {
public static void main(String[] args) {
List<Esame> esami = Arrays.asList(
new Esame("Mario", "Programmazione", 28, 2024),
new Esame("Mario", "Database", 30, 2024),
new Esame("Mario", "Reti", 25, 2024),
new Esame("Laura", "Programmazione", 30, 2024),
new Esame("Laura", "Database", 29, 2024),
new Esame("Giuseppe", "Programmazione", 24, 2024),
new Esame("Giuseppe", "Database", 26, 2023),
new Esame("Anna", "Programmazione", 30, 2024),
new Esame("Anna", "Reti", 28, 2024)
);
// 1. Media voti per studente
System.out.println("=== Media per studente ===");
Map<String, Double> mediaPerStudente = esami.stream()
.collect(Collectors.groupingBy(
Esame::getStudente,
Collectors.averagingInt(Esame::getVoto)
));
mediaPerStudente.forEach((nome, media) ->
System.out.printf("%s: %.2f%n", nome, media)
);
// 2. Studente con media più alta
System.out.println("\n=== Migliore studente ===");
mediaPerStudente.entrySet().stream()
.max(Map.Entry.comparingByValue())
.ifPresent(e ->
System.out.printf("%s con media %.2f%n", e.getKey(), e.getValue())
);
// 3. Distribuzione voti
System.out.println("\n=== Distribuzione voti ===");
Map<String, Long> distribuzione = esami.stream()
.collect(Collectors.groupingBy(
e -> {
int v = e.getVoto();
if (v >= 28) return "Ottimo (28-30)";
if (v >= 24) return "Buono (24-27)";
return "Sufficiente (18-23)";
},
Collectors.counting()
));
distribuzione.forEach((fascia, count) ->
System.out.println(fascia + ": " + count)
);
// 4. Esami per materia con lista studenti
System.out.println("\n=== Studenti per materia ===");
Map<String, String> studentiPerMateria = esami.stream()
.collect(Collectors.groupingBy(
Esame::getMateria,
Collectors.mapping(
Esame::getStudente,
Collectors.joining(", ")
)
));
studentiPerMateria.forEach((materia, studenti) ->
System.out.println(materia + ": " + studenti)
);
// 5. Top 3 voti
System.out.println("\n=== Top 3 voti ===");
esami.stream()
.sorted(Comparator.comparing(Esame::getVoto).reversed())
.limit(3)
.forEach(e -> System.out.printf("%s - %s: %d%n",
e.getStudente(), e.getMateria(), e.getVoto()));
// 6. Studenti con tutti esami >= 28
System.out.println("\n=== Studenti eccellenti ===");
Map<String, Boolean> tuttiEccellenti = esami.stream()
.collect(Collectors.groupingBy(
Esame::getStudente,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream().allMatch(e -> e.getVoto() >= 28)
)
));
tuttiEccellenti.entrySet().stream()
.filter(Map.Entry::getValue)
.map(Map.Entry::getKey)
.forEach(System.out::println);
// 7. Statistiche complete
System.out.println("\n=== Statistiche ===");
IntSummaryStatistics stats = esami.stream()
.mapToInt(Esame::getVoto)
.summaryStatistics();
System.out.println("Totale esami: " + stats.getCount());
System.out.println("Media: " + String.format("%.2f", stats.getAverage()));
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
}
}
class Esame {
private String studente;
private String materia;
private int voto;
private int anno;
public Esame(String studente, String materia, int voto, int anno) {
this.studente = studente;
this.materia = materia;
this.voto = voto;
this.anno = anno;
}
public String getStudente() { return studente; }
public String getMateria() { return materia; }
public int getVoto() { return voto; }
public int getAnno() { return anno; }
}
Conclusione
La programmazione funzionale in Java rende il codice più conciso e leggibile. Lambda, Stream e Optional sono strumenti essenziali per lo sviluppo moderno.
Punti Chiave da Ricordare
- Lambda: Funzioni anonime per codice conciso
- Method Reference: Sintassi abbreviata (Class::method)
- Stream: Pipeline dichiarative per elaborazione dati
- Collectors: Raggruppamento, partizionamento, statistiche
- Optional: Gestione sicura di valori nullable
- Lazy evaluation: Gli stream non elaborano finché non serve
Nel prossimo articolo affronteremo concorrenza e multithreading: Thread, ExecutorService e sincronizzazione.







