Groovy: Objektnavigation

Groovy bietet einige Erleichterungen beim Navigieren durch Objektnetze an. Mit Hilfe von zwei zusätzlichen Operatoren und einiger Hilfestellung durch das GDK ist es möglich, Navigationsausdrücke in der Art zu bilden, wie es sie unter der Bezeichnung XPath für XML-Dokumente gibt; bei Groovy heißen sie natürlich GPath. Wie das funktioniert, demonstrieren wir zunächst einmal an einem einfachen Beispiel, einer Liste mit URLs, an der wir einige Untersuchungen vornehmen wollen.

groovy> bookmarks = [
groovy>   new URL('http://groovy.codehaus.org'),
groovy>   new URL('http://www.google.de/search?hl=de&q=Groovy&btnG=Google-Suche&meta='),
groovy>   new URL('http://de.wikipedia.org/wiki/Groovy'),
groovy>   new URL('mailto:user@groovy.codehaus.org')
groovy> ]

Dereferenzieren von Listen

Bearbeiten

Als erstes wollen wir sehen, welche Protokolle in den URLs vertreten sind. Anstatt die Listenelemente alle einzeln durchzugehen und bei jedem einzeln die Property protocol abfragen, nutzen wir die angenehme Eigenschaft einer Liste, dass sie alle Property-Aufrufe, die sie selbst nicht befriedigen kann, an ihre Listenelemente weiter leitet und die Ergebnisse wiederum als Liste zurückliefert. Da eine List keine Property protocol kennt, können wir diese also direkt bei der Liste abfragen:

 groovy> bookmarks.protocol
 ===> ["http", "http", "http", "mailto"]

Um die Mehrfachnennungen zu entfernen, können wir auf das Ergebnis noch die vordefinierte Listenmethode unique() anwenden.

 groovy> bookmarks.protocol.unique()
 ===>["http", "mailto"]

Bei Methodenaufrufen ist die automatische Weiterleitung an die Listenelemente nicht möglich, stattdessen kann aber der Spread-Dot-Operator verwendet werden.

Der Spread-Dot-Operator

Bearbeiten

Nun interessiert uns, in welchen der URLs das Wort „Groovy“ vorkommt. Wir schreiben:

 groovy> println bookmarks.toString().contains('Groovy')
 true

Das Ergebnis überrascht jetzt etwas. Wir hätten analog zum ersten Versuch natürlich erwartet, dass wir eine Liste von Wahrheitswerten bekommen. Das Problem ist, dass die Methode toString() auch für die Klasse List definiert ist und daher dort aufgerufen wird. Dabei entsteht ein String, der auch das Wort „Groovy“ enthält. Das Ergebnis ist korrekt, aber nicht das Erwartete. Damit wir auch in diesen Fällen auf die Elemente der Liste durchgreifen können, bietet Groovy einen speziellen, aus einem Stern und einem Punkt (*.) bestehenden Operator, der auch als Spread-Dot-Operator bezeichnet wird. Hinter der Referenz auf eine Liste sorgt er dafür, dass die nachfolgende Referenzierung eines Feldes, einer Property oder eine Methode sich nicht auf die Liste selbst, sondern deren Elemente bezieht.

 groovy> println bookmarks*.toString()*.contains('Groovy')
 [false, true, true, false]

Auch hinter dem toString()-Aufruf müssen wir einen Stern einfügen, denn bookmarks*.toString() liefert wiederum eine Liste von Strings, und der contains()-Aufruf würde sich sonst auf die Liste beziehen und feststellen, ob eines ihrer Elemente der String „Groovy“ ist – und schlicht false liefen.

Sicheres Dereferenzieren mit ?.

Bearbeiten

Angenommen, wir wollten alle unsere URLs durchgehen und deren Query-Strings ausdrucken, wobei diese allerdings an den Und-Zeichen in ein String-Array aufgespalten werden sollen. Das geht beispielsweise in einer Schleife:

 groovy> for (url in bookmarks) { println url.query.split('&') }
 Exception thrown: java.lang.NullPointerException: Cannot invoke method contains() on null object

