Evolution in Iteration

An wenigen APIs lässt sich die Evolution der JDK-Versionen so gut erklären wie an Hand der Collection-API, speziell den Möglichkeiten der Iteration. Fangen wir mal an.

Alle der folgenden Beispiele haben Ihre Grundlage in einer recht einfachen Collection:

public static List zahlen
     = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Wir wollen schlicht alle Zahlen dieses Arrays aufaddieren, mehr nicht und schauen uns nun die Möglichkeiten an:

Externe Iterationen

Die einfachste Lösung ist die Folgende:

int summe = 0;
for (int i = 0 ; i < zahlen.size() ; i++){
   summe += zahlen.get(i);
}
System.out.println(summe);

Macht 5 Zeilen Code für eine einfache Aufgabe. Das wäre nicht das Schlimmste, wenn da nicht so viel unnötiger Code enthalten wäre, welches ein Leser erst mal verarbeiten muss.

Beginnen wir mit der Iteration. Erst muss man sich die for-Anweisung genau anschauen, um zu erkennen:

  • Okay, hier wird offensichtlich durch die Elemente des Arrays iteriert
  • hier wird offensichtlich durch ALLE Elemente des Arrays iteriert; erkennbar an den Grenzen 0 und size(). Wobei noch zu kontrollieren ist, ob auch wirklich der richtige Operation „echt kleiner“ verwendet wurde

Das einzig Interessante der for-Anweisung bleibt nur noch, dass hier wohl etwas addiert wird. Das wird aber auch arg verschleiert durch den indizierten Zugriff (get(..)).

Dieser Algorithmus funktioniert auch nicht mit jeder Collection, sondern nur mit List-Typen, die auch den indizierten Zugriffsoperator besitzen.

Dann noch die Hilfsvariable „summe“, die vor der for-Anweisung erstellt werden muss, um schließlich unten ausgegeben zu werden. Wie gut (und schlecht zugleich), dass diese Variable „summe“ heißt, so dass die Bedeutung recht schnell deutlich wird. ABER: was ist, wenn in der Schleife der Algorithmus geändert wird. Denkt der Entwickler dann auch an die Anpassung des Variablennamens 😉 …
Es gibt ja Entwickler, die hier lieber „ergebnis“ als Variablenname verwenden.

Das JDK 6 lieferte uns noch ein kleines Update, wodurch eine Iteration auch ohne indizierten Zugriff möglich wurde:

int summe = 0;
for (Integer zahl : zahlen){
   summe += zahl;
}
System.out.println(summe);

Vorteil dieser Variante ist, dass der Inhalt der for-Anweisung nicht mehr so genau betrachtet werden muss. Lediglich noch die zu iterierende Collection wird als Information benötigt. UND, diese Schreibeweise funktioniert wirklich mit jeder Collection, weil jede Collection das Iterable-Interface implementiert.

Aber dennoch: Hier fragt man sich noch mehr nach dem Sinn so viel unnötigen Codes.  „zahlen“ ist eine Collection vom Typ „Integer“. Warum in aller Welt ist hier noch die Typangabe für die Variable „zahl“ überhaupt nötig?
Und, so ganz ist der Entwickler (und Leser) von der Iteration noch nicht befreit. Eine gebundene Anweisung wäre super.

Interne Iteration

Mit dem JDK 8 kommt nun eine fundamentale Neuerung herein. Als einen wichtigen umgesetzten Aspekt im JDK 8 kann gesagt werden, dass hier alles getan wurde, um unnötigen Code künftig zu vermeiden. Alles implizit Vorhandene muss auch nicht mehr geschrieben werden.

Beispielsweise wurde für die Collection die Methode forEach eingeführt,  in die eine Consumer-Instanz geschrieben werden kann.

final StringBuffer stringBuffer = new StringBuffer("0");
zahlen.forEach(new Consumer<Integer>() {
 @Override
 public void accept(Integer integer) {
 int value = Integer.parseInt(stringBuffer.toString()) + integer;
 stringBuffer.delete(0, stringBuffer.length());
 stringBuffer.append(value);
 }
});
summe = Integer.parseInt(stringBuffer.toString());

Ok, schön sieht’s vielleicht nicht aus. Es ist sogar ein wenig geflunkert, weil hier das Ergebnis doch glatt in einem StringBuffer gespeichert wird und nicht in einer int-Variablen.

Das liegt daran, dass Collection<T>.forEach(Consumer<T> consumer) vom Typ void ist und prinzipiell nichts zurück geben kann. Daher müssen wir nun in der Consumer-Implementierung einen Wert verändern, den wir außerhalb dessen Instanz anlegen. Das Problem hier ist nun, dass nur final-Werte innerhalb der Instanz bearbeitet werden können. Also müssen wir hier zu dem Trick greifen, eine Instanz zu verwenden, dessen internen Wert wir immer ändern. Der Typ String ist selbst konstant und alle Operationen erzeugen eine neue Instanz, daher nehmen wir hier das nächst gelegene, nämlich den StringBuffer.

Bitte beachten, dass hier mit „final“ nicht die Unveränderlichkeit des Objekts bedeutet, sondern in Bezug auf die Variable/Referenz steht. Es bedeutet also, dass keine neue Referenz eingetragen werden darf. Aber das wollen wir ja auch nicht.

StringBuffer stringBuffer = new StringBuffer("0");
zahlen.forEach(new Consumer<Integer>() {
    @Override
    public void accept(Integer integer) {
        int value = Integer.parseInt(stringBuffer.toString()) + integer;
        stringBuffer.delete(0, stringBuffer.length());
        stringBuffer.append(value);
    }
});
summe = Integer.parseInt(stringBuffer.toString());

