1. Errori con le espressioni lambda: cattura delle variabili
In Java le espressioni lambda possono utilizzare variabili dal contesto esterno. Ma c’è una restrizione: tali variabili devono essere final o «effettivamente finali» (effectively final), cioè non devono essere modificate dopo l’inizializzazione.
Esempio di errore
int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.forEach(n -> sum += n); // Errore di compilazione!
Perché?
Il compilatore si lamenta: la variabile è usata nella lambda, quindi deve essere final o effectively final, ma sum viene modificata all’interno della lambda.
Come evitarlo?
- Usate operazioni terminali degli stream che non richiedono variabili esterne: mapToInt + sum().
- Nei casi limite — contenitori come AtomicInteger o un array con un solo elemento (ma è più un hack).
int sum = list.stream().mapToInt(Integer::intValue).sum();
Analogia
Immagina che una lambda sia un «viaggiatore del tempo»: “memorizza” il valore della variabile al momento della creazione e non può osservarne i cambiamenti. Tentare di modificarla — «paradosso del nonno»: il compilatore impedirà la compilazione.
2. Errori con l’ambito e this
In un’espressione lambda la parola chiave this si riferisce all’oggetto esterno, non a una classe anonima (come avverrebbe con le classi anonime).
Esempio
public class Example {
int value = 42;
void foo() {
Runnable r = () -> {
System.out.println(this.value); // this — è Example, non Runnable!
};
r.run();
}
}
Importante: riscrivendo il codice da classi anonime a lambda cambia il comportamento di this — tenetelo presente per evitare risultati inattesi.
3. Problemi con lo stato mutabile (effetti collaterali)
L’approccio funzionale raccomanda l’assenza di effetti collaterali: le funzioni non modificano lo stato esterno e non mutano collezioni/variabili esterne.
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));
List<String> newNames = new ArrayList<>();
names.forEach(name -> {
if (name.startsWith("A")) {
newNames.add(name); // Effetto collaterale!
}
});
Il codice “funziona”, ma è meno prevedibile e rischioso con parallelStream() (rischio di race condition ed eccezioni). È più difficile da testare e manutenere.
Corretto: utilizzate operazioni che creano esplicitamente un nuovo risultato senza modificare lo stato esterno.
List<String> newNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
4. Errori con i tipi e i generics
Java è un linguaggio fortemente tipizzato. A volte il compilatore non riesce a inferire i tipi da lambda o catene troppo complesse.
Esempio
List<Object> objects = List.of(1, "stringa", 3.14);
List<String> strings = objects.stream()
.filter(obj -> obj instanceof String)
.map(obj -> (String) obj)
.collect(Collectors.toList());
Sembra logico, ma un qualsiasi refuso o cast errato può portare a un errore di compilazione o, peggio, a una ClassCastException a runtime.
Come evitarlo?
- Aggiungere tipi espliciti quando l’inferenza dei tipi «inciampa».
- Non abbiate timore di scrivere <String> o di parametrizzare le lambda: (String s) -> ....
- Verificare la compatibilità dei tipi nelle conversioni.
Caso tipico con Optional
Optional<String> opt = Optional.of("hello");
opt.map(s -> s.length()); // risultato — Optional<Integer>
Se vi aspettavate Optional<String>, ma avete ottenuto Optional<Integer>, verificate che cosa restituisce la vostra funzione.
5. Effetti collaterali nelle lambda e parallelismo
Stream paralleli (parallelStream()) più effetti collaterali — combinazione pericolosa.
Esempio
List<Integer> numbers = IntStream.range(0, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();
numbers.parallelStream().forEach(n -> results.add(n)); // PERICOLOSO!
Che cosa può accadere?
- Perdita o duplicazione dei dati.
- ConcurrentModificationException o bug «misteriosi».
Come fare correttamente?
- Usare collezioni thread-safe: ConcurrentLinkedQueue, CopyOnWriteArrayList.
- Ancora meglio — evitare del tutto gli effetti collaterali e raccogliere il risultato tramite collect(...).
List<Integer> results = numbers.parallelStream()
.map(n -> n)
.collect(Collectors.toList());
6. Perdita di leggibilità: «stream-spaghetti» e catene lunghe
Lo stile funzionale va bene finché la catena non diventa uno «scontrino da ipermercato».
List<String> result = list.stream()
.filter(s -> s.length() > 2)
.map(String::trim)
.map(s -> s.toUpperCase())
.filter(s -> s.contains("JAVA"))
.sorted()
.distinct()
.collect(Collectors.toList());
Consigli:
- Suddividete le catene in blocchi logici.
- Estraete le lambda complesse in metodi separati con nomi chiari.
- Aggiungete commenti quando necessario — anche nel codice Stream.
7. Nomi poco efficaci di variabili e funzioni
Nomi eccessivamente corti (x, y, z) rendono più difficile la comprensione.
list.stream()
.map(x -> x.trim())
.filter(y -> y.length() > 3)
.map(z -> z.toUpperCase())
.forEach(System.out::println);
Usate nomi significativi, soprattutto se la lambda è multilinea o esprime una logica non banale.
8. Errori con null e Optional
Lo Stream API e le interfacce funzionali non amano i null. Passare null a una lambda o a uno stream è una causa frequente di NullPointerException.
List<String> list = Arrays.asList("a", null, "b");
list.stream()
.map(String::toUpperCase) // Boom! NPE sul secondo elemento
.forEach(System.out::println);
Come fare correttamente?
- Filtrate i null in anticipo: .filter(Objects::nonNull).
- Usate Optional per rappresentare esplicitamente l’assenza di un valore.
9. Problemi con il tipo di ritorno nelle funzioni combinate
Usando compose e andThen è facile invertire l’ordine di applicazione delle funzioni e i tipi attesi.
Function<String, Integer> parse = Integer::parseInt;
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> parseAndSquare = parse.andThen(square);
// Funziona: prima parse, poi square
Function<String, Integer> squareThenParse = parse.compose(square);
// Errore! square accetta Integer, mentre parse si aspetta String
Morale: controllate sempre l’ordine di applicazione e la compatibilità dei tipi.
10. Problemi con le eccezioni checked nelle lambda
Le interfacce funzionali del package java.util.function non consentono di lanciare eccezioni checked (ad esempio, IOException). Se all’interno della lambda serve codice che le solleva, gestite l’eccezione manualmente.
Function<String, String> readFile = path -> {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
throw new RuntimeException(e); // Oppure gestirla in altro modo
}
};
Altrimenti il compilatore non permetterà di usare tale funzione in uno stream o in una raccolta.
GO TO FULL VERSION