1. Errore: riutilizzo dello stream
Uno stream (Stream) in Java è uno strumento monouso. Non appena eseguite un’operazione terminale (per esempio, collect(), forEach(), count()), lo stream si chiude e non è più utilizzabile.
Esempio di errore
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println); // Tutto bene
stream.forEach(System.out::println); // Genererà IllegalStateException!
Output:
a
b
c
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
Come evitarlo
Se occorre eseguire più operazioni, create un nuovo stream ogni volta:
List<String> list = List.of("a", "b", "c");
list.stream().forEach(System.out::println); // OK
list.stream().count(); // OK
Trucco: Uno stream è come un bicchiere monouso della mensa: usalo e poi gettalo; prendine uno nuovo per la bevanda successiva.
2. Errore: modificare la collezione durante lo stream
Se modificate la collezione di origine durante l’iterazione tramite stream, otterrete una ConcurrentModificationException. È un problema comune durante il filtraggio e la rimozione di elementi.
Esempio di errore
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.stream().forEach(name -> {
if (name.startsWith("A")) {
names.remove(name); // BOOM!
}
});
Risultato:
Exception in thread "main" java.util.ConcurrentModificationException
Modo corretto
Usate metodi che non modificano la collezione durante l’iterazione, oppure raccogliete il risultato in una nuova collezione:
names.removeIf(name -> name.startsWith("A")); // OK, removeIf è sicuro
// Oppure con lo Stream API:
List<String> filtered = names.stream()
.filter(name -> !name.startsWith("A"))
.collect(Collectors.toList());
Ricorda: Gli stream amano la purezza — non toccate la collezione di origine mentre la stanno «consumando».
3. Errore: limit dimenticato con gli stream infiniti
I metodi Stream.iterate e Stream.generate possono creare stream infiniti. Se dimenticate di limitarli con il metodo limit, potete rimanere bloccati per sempre (o fino a OutOfMemoryError).
Esempio di errore
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
infinite.forEach(System.out::println); // Il programma non terminerà mai!
Modo corretto
Limitate lo stream con il metodo limit:
Stream<Integer> finite = Stream.iterate(0, n -> n + 1).limit(10);
finite.forEach(System.out::println); // Stamperà i numeri da 0 a 9
Consiglio: Gli stream infiniti ricordano una serie senza episodio finale: qualcuno deve fermarla, ed è meglio che siate voi!
4. Errore: gestione errata di null
Lo Stream API non ama il null. Se nella collezione c’è un null, molte operazioni possono lanciare NullPointerException, soprattutto se invocate metodi sugli elementi.
Esempio di errore
List<String> names = Arrays.asList("Alice", null, "Bob");
names.stream()
.map(String::toUpperCase) // BOOM! su null
.forEach(System.out::println);
Risultato:
Exception in thread "main" java.lang.NullPointerException
Modo corretto
Filtrate in anticipo i valori null:
names.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.forEach(System.out::println);
Lavorare con Optional
Se avete uno Stream<Optional<T>>, usate flatMap(Optional::stream) (Java 9+) per «espanderli»:
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty(), Optional.of("B"));
optionals.stream()
.flatMap(Optional::stream)
.forEach(System.out::println); // Stamperà solo "A" e "B"
Consiglio: Cercate di non aggiungere null alle collezioni. Se proprio non se ne può fare a meno, filtrateli all’inizio.
5. Errore: perdita dell’ordine degli elementi
Alcune operazioni (per esempio, flatMap, concat) possono influire sull’ordine degli elementi, soprattutto se lavorate con stream paralleli o unificate collezioni diverse.
Esempio di errore
List<Integer> list1 = List.of(1, 2, 3);
List<Integer> list2 = List.of(4, 5, 6);
Stream<Integer> combined = Stream.concat(list1.stream(), list2.stream());
combined.forEach(System.out::print); // 123456 — l'ordine è preservato
// Ma usando stream paralleli:
Stream<Integer> par = Stream.concat(list1.parallelStream(), list2.parallelStream());
par.forEach(System.out::print); // L'ordine può essere inaspettato!
Modo corretto
Se l’ordine è importante, evitate gli stream paralleli oppure usate metodi che garantiscono l’ordine (forEachOrdered):
par.forEachOrdered(System.out::print);
Fatto: Lo Stream API per impostazione predefinita preserva l’ordine, a meno che non siate voi a «romperlo».
6. Errore: unione inefficiente di collezioni
Per le operazioni sugli insiemi è meglio usare Set, non List, per evitare duplicati e migliorare le prestazioni. Inoltre è inefficiente scrivere cicli annidati dove si può usare flatMap.
Esempio di errore
List<String> list1 = List.of("a", "b", "c");
List<String> list2 = List.of("b", "c", "d");
// Inefficiente:
List<String> union = new ArrayList<>(list1);
for (String s : list2) {
if (!union.contains(s)) { // contains su List — O(n)
union.add(s);
}
}
// Efficiente con Set e Stream API:
Set<String> unionSet = Stream.concat(list1.stream(), list2.stream())
.collect(Collectors.toSet());
Oppure per la differenza:
// Inefficiente:
List<String> diff = new ArrayList<>(list1);
diff.removeAll(list2); // OK, ma se lo faceste via stream e list.contains sarebbe lento
// Efficiente:
Set<String> set2 = new HashSet<>(list2);
List<String> difference = list1.stream()
.filter(s -> !set2.contains(s))
.collect(Collectors.toList());
Consiglio: Per lavorare con elementi unici usate sempre Set. Per collezioni grandi evitate cicli annidati: preferite stream + flatMap.
7. Errore: operazioni terminali ripetute
A volte si desidera «sdoppiare» uno stream, ad esempio raccogliere gli elementi in una lista e in parallelo contarli. Ma lo stream è «monouso» e la seconda operazione provocherà un errore.
Esempio di errore
Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
long count = stream.count(); // BOOM! IllegalStateException
Modo corretto
Se dovete usare i dati più volte, prima raccoglieteli in una collezione:
List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
long count = list.size();
Consiglio: Se volete fare due cose con gli stessi dati, prima raccogliete in una collezione.
8. Errore: close() dimenticato per le risorse
Se usate stream collegati a risorse (per esempio, Files.lines(Path)), non dimenticate di chiuderli dopo l’uso per evitare perdite.
Esempio di errore
Stream<String> lines = Files.lines(Path.of("file.txt"));
lines.forEach(System.out::println); // OK, ma la risorsa non è chiusa!
Modo corretto
Usate try-with-resources:
try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
lines.forEach(System.out::println);
}
Consiglio: Gli stream su file sono come un bollitore: fallo bollire e poi spegnilo!
9. Errore: gestione scorretta di Optional e null in flatMap
Lavorando con collezioni del tipo List<Optional<T>>, molti usano map(Optional::get), il che porta a eccezioni se l’Optional è vuoto.
Esempio di errore
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty());
optionals.stream()
.map(Optional::get) // BOOM! NoSuchElementException
.forEach(System.out::println);
Modo corretto
Usate flatMap(Optional::stream) (Java 9+):
optionals.stream()
.flatMap(Optional::stream)
.forEach(System.out::println); // Solo "A"
Consiglio: Optional non è solo un involucro, ma uno strumento per lavorare in sicurezza con l’assenza di valore.
10. Errore: equals/hashCode implementati in modo errato
Le operazioni sugli insiemi (Set, distinct, unione, intersezione) dipendono da una corretta implementazione dei metodi equals e hashCode.
Esempio di errore
class Student {
String name;
int age;
// Mancano equals e hashCode!
}
Set<Student> students = new HashSet<>();
students.add(new Student("Alice", 20));
students.add(new Student("Alice", 20));
System.out.println(students.size()); // 2, anche se dovrebbero essere considerati uguali!
Modo corretto
Sovrascrivete equals e hashCode per le vostre classi se intendete usarle nelle collezioni Set, Map o per operazioni di distinct.
11. Errore: uso dello stream dopo collect
Dopo un’operazione terminale (per esempio, collect) lo stream si chiude. Qualsiasi tentativo di continuare a lavorare con lo stesso stream porterà a un errore.
Esempio di errore
Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
stream.filter(s -> s.startsWith("a")).forEach(System.out::println); // Exception!
Modo corretto
Create un nuovo stream dalla collezione:
list.stream().filter(s -> s.startsWith("a")).forEach(System.out::println);
12. Errore: uso scorretto di parallelStream
Molti pensano che parallelStream() acceleri sempre l’elaborazione. In realtà, per collezioni piccole o operazioni semplici il parallelismo può rallentare a causa dell’overhead di gestione dei thread.
Esempio di errore
List<Integer> numbers = IntStream.range(0, 10).boxed().collect(Collectors.toList());
numbers.parallelStream().forEach(System.out::println); // Può essere più lento del normale stream()
Modo corretto
Usate parallelStream solo per collezioni davvero grandi o calcoli pesanti, soprattutto se le operazioni sono pure (senza effetti collaterali) e associative.
13. Errore: operatore terminale dimenticato
Le operazioni intermedie (map, filter, flatMap ecc.) non vengono eseguite finché non viene invocato un operatore terminale (collect, forEach, count ecc.).
Esempio di errore
Stream<String> stream = Stream.of("a", "b", "c").map(String::toUpperCase);
// Non succederà nulla!
Modo corretto
Richiamate sempre un operatore terminale:
stream.forEach(System.out::println);
GO TO FULL VERSION