Zum Inhalt springen

Groovy: Vordefinierte Methoden

Aus Wikibooks

Überall in diesem Buch kommen in den Beispielprogrammen Aufrufe einer Methode println() vor, die offensichtlich für die Klassen, in denen sich diese Aufrufe befinden, nicht definiert ist. Im Kapitel Objekte haben Sie erfahren, dass in Groovy die meisten Operatoren durch spezielle Methoden implementiert werden, beispielsweise der Plus-Operator (+) durch die Methode plus(). Trotzdem können wir zwei Integer-Zahlen addieren, obwohl die Klasse Integer keine Methode plus() besitzt. Wir wollen uns ansehen, wie Groovy dies zustande bringt und wie wir diesen Mechanismus in eigenen Programmen nutzen können.

Das Prinzip

[Bearbeiten]

Wenn Groovy versucht, einen Methodenaufruf bei einem gegebenen Objekt durchzuführen, sucht es genau wie Java diejenige Methode, die für die Klasse des Objekts definiert oder von einer Basisklasse geerbt ist und am besten zu den übergebenen Parametern passt (allerdings findet die Zuordnung von Aufruf und aufrufbaren Methoden bei Java bereits beim Kompilieren statt; wenn es eine solche Zuordnung nicht gibt, führt dies zu einem Fehler). Wenn Groovy nun keine passende Methode findet, sieht es anschließend in einer zur Laufzeitumgebung von Groovy gehörenden Klasse org.codehaus.groovy.runtime.DefaultGroovyMethods nach ‒ im statischen Kontext ist es DefaultGroovyStaticMethods im selben Package ‒, ob sich darin eine statische Methode gleichen Namens befindet, deren erster Parameter von einem Typ ist, mit dem das Objekt zuweisungskompatibel ist, und deren restliche Parameter mit den Argumenten des Methodenaufrufs übereinstimmen. Und wenn es eine solche findet, ruft es diese anstelle einer für das Objekt definierten Methode auf.[1]

So enthält DefaultGroovyMethods beispielsweise die folgende Methode:

public static void println(Object self, Object value) {
  System.out.println(InvokerHelper.toString(value));
}

Wenn Sie nun in Ihrem Programm irgendwo einen Aufruf wie println('text') haben, und in der betreffenden Klasse eine solche Methode nicht bereits definiert ist, findet Groovy die obige Methode, denn in welcher Klasse sich der Aufruf auch immer befindet, muss diese doch zuweisungskompatibel zu Object sein, und der zweite Parameter der Methode passt zum ersten und einzigen Argument des Aufrufs. Diese println()-Methode macht nun nichts weiter, als das übergebene Objekt mit Hilfe einer Hilfsklasse in einen String umzuwandeln und an die Standardausgabemethode System.out.println() weiterzureichen.

Ein weiteres Beispiel ist die Methode plus(), die in DefaultGroovyMethods zu finden ist und es ermöglicht, dass der Plus-Operator auch bei den numerischen Standardtypen funktioniert:

public static Number plus(Number left, Number right) {
  return NumberMath.add(left, right);
}

Wenn Sie nun in Ihrem Code einen Ausdruck wie a+b mit zwei Integerzahlen haben, wandelt Groovy diesen in einen Methodenaufruf a.plus(b) um; eine entsprechende Methode ist in der Klasse Integer nicht zu finden, aber DefaultGroovyMethods.plus(a,b) ist möglich, und somit wird ersatzweise diese Methode ausgeführt.

Die vordefinierten Methoden kommen immer nur dann ins Spiel, wenn nicht das aktuelle Objekt selbst, bei dem die Methode aufgerufen werden soll, oder eines seiner Vorfahren eine zum Aufruf passende Methode implementiert. Folgendes Beispielskript soll dies demonstrieren.

class EineKlasse { }

k = new EineKlasse()
k.println('Hallo')

Wenn Sie dieses Skript laufen lassen, wird der Text »Hallo« ausgegeben, da die Klasse EineKlasse selbst keine Methode println() implementiert, es aber eine passende vordefinierte Methode gibt. Bauen wir die Klasse nun so um, dass sie eine Implementierung von println() enthält:

class EineKlasse {
  def println(text) {
    System.out.println("EineKlasse: $text")
  }
}

k = new EineKlasse()
k.println('Hallo')

