Goldman Sachs Collections
24.04.2014
Die Goldman Sachs Collections (GS Collections) ist eine Erweiterung des Java Collections Frameworks (JCF). Das Projekt entstand als interne Bibliothek der Entwicklungsabteilung von Goldman Sachs. Vor 2 Jahren wurde es als Open Source unter der Apache-Lizenz veröffentlicht.
Mit der kürzlich veröffentlichten Version 5.0.0 wurde das Projekt auf Java 8 umgestellt. Es folgt vielen Paradigmen, die mit Java 8 und als neue Features des JCF hinzugekommen sind.
Für die üblichen Interfaces wie List, Map und Set existieren eigene Implementierung. Bei den eigenen Implementierungen von ArrayList, HashMap und HashSet verspricht das Framework einen Performancevorteil gegenüber den JCF Klassen. Darüber hinaus erweitert GS Collections den Vorrat an Datenstrukturen und bietet ein vielfältiges API zur Transformation der Collections-Typen mit Hilfe von Funktions- und Prädikatsobjekten. Die Bibliothek ist dabei schon auf die neuen Lambda-Ausdrücke vorbereitet.
Iteration Pattern
Ein zentrales Konzept der Collections ist die interne bzw. implizite Iteration. Dabei bedient sich die Bibliothek Mustern, die mittlerweile in viele populären imperative Sprachen Einzug gehalten haben. Zudem ist die interne Iteration auch ein neues Feature des JCF in Java 8. Die Macher von GS Collections berufen sich darauf, von Smalltalk und Ruby inspiriert worden zu sein. Dieses Konzept steht im Gegensatz zum herkömmlichen imperativen Durchlaufen einer for- oder while-Schleife. Die Datenstrukturen bieten deklarative Methoden, die die Implementierung der Iteration verbergen. Möchte man beispielsweise aus einer Liste von Personen die Liste der Namen aller Personen bilden, so würde man bei expliziter Iteration folgendermaßen vorgehen:
ListSämtliche Datenstrukturen, die GS Collections bietet, implementieren das Iterable Interface, mit dem sich diese Aufgabe wie folgt umsetzen lässt:persons = … List names = new ArrayList<String>(); for(Person person : persons){ names.add(person.getName()); }
MutableListDie Funktionalität des Namenszugriffs wird an die Iterationsmethode collect übergeben. Die Iteration selbst wird nicht explizit aufgeschrieben. Üblicherweise, werden diese anonymen Interfaces als Konstanten der Person-Klasse definiert, so dass die erste Zeile lesbarer wird.persons = … MutableList names = collect.persons( new Function<Person, String>() { apply(Person person){ return person.getName(); } });
MutableList<String> names = persons.collect(Person.GET_NAME);Mit Hilfe von Methoden, Referenzen und Lambda-Ausdrücken in Java 8 fallen diese Funktions-Interfaces in vielen Fällen weg:
MutableList<String> names = persons.collect(Person::getName());oder
MutableList<String> names = persons.collect(p -> p.getName().toUpperCase());Will man übrigens die gleiche Aufgabe mit der neuen Stream-API umsetzen so wirkt GS Collections etwas schlanker:
List<String> names = persons.stream() .map(Person::getName) .collect(Collectors.toList());Ein komplexeres Beispiel zeigt, dass sich die Iterable API schön dazu verwenden lässt, in fluent-Manier mehrere Aufrufe hintereinander laufen zu lassen. Der Code liest sich dabei fast wie ein natürlichsprachlicher Satz. Hier werden alle Warenlieferungen an diejenigen Kunden verschickt, die in einer bestimmten Stadt wohnen:
getCustomers() .select(c -> c.isFrom("London")) .flatCollect(Customer::getOrders) .forEach(Order::deliver);Um das hier angerissene Iterationspattern auch auf Arrays und Strings anwenden zu können, existieren Adapterklassen die das Iteratable-Interface für diese Typen implementieren.
Lazy und Parallel
Ebenso wie die Stream-API in Java 8 bietet GS Collections Lazy- und Parallel-Iteration. Das heißt, Werte werden erst dann mit Hilfe der übergebenen Funktions-Interfaces berechnet, wenn sie benötigt werden. Bei Iterationen in Umgebungen mit mehreren Prozessorkernen können zudem die Berechnungen für die einzelnen Elemente parallelisiert werden. Dabei muss man sich als Entwickler nicht mit der Verwaltung von Threads auseinandersetzen, sondern kann auf die gewohnten Methoden des Iterable Interfaces zugreifen. Die parallele Iteration ist zur Zeit noch als Beta markiert.
Neue Datenstrukturen
GS Collections bietet neben den klassischen Datenstrukturen wie List, Set und Map noch Multimap. Letztere ist eine Map auf Listen von Elementen. Um in einer Map zu jeder Stadt ihre Einwohner zu finden, könnte man folgende Struktur deklarieren:
List<Person> persons = ... Map<City, List<Person>> citizens = … for(Person p : persons){ ListMit einer Multimap lässt sich dies vereinfachen, indem man diese direkt aus der Liste aller Personen befüllt:tmp = citizens.get(p.getCity()); if(tmp == null){ tmp = new ArrayList (); citizens.add(tmp); } tmp.add(p); }
MutableList<Person> persons = ... MutableMultimap<City, Person> citizens = persons.groupBy(Person::getCity);Im Vergleich zu Java 8 sind die Unterschiede dann aber nur noch minimal. Hier greift man weiterhin explizit auf List als Value zurück. Die Konstruktion gelingt jedoch ebenso einfach.
Map<City, List<Person>> citizens = persons.getStream().collect(groupingBy(Person::getCity);GS Collections folgt hier, ebenso wie die neuen JDK-Features, dem deklarativen Modell. Der Code drückt aus, WAS erreicht werden soll und nicht WIE es berechnet wird. Neben den Multimaps liefert die Bibliothek noch Bags (unsortierte Listen mit Mengenangaben) und eine eigene Stack implementierung.
Performance-Vorteile
Performance-Vorteile zieht GS Collections auf einfache Weise daraus, dass sie bei der Implementierung der Datenstrukturen näher an der internen Implementierung entlang entwickelt ist. Während ein allgemeines statisches Collections.sort() zunächst aus jeder beliebigen Collection zunächst eine Umwandlung in ein Array vornimmt, arbeitet FastList.sort() direkt auf dem internen Array und spart sich die Umwandlung. Ob dieser Vorteil tatsächlich signifikant ist, hängt sicherlich vom Einzelfall ab.
Fazit
Im Zusammenspiel mit Lambdas macht GS Collections auf den ersten Blick einen schlanken Eindruck und wirkt bei der internen Iteration etwas ausdrucksstärker, weil kompakter als die stream()-API des SDK. Es hat den Anschein, als wäre GS Collections unter anderem für alle, die auf Java 8 nicht warten wollten, als “Spielwiese” der neuen Patterns wie impliziter Iteration entstanden. Gleichwohl zeigen die Entwickler von GS Collections mit der Veröffentlichung ihres 5.0.0er Releases parallel zur Veröffentlichung von Java 8, dass sie die neuen Sprachfeatures als ideale Ergänzung ihrer eigenen Bibliothek sehen, die dadurch noch lange nicht obsolet wird. Es bleibt die Frage nach dem Alleinstellungsmerkmal von GS Collections. Natürlich bietet es viele Features, die JCF Collections erst mit Java 8 spendiert bekommt, setzt dabei aber Java 8 nicht voraus. Sollte daher aus technischen Gründen der Sprung auf Java 8 im eigenen Projekt noch nicht möglich sein, bekommt man mit GS Collections, viele hilfreiche Erweiterungen wie z.B. einen vollwertigen Ersatz der Stream-API. Ob man GS Collections tatsächlich als zusätzliche Bibliothek in sein Projekt aufnehmen möchte, vor allem wenn man z.B. schon Googles Guava als verbessertes Collection Framework verwendet, bleibt dahingestellt.