Autsch, das ging schief. Warum? Die Methode getQuery() liefert null, wenn im URL kein Query-String enthalten ist. Und beim Aufruf von split() an einer Nullreferenz wird natürlich eine NullPointerException ausgelöst. Ein Mittel dagegen besteht darin, vor dem kritischen Punkt im Pfad ein Fragezeichen einzufügen. Wenn das Dereferenzieren des Pfades an dieser Stelle den Wert null ergibt, wird diese Referenz nicht ausgeführt sondern gleich die null zurückgegeben.

 groovy> for (url in bookmarks) { println url.query?.split('&') }
 null
 {"hl=de", "q=Groovy", "btnG=Google-Suche", "meta="}
 null
 null

Nun verzichtet Groovy darauf, wenn der Aufruf von getQuery() den Wert null liefert, auch noch split() aufzurufen und damit die Exception zu provozieren.

Diese Fragezeichen-Notation für Navigationspfade in Objektstrukturen ist vor allem dann außerordentlich praktisch, wenn man an vielen Stellen mit Nullreferenzen rechnen muss. Angenommen, Sie hätten in Java einen Pfad wie diesen:

 x = getRef1().getRef2().getRef3().toString();

Wenn jede dieser Referenzen null sein kann, müssen Sie schreiben:

 if (getRef1()!=null && getRef1().getRef2()!=null
       &&getRef1().getRef2().getRef3()!=null) {
   x = getRef1().getRef2().getRef3().toString();
 } else {
   x = null;
 }

Eine Alternative ist das Einkleiden in einen Try-Catch-Block:

 try {
   x = getRef1().getRef2().getRef3().toString();
 } catch (NullPointerException x) {
   x = null;
 }

Dieses Vorgehen ist aber eher problematisch, weil Sie nicht es nicht mehr bemerken können, wenn eine der Getter-Methoden auf dem Navigationspfad aus irgendeinem anderen Grund eine NullPointerException auslöst. Da sind Sie mit Groovy immer auf der sicheren Seite, wenn Sie einfach schreiben:

 x = getRef1()?.getRef2()?.getRef3()?.toString()

Oder in der Property-Notation:

 x = ref1?.ref2?.ref3?.toString()

Projekt: Genealogie

Bearbeiten

Der Umgang mit langen GPath-Ausdrücken lässt sich am besten an umfangreichen Objektnetzen demonstrieren, z.B. einem Familienstammbaum. Um eine einfache Ahnentafel oder einen Familienstammbaum aufzubauen, genügt eigentlich eine ganz einfache Klasse, wir nennen sie Person, und sie ist in folgendem Beispiel abgebildet.

 class Person {
   def name
   def vater, mutter
   def kinder = []
   
   def getEltern() {
     [vater,mutter].findAll {it != null}
   }
     
   void setVater(person) {
     vater = person
     vater.kinder.add(this)
   }
   
   void setMutter(person) {
     mutter = person
     mutter.kinder.add(this)
   }
   
   String toString() { name }
 }

Die Person hat einen Namen, zwei Referenzen auf Vater und Mutter, eine Liste mit Kindern, eine Quasi-Property eltern, die eine Liste aus Vater und Mutter bildet und dabei die Null-Objekte gleich herausfiltert. Außerdem hat sie noch zwei Setter, die nicht nur Vater oder Mutter setzen, sondern die Person gleich noch bei diesen als Kind eintragen, sowie eine toString()-Methode, die den Namen liefert. Das genügt erst einmal.

