In dieser Serie erwarten dich kleine, aber knifflige Java-Code-Schnipsel, die auf den ersten Blick völlig harmlos aussehen und genau deshalb besonders gefährlich sind.
Denn manchmal wird Code „moderner“ oder „schöner“ umgeschrieben, obwohl sich dabei unbemerkt das Verhalten ändert. Und genau so ein Fall ist mir heute begegnet.
Sind diese beiden Code-Beispiele wirklich gleichwertig?
Code vorher:
for (int i = 0; i < myList.length; i++) {
myList[i] = convert(myList[i]).trim();
}
Code nachher:
Arrays.stream(myList).forEach(s -> convert(s).trim());
Antwortmöglichkeiten:
A. Ja, beide Codes machen exakt das Gleiche
B. Nein, der zweite Code verändert myList gar nicht
C. Nein, der zweite Code führt zu einer Exception
D. Das hängt nur von der Methode convert() ab
Step-by-Step zur Lösung
Auf den ersten Blick sieht der zweite Code ziemlich modern aus. Weniger Code, mehr Stream-API, wirkt also erstmal wie ein sauberes Refactoring.
Aber genau hier liegt der Denkfehler.
Schauen wir uns zuerst den alten Code an:
myList[i] = convert(myList[i]).trim();
Hier passiert etwas sehr Wichtiges:
- Das Element
myList[i]wird genommen - durch
convert(...)verarbeitet - mit
trim()bereinigt - und anschließend wieder in das Array zurückgeschrieben
Genau dieses Zurückschreiben ist der entscheidende Punkt.
Jetzt schauen wir auf den neuen Code:
Arrays.stream(myList).forEach(s -> convert(s).trim());
Hier wird zwar für jedes Element convert(s).trim() aufgerufen, aber das Ergebnis wird nirgendwo gespeichert.
Das bedeutet:
convert(s).trim();
wird berechnet und danach sofort wieder verworfen.
forEach führt nur eine Aktion pro Element aus; es liefert keinen neuen Stream zurück und schreibt auch nichts automatisch in das ursprüngliche Array. trim() wiederum gibt einen String mit entfernten Leerzeichen zurück, statt den bestehenden String direkt zu verändern.
Und genau deshalb bleibt myList am Ende unverändert.
Warum ist das so tückisch?
Weil der neue Code beim schnellen Lesen absolut plausibel aussieht.
Man sieht:
- Stream über das Array
- Verarbeitung jedes Elements
convert(...)trim()
Und im Kopf klingt das schnell nach: „Ja klar, das ist einfach die moderne Version der Schleife.“
Ist es aber nicht.
Der Unterschied ist: Im ersten Beispiel wird das Ergebnis zurück in die Datenstruktur geschrieben. Im zweiten Beispiel wird das Ergebnis nur kurz berechnet und dann weggeworfen.
Gerade bei String ist das fatal, weil Strings unveränderlich sind. Methoden wie trim() verändern also nicht das vorhandene Objekt, sondern liefern einen neuen String zurück. Ohne Zuweisung passiert mit deinem Array exakt nichts. Die Java-Dokumentation beschreibt forEach als terminale Operation zum Ausführen einer Aktion pro Element; trim() gibt einen String mit entfernten Leerzeichen zurück.
Die richtige Lösung lautet also B: Nein, der zweite Code verändert myList gar nicht.
Und genau das macht diesen Fehler so gefährlich: Der Code sieht sauber aus, kompiliert problemlos, läuft ohne Exception und macht trotzdem fachlich nicht das, was vorher gemacht wurde.
Das ist oft die unangenehmste Art von Bug.
Wie wäre eine korrekte Stream-Variante?
Wenn man wirklich ein neues Array haben will, müsste man das Ergebnis auch sammeln:
myList = Arrays.stream(myList)
.map(s -> convert(s).trim())
.toArray(String[]::new);
Oder man bleibt einfach bei der ursprünglichen Schleife, wenn man direkt das bestehende Array überschreiben möchte.
Merksatz:
Streams verarbeiten Daten nicht automatisch „zurück“ in die ursprüngliche Struktur. Wenn du ein Ergebnis behalten willst, musst du es auch explizit speichern.
Wie fandest du dieses Beispiel? Hättest du beim zweiten Code auf den ersten Blick gedacht, dass er dasselbe macht wie die Schleife?