Und, die Vereinfachung gesehen? Das Wort „final“ ist entfernt. Wie schon gesagt, hat JAVA kräftig im Syntax aufgeräumt und viel unnötigen Syntax entfernt. Dazu gehört auch, dass mittlerweile die meisten JAVA-Entwickler wissen, dass eine in einer anonymen inneren Klasse verwendete Variable final sein muss, also nicht mehr neu zugewiesen werden darf. Dies kontrolliert künftig der Compiler direkt, so dass keine explizite final-Deklaration mehr nötig ist. Die Variable „stringBuffer“ ist nun „effektiv final“.

Daher würde beispielsweise der folgende Code nicht mehr kompilieren:

int summe = 0;
StringBuffer stringBuffer = new StringBuffer("0");
zahlen.forEach(new Consumer<Integer>() {
    @Override
    public void accept(Integer integer) {
        int value = Integer.parseInt(stringBuffer.toString()) + integer;
        stringBuffer.delete(0, stringBuffer.length());
        stringBuffer = stringBuffer.append(value);
    }
});
summe = Integer.parseInt(stringBuffer.toString());

Übrigens erstaunlicherweise nicht in Zeile 2, sondern in Zeile 8, weil „stringBuffer“ durch die Neuzuweisung nicht mehr final sein kann:
„local variables referenced from an inner class must be final or effectively final“

Doch wieder zurück zum eigentlichen Beispiel:

StringBuffer stringBuffer = new StringBuffer("0");
zahlen.forEach(new Consumer<Integer>() {
    @Override
    public void accept(Integer integer) {
        int value = Integer.parseInt(stringBuffer.toString()) + integer;
        stringBuffer.delete(0, stringBuffer.length());
        stringBuffer.append(value);
    }
});
summe = Integer.parseInt(stringBuffer.toString());

Im ersten Moment sieht die Umsetzung schlimmer aus als die gebundene for-Anweisung. Es gibt hier aber eine entscheidende Verbesserung: forEach-Methode.

Die forEach-Methode sagt uns, dass schlicht durch alle Instanzen der Collection iteriert wird.  Aber die Schreibweise ist der blanke Horror. Hier kommen nun erstmal Lambdas ins Spiel. Denn, Consumer ist ein „FunctionalInterface“, so dass dieser ganze Klassen- und Methoden-Code nicht mehr geschrieben werden muss:

new Consumer<Integer>() {
    @Override
    public void accept(Integer integer) {
        int value = Integer.parseInt(stringBuffer.toString()) + integer;
        stringBuffer.delete(0, stringBuffer.length());
        stringBuffer.append(value);
    }
}

Die grünen Anteile können entfallen:

  • Da forEach ein Consumer-Objekt aufnimmt, muss nicht mehr die gesamte Klasse mit der (einzigen) Methoden aufgeschrieben werden. Das kennen wir implizit und wird vom Compiler überwacht.
  • Dass die Variable „t“ vom Typ Integer ist, ist auch klar, weil „zahlen“ eine Collection vom Typ Integer ist.

Der Code reduziert sich also auf:

StringBuffer stringBuffer = new StringBuffer("0");
zahlen.forEach((t) -> {int value = Integer.parseInt(stringBuffer.toString()) + t;
    stringBuffer.delete(0, stringBuffer.length());
    stringBuffer.append(value);});
System.out.println("" + stringBuffer);

Ich hoffe, nun beginnt die Versöhnungsphase 😉 .

Der Vorteil ist nicht nur die reduzierende Schreibweise. Auch der Code selbst wird einfacher verständlich und selbstsprechender:

  1. Es ist klar, dass hier durch alle Elemente iteriert wird
  2. jedes Element wird ergriffen und zur äußeren Variablen hinzu gezählt

Okay, Punkt zwei kommt im Code momentan noch sehr verschleiert zur Geltung. Das werden wir nun ändern.

Das Problem der forEach-Anweisung ist, dass diese leider den Rückgabetyp „void“ hat, so dass innerhalb der forEach-Anweisungen auch keine wertschöpfenden Operationen durchgeführt werden können.

Interne Iteration im Streams und Lambdas

Hier helfen uns die Streams, natürlich in Verbindung mit Lambdas.

Integer summe = zahlen.stream()
                      .reduce(0, (carry, e) -> carry + e);
System.out.println(summe);

Hier rate ich dringendst dazu, sich einerseits mit den einzelnen Stream-Methoden und die FunctionalInterfaces im Package „java.util.function“ auseinander zu setzen. So gelangt man sehr schnell zu dieser Darstellung.

Mehr Reduktion geht kaum noch. Dem Leser wird nach ein wenig Übung im Umgang mit Streams und Lambdas die Bedeutung einer solchen Zeile sofort klar.

Eine letzte Optimierung unter Anwendung einer JDK-8-Neuerung möchte ich nicht unerwähnt lassen:

Integer summe = zahlen.stream()
                      .reduce(0, Integer::sum);
System.out.println(summe);

Mit Hife von Methodenreferenzen (Syntax: „::“)lassen sich diese auch bei geeigneter Signatur als Instanzen von FunctionalInterfaces betrachten und damit auch als entsprechende Lambda-Instanz. Wie beispielsweise die Integer.sum(a, b)-Methode, die hier schlicht verwendet werden kann.

Fertige Ergebnis:

Na ja, wenn es – wie in unserem Beispiel – um die schlichte Ausgabe der Summe geht, lässt sich alles natürlich auch direkt ohne Hilfs-Variable schreiben:

System.out.println(zahlen.stream()
                     .reduce(0, Integer::sum));

Kürzer geht’s nun wirklich nicht mehr; und aussagefähiger auch nicht.