1. Fehler mit Lambda-Ausdrücken: Capture von Variablen
In Java können Lambda-Ausdrücke Variablen aus dem äußeren Kontext verwenden. Es gibt jedoch eine Einschränkung: Solche Variablen müssen final oder „effektiv final“ (effectively final) sein, das heißt, sie dürfen nach der Initialisierung nicht mehr geändert werden.
Fehlerbeispiel
int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.forEach(n -> sum += n); // Kompilierfehler!
Warum?
Der Compiler beschwert sich: Die Variable wird in einer Lambda verwendet, also muss sie final oder effectively final sein, aber sum wird innerhalb der Lambda verändert.
Wie vermeiden?
- Verwenden Sie Terminaloperationen von Streams, die keine externen Variablen benötigen: mapToInt + sum().
- Im äußersten Fall – Container wie AtomicInteger oder ein Ein‑Element‑Array (eher ein Hack).
int sum = list.stream().mapToInt(Integer::intValue).sum();
Analogie
Stellen Sie sich vor, eine Lambda ist ein „Zeitreisender“: Sie „merkt sich“ den Wert der Variable zum Erzeugungszeitpunkt und kann nicht beobachten, wie er sich später ändert. Der Änderungsversuch – das „Großvaterparadoxon“ – wird vom Compiler unterbunden.
2. Fehler mit Sichtbarkeit und this
In einem Lambda-Ausdruck bezieht sich das Schlüsselwort this auf das äußere Objekt und nicht auf eine anonyme Klasse (wie es bei anonymen Klassen der Fall wäre).
Beispiel
public class Example {
int value = 42;
void foo() {
Runnable r = () -> {
System.out.println(this.value); // this bezieht sich auf Example, nicht auf Runnable!
};
r.run();
}
}
Wichtig: Beim Umschreiben von anonymen Klassen auf Lambdas ändert sich die Logik von this – berücksichtigen Sie das, um keine unerwarteten Ergebnisse zu erhalten.
3. Probleme mit veränderlichem Zustand (Side Effects)
Der funktionale Ansatz empfiehlt Nebenwirkungsfreiheit: Funktionen verändern keinen Zustand außerhalb von sich selbst und mutieren keine externen Collections/Variablen.
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Victoria"));
List<String> newNames = new ArrayList<>();
names.forEach(name -> {
if (name.startsWith("A")) {
newNames.add(name); // Nebenwirkung!
}
});
Der Code „funktioniert“, ist aber weniger vorhersehbar und gefährlich bei Verwendung von parallelStream() (Race Conditions und Ausnahmen). Testen und Wartung sind schwieriger.
Richtig: Verwenden Sie Operationen, die das neue Ergebnis explizit bilden, ohne externen Zustand zu verändern.
List<String> newNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
4. Fehler mit Typen und generics
Java ist streng typisiert. Manchmal kann der Compiler die Typen aus zu komplexen Lambdas oder Ketten nicht mehr herleiten.
Beispiel
List<Object> objects = List.of(1, "string", 3.14);
List<String> strings = objects.stream()
.filter(obj -> obj instanceof String)
.map(obj -> (String) obj)
.collect(Collectors.toList());
Das wirkt logisch, aber jeder Tippfehler oder eine falsche Typumwandlung kann zu einem Kompilierfehler führen oder – schlimmer – zur ClassCastException zur Laufzeit.
Wie vermeiden?
- Fügen Sie explizite Typen hinzu, wenn die Typinferenz „stolpert“.
- Scheuen Sie sich nicht, <String> zu schreiben oder Lambdas zu typisieren: (String s) -> ....
- Prüfen Sie die Typkompatibilität bei Umwandlungen.
Typischer Fall mit Optional
Optional<String> opt = Optional.of("hello");
opt.map(s -> s.length()); // Ergebnis – Optional<Integer>
Wenn Sie Optional<String> erwartet haben, aber Optional<Integer> erhalten, prüfen Sie, was Ihre Funktion zurückgibt.
5. Nebenwirkungen in Lambdas und Parallelität
Parallele Streams (parallelStream()) plus Nebenwirkungen sind eine gefährliche Kombination.
Beispiel
List<Integer> numbers = IntStream.range(0, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();
numbers.parallelStream().forEach(n -> results.add(n)); // GEFÄHRLICH!
Was kann passieren?
- Datenverlust oder Duplikate.
- ConcurrentModificationException oder „mysteriöse“ Bugs.
Wie richtig?
- Verwenden Sie thread‑sichere Collections: ConcurrentLinkedQueue, CopyOnWriteArrayList.
- Noch besser – vermeiden Sie Nebenwirkungen vollständig und sammeln Sie das Ergebnis via collect(...).
List<Integer> results = numbers.parallelStream()
.map(n -> n)
.collect(Collectors.toList());
6. Verlust an Lesbarkeit: „Stream-Spaghetti“ und lange Ketten
Der funktionale Stil ist gut, bis die Kette zu einem „Kassenbon aus dem Hypermarkt“ wird.
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());
Tipps:
- Teilen Sie Ketten in logische Blöcke auf.
- Lagern Sie komplexe Lambdas in separate Methoden mit sprechenden Namen aus.
- Fügen Sie bei Bedarf Kommentare hinzu – sogar im Stream‑Code.
7. Unglückliche Namen von Variablen und Funktionen
Übermäßig kurze Namen (x, y, z) erschweren das Verständnis.
list.stream()
.map(x -> x.trim())
.filter(y -> y.length() > 3)
.map(z -> z.toUpperCase())
.forEach(System.out::println);
Verwenden Sie aussagekräftige Namen, insbesondere wenn eine Lambda mehrzeilig ist oder nichttriviale Logik ausdrückt.
8. Fehler mit null und Optional
Das Stream‑API und funktionale Interfaces mögen null nicht. null an eine Lambda oder in einen Stream zu übergeben, ist eine häufige Ursache für NullPointerException.
List<String> list = Arrays.asList("a", null, "b");
list.stream()
.map(String::toUpperCase) // Boom! NPE beim zweiten Element
.forEach(System.out::println);
Richtig:
- Filtern Sie null vorab heraus: .filter(Objects::nonNull).
- Verwenden Sie Optional, um die Abwesenheit eines Werts explizit darzustellen.
9. Probleme mit dem Rückgabetyp in kombinierten Funktionen
Bei der Verwendung von compose und andThen ist es leicht, die Reihenfolge der Anwendung und die erwarteten Typen zu verwechseln.
Function<String, Integer> parse = Integer::parseInt;
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> parseAndSquare = parse.andThen(square);
// Funktioniert: zuerst parse, dann square
Function<String, Integer> squareThenParse = parse.compose(square);
// Fehler! square nimmt Integer, parse erwartet aber String
Merke: Überprüfen Sie stets die Anwendungsreihenfolge und die Typkompatibilität.
10. Probleme mit Checked Exceptions in Lambdas
Funktionale Interfaces aus dem Paket java.util.function erlauben keine Checked Exceptions (z. B. IOException). Wenn innerhalb einer Lambda Code benötigt wird, der solche wirft, behandeln Sie die Ausnahme manuell.
Function<String, String> readFile = path -> {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
throw new RuntimeException(e); // Oder anders behandeln
}
};
Andernfalls lässt der Compiler eine solche Funktion nicht in Streams oder Collections verwenden.
GO TO FULL VERSION