Unsere eigene println()-Implementierung funktioniert ähnlich wie das Original, fügt aber etwas vor dem ausgegebenen Text ein. Starten Sie dieses Skript, und es erscheint tatsächlich der Text »EineKlasse: Hallo« auf der Konsole.

Dieses Verhalten entspricht weitgehend dem Überschreiben von Methoden in abgeleiteten Klassen. So können Sie auch eine der Elternklasse zugeordnete vordefinierte Methode mit dem Präfix super adressieren. Der Vorteil dieses Vorgehens ist, dass auch manche Klassen der Java-Standardbibliothek Methoden aufweisen, die mit korrespondierenden vordefinierten Methoden übereinstimmen. In solchen Fällen wird die spezifische, in der Klasse definierte Methode aufgerufen und nicht die von Groovy vordefinierte Variante. Ein Beispiel hierfür ist die Methode iterator(), die von allen Collection-Klassen implementiert wird, gleichzeitig aber auch für alle Objekte vordefiniert ist. Es ist also, als hätten diese Standardklassen die vordefinierte Methode »überschrieben«.

Vordefinierten Methoden für alle Objekte

[Bearbeiten]

Es gibt zahlreiche Methoden, die für verschiedene Standard-Klassen und -Interfaces vordefiniert sind. Wir wollen uns zunächst denjenigen zuwenden, die für den Typ Object verfügbar sind, also überall im Programm verwendet werden können. Manche von ihnen sind für bestimmte Typen überladen.

Object asType (Class zielTyp)
Wandelt das aktuelle Objekt in eine Instanz des genannten Typs ‒ sofern dies möglich ist. Die Methode implementiert das Schlüsselwort as zur Typanpassung und wird für viele spezifische Typen überladen.
String dump()
Generiert einen zusammenfassenden String aus Klassennamen, Hashcode und Feldern des Objekts, der besonders für Debugging-Zwecke geeignet ist.
ProxyMetaClass getMetaClass()
Ordnet dem aktuellen Objekt eine ProxyMetaClass-Instanz zu, falls dies nicht bereits geschehen ist, und übergibt diese als Ergebnis. Auf diesem Weg kann man einer Klasse bequem neue Methoden zuordenen. Näheres dazu im Kapitel Dynamisches Programmieren.
Map getProperties ()
Liefert eine Map mit den Namen und Werten aller für das Objekt definierten Properties.
Object identity (Closure c)
Ruft die Closure mit dem aktuellen Objekt selbst als Argumente auf. Objektreferenzen in der Closure werden ebenfalls über das aktuelle Objekt aufgelöst, da es gleichzeitig als Delegate angemeldet ist. objekt.identity { closure } ist also ungefähr gleichbedeutend mit closure.delegate=objekt; closure.call (objekt). Die Konstruktion ähnelt der with-Anweisung in in Basic; man kann sie verwenden, wenn man auf viele Member eines Objekts zugreifen will. Ein typischer Anwendungsfall sind Plausibilitätsprüfungen an einem Domain-Objekt:
def validatePerson (person) {
  person.identity {
    if (vorname==null || vorname.length()<2) {
      throw new ...
    }
    // usw.
  }
}
String inspect ()
Generiert einen String, dessen Inhalt die Form, mit der in einem Groovy-Programm ein Objekt mit demselben Inhalt wie das aktuelle Objekt gebildet werden könnte. Wenn dies nicht möglich ist, liefert die Methode dasselbe Ergebnis wie toString().
Beispiel: Das Ergebnis von [1,2,3].inspect() ist ein String mit dem Inhalt "[1,2,3]".
Boolean is (Object obj)
Prüft, ob das aktuelle Objekt identisch mit dem als Argument übergebenen Objekt ist. Diese Methode wird benötigt, da der Gleichtheitsoperator (==) in Groovy die equals()-Methode aufruft und nicht, wie in Java, auf Objektidentität prüft.
Boolean isCase (Object switchObjekt)
Prüft, ob das als Argument übergebene switch-Objekt in einer switch-case-Verzeweigung ein »Fall« des aktuellen Objekts ist. Wenn diese Methode nicht überladen ist, gibt sie das Ergebnis eines Aufrufs von equals(switchObject) zurück. Dass diese Methode für die Klasse Object definiert ist, bedeutet, dass jedes beliebige Objekt als case verwendet werden kann.
void print (Object obj)
Ruft System.out.print (obj) auf.
void print (PrintWriter writer)
Gibt eine String-Repräsentation des aktuellen Objekts über die print()-Methode des angegebenen PrintWriter aus.
void printf (String format, Object[] args)
Ruft System.out.printf(format, args) auf. Funktioniert nur ab Java 5.0.
void println (Object obj)
Ruft System.out.println (obj) auf.
void println (PrintWriter writer)
Gibt eine String-Repräsentation des aktuellen Objekts über die println()-Methode des angegebenen PrintWriter aus.
void println ()
Gibt einen Zeilenvorschub über System.out.println() aus.
void putAt (String name, Object wert)
Ermöglich den schreibenden Zugriff auf die Properties eines Objekts über den Property-Namen wie auf den Index einer Map.
static void sleep (long milliseconds, Closure onInterrupt=null)
Unterbricht die Ausführung für die angegebene Anzahl von Millisekunden. Unterbrechungen werden während dieser Zeit intern abgefangen; man kann also davon ausgehen, dass die angegebene Zeit ungefähr eingehalten wird. Wenn eine Closure angegeben ist, wird sie im Fall einer Unterbrechung mit der InterruptedException als Argument aufgerufen.
void use (Class[] kategorien, Closure c)
void use (List kategorien, Closure c)
Die beiden Methoden bewirken die Ausführung der übergebenen Closure, dabei werden bei allen innerhalb der Closure (auch indirekt) ausgelösten Methodenaufrufen die in den Kategorienklassen definierten Methoden als vordefinierte Methoden berücksichtigt (näheres dazu im Kapite ##DYNAMIK##). Die beiden Methoden unterscheiden sich darin, dass bei der einen eine variable Anzahl einzelner und bei der anderen eine Liste von Kategorienklassen angegeben werden kann.

Vordefinierte Methoden zur Iteration über Objektmengen

[Bearbeiten]

Es gibt eine Reihe vordefinierter Methoden, die speziell für Container, Files, Streams und andere Objekte vorgesehen sind, die den Zugriff auf ein oder mehrere Elemente organisieren. Diese iterativen Methoden bilden mit Hilfe eines Aufrufs von iterator() einen Iterator über den Inhalt des aktuellen Objekts und führen eine bestimmte Aktion für jedes der vom Iterator gelieferten Elemente aus. Die meisten von ihnen rufen eine als Argument angegebene Closure auf und reagieren in unterschiedlicher Weise auf das Ergebnis des Closure-Aufrufs. Alle diese Methoden stützen sich allein auf das Vorhandensein der iterator()-Methode bei dem aktuellen Objekt, sie setzen aber nicht die Implementierung eines bestimmten Interfaces voraus. Tatsächlich funktionieren sie prinzipiell mit beliebigen Objekten, da bereits der Klasse Object eine vordefinierte iterator()-Methode zugeordnet ist:

Iterator iterator ()

Sie bildet einen Iterator über im Objekt enthaltene Elemente. Im einfachsten Fall ist es nur ein Iterator über das aktuelle Element selbst. Die Methode wird nicht nur in den weiter unten aufgeführten vordefinierten iterierenden Methoden verwendet, sondern auch in der Implementierung der Groovy-eigenen for-Schleife.

groovy> for (element in new Object()) { println element }
java.lang.Object@14807d9

Die for-Schleife in dem folgenden Skript gibt nacheinander die Werte der drei Properties (100, »alpha« und das aktuelle Datum) aus, da die iterator()-Methode der Klasse K einen entsprechenden Iterator liefert.

class K {
  def a = 100
  def b = 'alpha'
  def c = new Date()
  def iterator() {
    this.properties.values().iterator()
  }
}

for (element in new K()) { println element }

Für zahlreiche Typen ist iterator() so überschrieben, dass sie eine für den jeweiligen Typ sinnvollen Iterator liefert. Die entsprechenden Methodendefinitionen befinden sich teilweise in der jeweiligen Klasse selbst und teilweise in DefaultGroovyMethods. Die folgende Tabelle gibt eine Übersicht einiger dieser Typen und beschreibt, welchen Typ von Elementen der Iterator jeweils liefert.

Typ des Objekts Elemente des Iterators
null Keine (leerer Iterator)
Beliebiges Array Inhaltselemente des Arrays; die, wenn es ein Array aus primitiven Werten ist, in die korrespondierenden Wrapper-Typen umgewandelt werden.
java.lang.String Im String enthaltene Zeichen
java.util.Collection In der Collection enthaltene Objekte
java.util.Enumeration Von der Enumeration gelieferte Objekte
java.util.Iterator Die Elemente des Iterators selbst
java.util.Map Map.Entry-Objekte
java.util.regex.Matcher Gefundene Teilsequenzen (Ergebnisse des Aufrufs von group())
java.io.File Zeilen der Datei
java.io.InputStream Im Stream enthaltene Bytes
java.io.Reader Eingabezeilen
org.w3c.dom.NodeList In der NodeListe enthaltene Node-Objekte.

Alle folgenden vordefinierten Methoden durchlaufen die Elemente des Iterators.

Boolean any (Closure c)
Prüft, ob der Aufruf der Closure bei mindestens einem der Elemente das Ergebnis true liefert.
Beispiel: [1,2,3].any {it==2 } liefert true, denn eines der Elemente ist gleich 2.
List collect (Closure c)
Sammelt alle Ergebnisse der Closure-Aufrufe in einer Liste.
Beispiel: [1,2.3].collect {it * 2 } liefert als Ergebnis die Liste [2,4,6].
List collect (Collection coll, Closure c)
Fügt alle Ergebnisse der Closure-Aufrufe der angegebenen Collection hinzu.
Beispiel: [1,2,3].collect (['a','b']) { it.toString() } liefert die verlängerte Liste ['a','b','1','2','3'].
void each (Closure c)
Ruft die Closure für jedes Element auf, ohne den Ergebniswert zu berücksichtigen.
Beispiel: [1,2,3].each { println it } gibt alle Elemente der Reihe nach aus.
void eachWithIndex (Closure c)
Ruft die Closure für jedes Element auf, ohne den Ergebniswert zu berücksichtigen. Dabei wird der Index des Elements, bei 0 beginnend, der Closure als zweites Argument übergeben.
Beispiel: [1,2,3].eachWithIndex {elem, index -> println "Nr. $index = $elem"} gibt alle Element in einer nummerierten Liste aus.
Boolean every (Closure c)
Prüft, ob die Closure für alle Elemente true liefert. Folgende Anweisung gibt true zurück, weil alle Elemente größer als 0 sind:
[1,2,3].every { it > 0 } 
Object find (Closure c)
Liefert das erste Element zurück, für das der Closure-Aufruf true ergibt. Wenn dies bei keinem der Elemente der Fall ist, wird null zurückgegeben.
Beispiel: [1,2,3].find { it > 1 } gibt das Element 2 zurück, da bei ihm als erstes die Bedingung erfüllt ist.
List findAll (Closure c)
Sammelt alle Elemente, für das der Closure-Aufruf true ergibt, in einer Liste. Wenn dies bei keinem der Elemente der Fall ist, wird eine leere Liste zurückgegeben.
Beispiel: [1,2,3].findAll { it > 1 } gibt die Liste [2,3] zurück, da bei diesen beiden Elementen die Bedingung erfüllt ist.
Integer findIndexOf (Closure c)
Liefert den Index des ersten Elements zurück, für das der Closure-Aufruf true ergibt, beginnend mit 0. Wenn die Closure bei keinem der Elemente true liefert, wird die Anzahl der Elemente zurückgegeben.
Beispiel: [10,20,30].findIndexOf { it > 10 } gibt den Wert 1 zurück, da bei dem zweiten Element als erstes die Bedingung erfüllt ist.
List grep (Object obj)
Ermittelt einen Iterator über das aktuelle Objekt und liefert alle Elemente daraus, die mit dem als Argument übergebenen Objekt übereinstimmen, als Liste zurück. Ob es eine Übereinstimmung gibt, wird durch einen Aufruf von obj.isCase(element) ermittelt.
Beispiel: [10,20,30].grep(15..25) liefert die einelementige Liste [20], denn nur der Wert 20 ist im Wertebereich 15..25 enthalten.

  1. Die Klasse DefaultGroovyMethods enthält inzwischen weit über 300 Methoden, und ein Rafactoring wird bereits seit einiger Zeit diskutiert. Eine neue Implementierung wird vermutlich die vordefinierten Methoden auf verschiedene Klassen verteilen, die über eine Namenslogik mit den korrespondierenden Typen verknüpft sind.