Iteratoren und Streams in Java: Gemeinsamkeiten und Unterschiede

Iterierst du noch, oder streamst du schon?

Iterierst du noch, oder streamst du schon?

Iteratoren und Streams in Java: Gemeinsamkeiten und Unterschiede

Iterierst du noch, oder streamst du schon?


Bei einer Diskussion mit Kollegen kam die Frage auf, worin sich Streams und Iteratoren in Java eigentlich unterscheiden. Man sollte denken, dass beide Ansätze dazu gedacht sind, Dinge wiederholt auszuführen, und dass Streams nur eine komfortablere Art und Weise sind, den Algorithmus auszudrücken. Tatsächlich sind sich die beiden APIs in manchen Dingen ähnlich, in anderer Hinsicht unterscheiden sie sich jedoch.

Ziel dieses Artikels ist es, die beiden Ansätze zu vergleichen, zum einen mit Blick auf die Ergonomie (oder die „Schönheit“, wenn man es ehrlich formuliert) eines Beispiels, zum anderen auf funktionale Unterschiede bei der Parallelisierung und Mutabilität.

Als Diskussionsgrundlage verwenden wir die Implementierung einer paginierten Datenbankabfrage sowohl via Stream als auch via Iterator, damit wir die grundlegenden Unterschiede sehen können. Bei Paginierung werden die Ergebnisse nicht auf einmal aus der Datenbank geholt, sondern „Stück für Stück“. Wir kennen dieses Vorgehen aus der Google-Suche, auch wenn ich normalerweise alle Ergebnisse nach Seite eins ignoriere.

Paginierung hat den Vorteil, dass große Ergebnismengen verarbeitet werden können, ohne die Daten komplett in den Arbeitsspeicher zu laden. Nach dem Verarbeiten eines Teilstücks können die verarbeiteten Daten verworfen und das nächste Teilstück kann verarbeitet werden. Als Beispiel sei folgende Tabelle angegeben, bei der wir die Summe der Werte bestimmen wollen (und das zum Wohle des Beispiels nicht in der Datenbank tun). Stellvertretend soll dies für kompliziertere Operationen stehen, die nicht einfach in der Datenbank ausgeführt werden können. Tabelle 1 zeigt ein paar Zeilen Datenbank für unsere Demozwecke. Wie durch die Punkte impliziert, ist die eigentliche Datenbank natürlich deutlich größer.

ID

Wert

1

15

2

24

3

45

4

38

Tabelle 1: Beispieldatenbank

Unpaginiert würden wir die gesamte Datenbank abfragen und die Werte addieren. Das würde bedeuten, dass wir die komplette lange Liste im Hauptspeicher halten müssen. Bei einer paginierten Abfrage würden wir immer z. B. zehn Werte abfragen, summieren und dann die nächsten zehn Werte abfragen und so weiter. Bei einer paginierten Vorgehensweise müssen wir also nie alle Daten vorhalten und können auch Datenmengen verarbeiten, die nicht in den Hauptspeicher passen. Natürlich geht mit diesem Vorgehen auch ein gewisser Overhead einher, weil ja wiederholt Abfragen an die Datenbank geschickt werden und somit mehr Zeit für Roundtrips zwischen Server und Datenbank verbraucht wird.

In SQL können wir mit den LIMIT- und OFFSET-Parametern [1] paginieren. Wir fragen also erst die ersten zehn Werte ab (LIMIT 10 OFFSET 0), dann die nächsten zehn (LIMIT 10 OFFSET 10) und so weiter. Nachfolgend die Query für unsere Beispieldatenbank:

SELECT wert FROM t1 ORDER BY id OFFSET 0 LIMIT 10;

Wichtig ist hier, dass die Abfrage geordnet ist, da sonst die Reihenfolge der Ergebnisse nicht definiert ist und wir ohne konsistente Reihenfolge keine korrekte Paginierung erhalten.

Es gibt bessere Wege zur Paginierung, diese sind jedoch komplizierter zu implementieren, und stärker vom verwendeten Datenbanksystem abhängig: Eine Paginierung via Cursor [2] oder Keyset Pagination [3] wird meistens vorzuziehen sein, die Implementierung würde jedoch den Rahmen dieses Artikels sprengen. Das generelle Schema der Iteration bleibt jedoch erhalten und die Implementierung einer paginierten Abfrage wäre ähnlich.

Implementierung der Paginierung

Das Ziel unserer Implementierung ist es, eine Query mit großer Ergebnismenge so auszuführen, dass der Speicherbedarf der Lösung konstant ist, d. h. zu keiner Zeit mehr als x Ergebnisse vorgehalten werden, egal wie groß die gesamte Ergebnismenge ist. Der Benutzer sollte von dieser Eigenschaft nichts merken und sollte mit dem Ergebnis-Stream bzw. Iterator umgehen können wie mit einem „normalen“ Stream oder Iterator. Natürlich kann man das von Hand implementieren, aber Java gibt einem mächtige APIs an die Hand, damit man das nicht tun muss. Also ab in den Code!

Paginierung mit Streams

Zuerst betrachten wir die Streams-Implementierung, in der natürlich v...