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.
GO TO FULL VERSION