CodeGym /Corsi /JAVA 25 SELF /Analisi degli errori nella programmazione funzionale

Analisi degli errori nella programmazione funzionale

JAVA 25 SELF
Livello 49, Lezione 4
Disponibile

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.

1
Compito
JAVA 25 SELF, livello 49, lezione 4
Bloccato
Calcolo del ricavo totale della startup "Costruttori Spaziali" 🚀
Calcolo del ricavo totale della startup "Costruttori Spaziali" 🚀
1
Compito
JAVA 25 SELF, livello 49, lezione 4
Bloccato
Moderazione dei messaggi su "Forum Galattico" 💬
Moderazione dei messaggi su "Forum Galattico" 💬
1
Sondaggio/quiz
Programmazione funzionale, livello 49, lezione 4
Non disponibile
Programmazione funzionale
Programmazione funzionale
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION