© Excellent backgrounds/Shutterstock.com
Effective Java - Teil 8

Seiteneffekte


In unserer Reihe über das Stream API von Java 8 haben wir bereits erläutert, was Streams sind [1], wie man sie erzeugt und welche Operationen sie haben [2]. Wir haben gesehen, dass jeder Stream-Operation in der Regel eine Funktion übergeben wird, die wir üblicherweise als Lambda-Ausdruck oder Methodenreferenz ausdrücken. Diese Funktionen müssen gewissen Bedingungen bezüglich ihrer Seiteneffekte genügen. In diesem Artikel wollen wir uns ansehen, welche Anforderungen es sind und warum sie existieren.

Lambda-Ausdrücke in Java, wie auch das grundsätzliche Programmierparadigma der Streams, sind stark von den Ideen der funktionalen Programmierung abgeleitet. Ein wichtiges Grundprinzip der funktionalen Programmierung ist (sehr vereinfacht ausgedrückt): Seiteneffekte sind schlecht und deshalb weitestgehend unmöglich, oder falls möglich, doch zu vermeiden. Ein solcher Ansatz (seiteneffektfreie Programmierung) passt nicht so ganz zu Java als objektorientierte Programmiersprache. So hat Java zum Beispiel kein explizites Konzept für unveränderliche Typen, und mit final hat es auch nur ein sehr schwaches Konzept für unveränderliche Variablen.

Was blieb also anderes, als einen pragmatischen Kompromiss zu suchen. Das bedeutet im Fall von Lambda-Ausdrücken (und Methodenreferenzen), die an Stream-Operationen übergeben werden: Manche Seiteneffekte sind zulässig, manche sind ganz verboten. Manche Steiteneffekte sind zwar zulässig, aber wenn möglich zu vermeiden. Zum Teil hängt dies auch noch von der spezifischen Stream-Operation ab.

Schauen wir uns die Details an. Seiteneffekte, die verboten sind, werden in der Javadoc der Stream-Operation bei der Beschreibung des Funktionsparameters aufgelistet. Zum Beispiel findet man in der Javadoc der Methode filter() zum Parameter predicate folgende Erläuterung:

predicate - a non-interfering, stateless predicate to apply to each element todetermine if it should be included

Die entscheidenden Worte sind non-interfering und stateless. Sie beschreiben die verbotenen Seiteneffekte. Schauen wir sie uns genauer an. Fangen wir dabei mit Statelessness an.

Statelessness

Die Statelessness (Zustandslosigkeit) ist eine Anforderung, die von den meisten Funktionen verlangt wird, die an Stream-Operationen übergeben werden. Ausgenommen sind nur die Funktionen, die an forEach(), for­EachOrdered() und peek() übergeben werden.

Zustandslosigkeit heißt, dass die Ausführungen der Funktion auf den verschiedenen Sequenzelementen nicht voneinander abhängig sein dürfen. Insbesondere dürfen die Funktionen keine Daten akkumulieren oder modifizieren. Hier sind Beispiele für zustandslose Funktionen, die die Statelessness-Anforderung erfüllen:

List<String> streamSource = ...; streamSource.parallelStream() .filter(w->w.length()>0) .map(w->w.charAt(0)) .forEach(System.out::print);

Das Prädikat, das an die filter-Operation übergeben wird, liest einen String und bestimmt dessen Länge. Das Prädikat ist zustandslos und hat keinerlei Seiteneffekte. Man kann es beliebig oft und in beliebiger Reihenfolge auf die Sequenzelemente anwenden; es kommt für jedes Sequenzelement immer seine Länge heraus. Gleiches gilt für das Mapping, das der map-Operation übergeben wird. Es kommt immer der Anfangsbuchstabe des jeweiligen Strings heraus. Die Funktion, die an die forEach-Operation übergeben wird, hat hingegen Seiteneffekte: Sie gibt das Zeichen auf System.out aus. Jetzt ist nicht mehr egal, wie oft und in welcher Reihenfolge die Funktion ausgeführt wird; es wirkt sich auf die Ausgabe aus, die auf System.out erscheint. Die Ausführungen der print-Methode auf den verschiedenen Stream-Elementen sind jedoch nicht voneinander abhängig. Deshalb ist der Seiteneffekt der print-Methode vergleichsweise harmlos. Das Problem mit Seiteneffekten und der Verletzung der Statelessness-Anforderung ist, dass sie nicht immer harmlos sind, sondern im Gegenteil sehr leicht zu Fehlern führen können.

Listing 1 ist ein Beispiel für eine zustandsbehaftete Funktion, die die Statelessness-Anforderung verletzt.

Listing 1

List<String> streamSource = ...; Set<String> wordsAlreadySeen = new HashSet<>(); streamSource.stream() .filter(w-> {if(wordsAlreadySeen.contains(w)) { return false; } else { wordsAlreadySeen.add(w); return true; }) .forEach(System.out::println);

Aus einer Sequenz von Strings sollen alle Duplikate entfernt werden. Dafür wird ein Set zur Hilfe genommen, in dem eingetragen wird, welche Strings schon vorgekommen sind. Nur diejenigen Strings, die noch nicht aufgetreten sind, werden in den Downstream weitergegeben. Das gezeigte Prädikat ist ein Beispiel für die Verletzung der Statelessness-Anforderung, denn es hängt vom...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang