Streams: Lokale Variablen
Beim Streunen durch den Code auf der Suche nach refakturierungswürdigen Häufchen stieß ich auf diese Methode:
ControllerResult addAccounts(List<Account> accounts) { AccountCreator creator = new AccountCreator(); List<AccountCreationResult> accountList = accounts.stream() // .map(creator::create) // .collect(Collectors.toList()); return ControllerResult.result(Status.CREATED).entity(accountList); }
Für jedes Account
-Objekt der Liste wird – mit Hilfe des AccountCreator
-Objekts ein Konto angelegt.
Dessen create
-Methode liefert für jeden Account ein AccountCreationResult
-Objekt, das das Ergebnis der Anlage beschreibt. Die Ergebnisse werden gesammelt und als Liste zurückgegeben. Soweit, so einfach.
Ich will nicht sagen, daß lokale Variablen grundsätzlich von Übel sind, aber jede Zeile Code die man löschen kann ohne die Lesbarkeit zu gefährden ist ein Gewinn. Warum also wird hier überhaupt eine lokale Variable verwendet? Das konventionelle Konstrukt – ohne Stream – sähe so aus:
ControllerResult addAccounts(List<Account> accounts) { AccountCreator creator = new AccountCreator(); List<AccountCreationResult> accountList = new ArrayList<>(); for (Account account : accounts) { accountList.add(creator.create(account)); } return ControllerResult.result(Status.CREATED).entity(accountList); }
Offensichtlich ist es sinnvoller, das Creator
-Objekt nur einmal vor dem Eintritt in die Schleife zu erzeugen
und dann bei jedem Durchlauf wiederzuverwenden, statt für jeden Account einen eigenen Creator zu erzeugen. Wie aber sieht
das mit der Stream-Lösung aus? Betrachten wir dazu den schematischen Ablauf wenn die Stream-Anweisungen ausgeführt werden:
accounts.stream()
erzeugt ein neues Stream-Objekt mit einer leeren Liste von Anweisungen, die für jedes Objekt im Stream ausgeführt werden sollen.
.map(creator::create)
fügt der Anweisungsliste eine Anweisung hinzu, die den λ-Ausdruck creator::create
enthält. Das ist ein
Verweis auf die Methode create()
des Objektes, das in der lokalen Variable creator
steckt.
.collect(Collectors.toList())
führt eine Schleife aus, die auf jedem Objekt des Streams die Anweisungen der Liste ausführt, die entstehenden
Ergebnisse in einer Liste sammelt und diese als List
-Objekt zurückgibt.
Die Behauptung, daß das Stream-Objekt eine Anweisungsliste enthält ist extrem vereinfacht ausgedrückt, ist aber –
was den Ablauf angeht – eine ausreichende Beschreibung. Der entscheidende Punkt ist, daß die Methode map()
hier nur ein einziges Mal ausgeführt wird, und zwar bevor die Schleife über die Stream-Objekte ausgeführt wird.
Schreiben wir also:
ControllerResult addAccounts(List<Account> accounts) { List<AccountCreationResult> accountList = accounts.stream() // .map(new AccountCreator(session)::create) // .collect(Collectors.toList()); return ControllerResult.result(Status.CREATED).entity(accountList); }
dann wird tatsächlich – mit dem Debugger leicht nachzuvollziehen – auch nur einziges AccountCreator
-Objekt
erzeugt. Die Objekt-Referenz wird dabei scheinbar nirgendwo gespeichert. Scheinbar deshalb, weil sie natürlich implizit im λ-Ausdruck
new AccountCreator(session)::create
enthalten ist. Wir können also tatsächlich auf die lokale Variable verzichten und das AccountCreator
-Objekt
on-the-fly beim Erzeugen des λ-Ausdrucks instantiieren.
Nun mag der Lambda-Neuling annehmen, daß die ::
-Notation des Ausdrucks nur syntaktischer Zucker ist und sich
der Ausdruck auch so schreiben ließe:
(Account a) -> new AccountCreator(session).create(a);
Achtung! Hier passiert etwas völlig anderes, denn wenn wir diesen λ-Ausdruck in unserem Stream
verwenden wird tatsächlich für jedes Stream-Objekt ein neues Creator-Objekt erzeugt, denn jetzt ist die Objekt-
Erzeugung mit new
Teil des λ-Ausdrucks geworden. Warum ist das so?
Für den λ-Ausdruck
(Account a) -> new AccountCreator(session).create(a);
wird ein Objekt einer anonymen Klasse erzeugt mit einer anonymen Methode, deren Body den Ausdruck rechts
des ->
-Operators enthält. Beim Ausführen des λ-Audrucks wird nun diese Methode dieses
Objekts – die eine neues AccountCreator
-Objekt erzeugt – ausgeführt. Für den λ-Ausdruck
new AccountCreator(session)::create
hingegen wird ein Objekt der Klasse AccountCreator
erzeugt die die Methode create
implementiert.
Bei der Ausführung des λ-Ausdrucks wird die Methode create
dieses Objekts ausgeführt und es wird kein
neues AccountCreator
-Objekt erzeugt.