Ein ganzes Netz von Eltern-Kind-Beziehungen können wir aufbauen, indem wir die Personen in folgender Weise instanziieren:

 wilhelm = new Person(name:'Wilhelm')
 elfriede = new Person(name:'Elfriede')
 otto = new Person(name:'Otto')
 henriette = new Person(name:'Henriette')
 karl = new Person(name:'Karl', vater:wilhelm, mutter:elfriede)
 hanna = new Person(name:'Hanna', vater:otto, mutter:henriette)
 helene = new Person(name:'Helene', vater:wilhelm, mutter:elfriede)
 egon = new Person(name:'Egon', mutter:henriette)
 franz = new Person(name:'Franz', vater:karl)
 ina = new Person(name:'Ina',vater:karl)
 kurt = new Person(name:'Kurt',vater:karl, mutter:hanna)
 heinz = new Person(name:'Heinz',mutter:hanna)
 gigi = new Person(name:'Gigi',mutter:ina)
 klaus = new Person(name:'Klaus',mutter:ina)
 manuela = new Person(name:'Manuela',vater:kurt)
 jeanette = new Person(name:'Jeanette',vater:heinz)

Hieran können wir nun verschiedene verwandtschaftliche Beziehungen durch Navigation durch das Geflecht abfragen.

 groovy> println 'Großeltern von Kurt' 
 groovy> println kurt.eltern.eltern
 groovy> println 'Geschwister von Kurt'
 groovy> println (kurt.eltern.kinder.unique() - kurt)
 groovy> println 'Onkel und Tanten von Kurt'
 groovy> println (kurt.eltern.eltern.kinder.unique() - kurt.eltern)
 groovy> println 'Cousins von Manuela'
 groovy> println ((manuela.eltern.eltern.kinder.unique()-manuela.eltern).kinder)
 groovy> println 'Neffen und Nichten von Kurt'
 groovy> println ((kurt.eltern.kinder.unique() - kurt).kinder.unique())
 Großeltern von Kurt
 [Wilhelm, Elfriede, Otto, Henriette]
 Geschwister von Kurt
 [Franz, Ina, Heinz]
 Onkel und Tanten von Kurt
 [Helene, Egon]
 Cousins von Manuela
 [Gigi, Klaus, Jeanette]
 Neffen und Nichten von Kurt
 [Gigi, Klaus, Jeanette]

Diese Abfragen arbeiten alle mit Listen, die nie leer sind und daher immer funktionieren. Bei den folgenden Abfragen können aber Referenzen null sein, daher ist der Fragezeichen-Operator am Platze.

 groovy> println 'Name des Großvaters mütterlicherseits von Kurt, Egon und Jeannette' 
 groovy> def grossvaterM(p) { p.mutter?.vater?.name }
 groovy> println grossvaterM(kurt)
 groovy> println 'Von Egons Mutter ist der Vater nicht bekannt.'
 groovy> println grossvaterM(egon)
 groovy> println 'Von Jeannette ist die Mutter nicht bekannt.'
 groovy> println grossvaterM(jeanette)
 Name des Großvaters mütterlicherseits von Kurt, Egon und Jeannette
 Otto
 Von Egons Mutter ist der Vater nicht bekannt.
 null
 Von Jeannette ist die Mutter nicht bekannt.
 null

Sehen Sie, so leicht können Sie auch Ihre Verwandtschaft ergründen. Wenn Sie sich allerdings auch noch Halbschwestern, Schwiegeronkel und Schwippschwager anzeigen lassen möchten, müssen Sie die Klasse Person noch etwas erweitern. Dies sei Ihnen zur Übung selbst überlassen.


Nachdem wir uns nun eingehend mit den verschiedenen Möglichkeiten beschäftigt haben, wie man in Groovy Klassen und Skripte – wobei letztere im Grund auch Klassen sind – programmieren kann, kennen Sie nun alle wesentlichen Veränderungen und Neuerungen der Sprache Groovy gegenüber Java. Aber Groovy ist nicht nur eine Programmiersprache; dazu gehört auch eine umfangreiche Klassenbibliothek, mit deren Hilfe die neuen Möglichkeiten der Sprache erst richtig zum Tragen kommen. Ihr wenden wir uns ab dem folgenden Kapitel zu. Dabei beschäftigen wir uns zunächst mit denjenigen APIs in der Groovy-Library, die für die meisten Java-Programmierer Neuland sein dürften.