Open-Source-Perle JCF

Java Collections à la Goldman Sachs

Moritz Rebbert
©Shutterstock.com/wongwean

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 zwei 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 Implementierungen. 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 mithilfe 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äre 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:

List<Person> persons = ...
List<String> names = new ArrayList<String>();
for(Person person : persons){
  names.add(person.getName());
}

Sämtliche Datenstrukturen, die GS Collections bietet, implementieren das Iterable-Interface, mit dem sich diese Aufgabe wie folgt umsetzen lässt:

MutableList<Person> persons = ...
MutableList<String> names = collect.persons(new Functions<Person, String>() {
     apply(Person person){
      return person.getName();
    }
});

Die 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, sodass die erste Zeile lesbarer wird.

MutableList<String> names = persons.collect(Person.GET_NAME);

Mithilfe von Methoden, Referenzen und Lambda-Ausdrücken in Java 8 fallen diese Funktionsinterfaces in vielen Fällen weg:

MutableList<String> names = persons.collect(Person::getName);

oder

MutableList<String> upperCaseNames = persons.collect(p -> p.getName().toUpperCase()); 

Will man übrigens die gleiche Aufgabe mit dem 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 das 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 das Stream-API in Java 8 bietet GS Collections Lazy- und Parallel-Iteration. Das heißt, Werte werden erst dann mithilfe der übergebenen Funktionsinterfaces 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 zurzeit 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){
  List<Person> tmp = citizens.get(p.getCity());
  if(tmp == null){
    tmp = new ArrayList<Person>();
    citizens.add(tmp);
  }
  tmp.add(p);
}

Mit einer Multimap lässt sich dies vereinfachen, indem man diese direkt aus der Liste aller Personen befüllt:

MutableList 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 Wert zurück. Die Konstruktion gelingt jedoch ebenso einfach.

Map<City, List<Person>> citizens = person.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.

Performancevorteile

Performancevorteile 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() 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 das 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“ neuer Patterns wie impliziter Iteration entstanden. Gleichwohl zeigen die Entwickler von GS Collections mit der Veröffentlichung ihres 5.0.0-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 des 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, sei dahingestellt.

Aufmacherbild: Program code on a monitor von Shutterstock / Urheberrecht: wongwean

Geschrieben von
Moritz Rebbert
Moritz Rebbert
Moritz Rebbert arbeitet als Softwareentwickler und Berater für die agido GmbH in Dortmund, spezialisiert auf mobile und webbasierte Applikationen. Er ist ein Freund pragmatischer Softwarelösungen und entwickelt schwerpunktmäßig im Bereich Content Management für skalierbare und hochperformante Anwendungen. Darüber hinaus beschäftigt er sich mit Entwurfsmustern und Sprachfeatures in Java und JavaScript.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: