CodeGym /Cours /JAVA 25 SELF /Analyse des erreurs en programmation fonctionnelle

Analyse des erreurs en programmation fonctionnelle

JAVA 25 SELF
Niveau 49, Leçon 4
Disponible

1. Erreurs avec les expressions lambda : capture de variables

En Java, les expressions lambda peuvent utiliser des variables du contexte externe. Mais il y a une contrainte : ces variables doivent être final ou « effectively final », c’est‑à‑dire ne pas être modifiées après l’initialisation.

Exemple d’erreur

int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.forEach(n -> sum += n); // Erreur de compilation !

Pourquoi ?
Le compilateur proteste : la variable est utilisée dans une lambda, elle doit donc être final ou effectively final, mais sum est modifiée à l’intérieur de la lambda.

Comment l’éviter ?

  • Utilisez des opérations terminales de flux qui n’exigent pas de variables externes : mapToInt + sum().
  • Dans les cas extrêmes — des conteneurs tels que AtomicInteger ou un tableau à un seul élément (mais c’est plutôt un hack).
int sum = list.stream().mapToInt(Integer::intValue).sum();

Analogie
Imaginez qu’une lambda soit un « voyageur dans le temps » : elle « mémorise » la valeur de la variable au moment de sa création et ne peut pas observer son évolution. Tenter de la modifier — c’est le « paradoxe du grand‑père », le compilateur refusera de construire le programme.

2. Erreurs de portée et this

Dans une expression lambda, le mot‑clé this fait référence à l’objet englobant, et non à une classe anonyme (contrairement aux classes anonymes).

Exemple

public class Example {
    int value = 42;

    void foo() {
        Runnable r = () -> {
            System.out.println(this.value); // this désigne Example, pas Runnable !
        };
        r.run();
    }
}

Important : lors de la réécriture de code de classes anonymes vers des lambdas, la sémantique de this change — tenez‑en compte pour éviter un résultat inattendu.

3. Problèmes d’état mutable (effets de bord)

L’approche fonctionnelle recommande l’absence d’effets de bord : les fonctions ne modifient pas l’état externe et ne mutent pas les collections/variables externes.

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); // Effet de bord !
    }
});

Ce code « fonctionne », mais il est moins prévisible et dangereux avec parallelStream() (risque de courses et d’exceptions). Il est plus difficile à tester et à maintenir.

Correct : utilisez des opérations qui produisent explicitement un nouveau résultat sans modifier l’état externe.

List<String> newNames = names.stream()
    .filter(name -> name.startsWith("A"))
    .collect(Collectors.toList());

4. Erreurs de types et generics

Java est un langage fortement typé. Parfois, le compilateur n’arrive pas à déduire les types à partir de lambdas ou de chaînes trop complexes.

Exemple

List<Object> objects = List.of(1, "chaîne", 3.14);
List<String> strings = objects.stream()
    .filter(obj -> obj instanceof String)
    .map(obj -> (String) obj)
    .collect(Collectors.toList());

Cela semble logique, mais la moindre faute de frappe ou un transtypage incorrect peut provoquer une erreur de compilation ou, pire, une ClassCastException à l’exécution.

Comment l’éviter ?

  • Ajoutez des types explicites lorsque l’inférence des types « trébuche ».
  • N’ayez pas peur d’écrire <String> ou de paramétrer les lambdas : (String s) -> ....
  • Vérifiez la compatibilité des types lors des conversions.

Cas typique avec Optional

Optional<String> opt = Optional.of("hello");
opt.map(s -> s.length()); // résultat — Optional<Integer>

Si vous attendiez Optional<String>, mais avez obtenu Optional<Integer>, vérifiez ce que renvoie votre fonction.

5. Effets de bord dans les lambdas et parallélisme

Les flux parallèles (parallelStream()) combinés aux effets de bord — une combinaison dangereuse.

Exemple

List<Integer> numbers = IntStream.range(0, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();

numbers.parallelStream().forEach(n -> results.add(n)); // DANGEREUX !

Que peut‑il se passer ?

  • Perte ou duplication de données.
  • ConcurrentModificationException ou bogues « mystiques ».

La bonne approche :

  • Utiliser des collections sûres pour les threads : ConcurrentLinkedQueue, CopyOnWriteArrayList.
  • Encore mieux — éviter totalement les effets de bord et collecter le résultat via collect(...).
List<Integer> results = numbers.parallelStream()
    .map(n -> n)
    .collect(Collectors.toList());

6. Perte de lisibilité : « spaghetti de streams » et longues chaînes

Le style fonctionnel est excellent tant que la chaîne ne se transforme pas en « ticket de caisse de supermarché ».

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());

Conseils :

  • Découpez les chaînes en blocs logiques.
  • Extrayez les lambdas complexes dans des méthodes séparées avec des noms explicites.
  • Ajoutez des commentaires si nécessaire — même dans le code Stream.

7. Mauvais noms de variables et de fonctions

Des noms trop courts (x, y, z) nuisent à la compréhension.

list.stream()
    .map(x -> x.trim())
    .filter(y -> y.length() > 3)
    .map(z -> z.toUpperCase())
    .forEach(System.out::println);

Utilisez des noms explicites, surtout si la lambda s’étend sur plusieurs lignes ou exprime une logique non triviale.

8. Erreurs avec null et Optional

L’API Stream et les interfaces fonctionnelles n’aiment pas les null. Passer null à une lambda ou à un flux est une cause fréquente de NullPointerException.

List<String> list = Arrays.asList("a", null, "b");
list.stream()
    .map(String::toUpperCase) // Boom ! NPE sur le deuxième élément
    .forEach(System.out::println);

Comment bien faire ?

  • Filtrez les null à l’avance : .filter(Objects::nonNull).
  • Utilisez Optional pour représenter explicitement l’absence de valeur.

9. Problèmes de type de retour dans les fonctions combinées

Avec compose et andThen, il est facile d’inverser l’ordre d’application des fonctions et les types attendus.

Function<String, Integer> parse = Integer::parseInt;
Function<Integer, Integer> square = x -> x * x;

Function<String, Integer> parseAndSquare = parse.andThen(square);
// Ça fonctionne : d’abord parse, puis square

Function<String, Integer> squareThenParse = parse.compose(square);
// Erreur ! square prend un Integer, alors que parse attend un String

Morale : vérifiez toujours l’ordre d’application et la compatibilité des types.

10. Problèmes avec les exceptions vérifiées dans les lambdas

Les interfaces fonctionnelles du package java.util.function n’autorisent pas de lever des exceptions vérifiées (par exemple, IOException). Si vous avez besoin de code qui en lève à l’intérieur d’une lambda, gérez l’exception manuellement.

Function<String, String> readFile = path -> {
    try {
        return Files.readString(Path.of(path));
    } catch (IOException e) {
        throw new RuntimeException(e); // Ou traiter autrement
    }
};

Sinon, le compilateur n’autorisera pas l’utilisation d’une telle fonction dans un flux ou une collection.

1
Mission
JAVA 25 SELF, niveau 49, leçon 4
Bloqué
Calcul du revenu total de la startup "Constructeurs Cosmiques" 🚀
Calcul du revenu total de la startup "Constructeurs Cosmiques" 🚀
1
Mission
JAVA 25 SELF, niveau 49, leçon 4
Bloqué
Modération des messages sur "Forum Galactique" 💬
Modération des messages sur "Forum Galactique" 💬
1
Étude/Quiz
Programmation fonctionnelle, niveau 49, leçon 4
Indisponible
Programmation fonctionnelle
Programmation fonctionnelle
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION