Groovy: Mit Objekten arbeiten

Auf den ersten Blick unterscheiden sich Groovy-Objekte nicht wesentlich von Objekten in Java: Jedes Objekt ist Instanz einer Klasse, in der die Konstruktoren, die Methoden und die Felder festgelegt sind. Solche Groovy-Objekte können auch von Java-Programmen aus verwendet werden, und sie stellen sich in diesem Zusammenhang – zumindest auf den ersten Blick – kaum anders dar, als wären sie ebenfalls in Java programmiert.

Sie sind aber nicht in Java programmiert, sondern in einer Programmiersprache, die sich Einfachheit und Flexibilität auf die Fahnen geschrieben hat. Und das kann man sich als Groovy-Programmierer zunutze machen.

Wenn Sie in Groovy ein Programm schreiben, müssen Sie zunächst unterscheiden, ob Sie dies in Form einer Klasse oder in der Form eines Skripts tun. Skripte sind einfach zu erstellen, weil Sie weder Klasse noch main()-Methode explizit definieren müssen und einige Freiheiten im Aufbau des Programms haben. Daher bieten sie sich vor allem für kleine, einfache Aufgaben an. Auch wenn Sie Groovy-Programme dynamisch in Java-Anwendungen integrieren, ist das Groovy-Skript dafür die geeignete Form. Auf die Besonderheiten des Skript-Schreibens gehen wir unter Skriptobjekte noch ausführlicher ein; zunächst einmal kommen wir auf die eher gewohnte Form der Groovy-Klassen zu sprechen, die sicher vorzuziehen ist, sobald es komplexer wird oder Sie Programmteile wieder verwenden möchten.

Package-Struktur Bearbeiten

Genau wie Java-Klassen werden auch Groovy-Klassen in Packages organisiert. Der Name des Packages muss am Anfang der Quelldatei vor den Import-Anweisungen und der ersten Klassendefinition genannt sein. Dabei können Sie die gewohnte Form der Strukturierung anhand von Domain-Namen in umgekehrter Reihenfolge verwenden, aber niemand zwingt Sie dazu.

package de.oreilly.groovy.beispiele.kap3

Wenn Sie in Ihrem Programm Klassen oder Interfaces aus anderen Packages referenzieren, müssen Sie entweder an Ort und Stelle den vollen Pfadnamen verwenden, oder sie vor dem Beginn Ihrer Klassendefinition importieren. Sie können auch alle Klassen und Interfaces eines Packages en bloc importieren, indem Sie anstelle des Klassennamens einen Stern einsetzen. Dabei macht es keinen Unterschied, ob die importierte Klasse eine Java- oder Groovy-Klasse ist.

import groovy.inspect.swingui.TableSorter
import javax.swing.*

Auch statische Imports sind erlaubt, und dies sogar, wenn Sie noch mit Java 1.4 arbeiten.

import static java.lang.Math.*

Insoweit nicht viel Neues. Anders als Java brauchen Sie in Groovy-Programmen jedoch die am häufigsten verwendeten Packages nicht eigens zu importieren, da Groovy dies schon für Sie vornimmt. Die folgenden Import-Anweisungen sind also alle überflüssig:

import java.lang.* // Dieses Package wird auch von Java importiert
import java.util.*
import java.net.*
import java.io.*
import java.math.BigInteger
import java.math.BigDecimal
import groovy.lang.*
import groovy.util.*

Außerdem ermöglicht Ihnen Groovy, Alias-Namen für importierte Klassen und Interfaces zu vergeben. Dadurch können Sie zwischen Typen mit gleich lautenden Namen unterscheiden oder lange Typnamen abkürzen.

import java.awt.ContainerOrderFocusTraversalPolicy as COFTP
import java.sql.Date as SqlDate
import javax.naming.Binding as JndiBinding
. . .
COFTP myPolicy = new COFTP()
def myDate = new SqlDate(106,11,3)
JndiBinding binding = new JndiBinding("Date",myDate)

Am Beispiel JndiBinding erkennen Sie, dass die Alias-Namen in Groovy auch wegen der vielen automatisch importierten Packages eine hilfreich sein können, da die Gefahr von Namenskonflikten viel größer ist als in Java. Eine Klasse namens Binding gibt es auch schon in dem Package groovy.lang, das von Groovy automatisch importiert wird. Durch ein normales import von javax.naming.Binding hätten Sie also zwei Klassen mit demselben einfachen Namen; die as-Klausel mit dem Alias-Namen hilft Ihnen, diesen Konflikt zu vermeiden.

Die Pfadstruktur der Packages muss sich genau wie in Java in der Verzeichnisstruktur widerspiegeln. Das heißt, die Quelldatei einer Groovy-Klasse im Package de.groovy-grails.buch.beispiele.kap3 muss – relativ zu einem Verzeichnis im Klassenpfad – im Verzeichnis de/groovy-grails/buch/beispiele/kap3 zu finden sein.

Klassenpfade Bearbeiten

Wenn Sie Ihre Groovy-Programme mit groovyc in Bytecode übersetzen und mit java oder javaw ausführen, gilt bezüglich der Klassenpfade dasselbe wie für jedes beliebige Java-Programm: Alle benötigten .class-Dateien müssen aus den im Klassenpfad aufgeführten Verzeichnissen und JAR-Dateien – unter Berücksichtigung der Package-Struktur – auffindbar sein. Der Klassenpfad setzt sich aus den JDK- und Extensions-Bibliotheken, dem Inhalt der Umgebungsvariablen CLASSPATH und den Argumenten des java-Aufrufparameters -classpath bzw. -cp zusammen. Sie müssen nur darauf achten, dass die von Groovy benötigten Bibliotheken (am besten groovy-all-….jar) in diesem Klassenpfad aufgeführt sind.

Wenn Sie ein Groovy-Programm mit dem Befehl groovy starten, sieht es etwas anders aus. Hinter dem Namen verbirgt sich ein Shell-Skript, das schon einiges für Sie erledigt, bevor es die Groovy-Hauptklasse groovy.ui.GroovyMain aufruft; unter anderem baut es schon einen Klassenpfad mit benötigten Bibliotheken zusammen. Wenn Sie groovy mit einem Klassennamen aufrufen sucht es diese Klasse nicht in einer Klassendatei (.class) sondern in einer Quelldatei (.groovy), und dieser muss im Klassenpfad zu finden sein. Das gleiche gilt, wenn Ihr Groovy-Programm andere Klassen referenziert, diese können normale .class-Dateien sein, die in der üblichen Weise lokalisiert werden, es können aber auch wiederum Groovy-Quelldateien sein, die über den Klassenpfad auffindbar sein müssen.

Da beim Aufruf von groovy letztendlich die Virtuelle Maschine von Java gestartet wird, gelten im Prinzip dieselben Pfadangaben wie oben genannt. Allerdings fügt Groovy noch einige weitere hinzu, die in der Konfigurationsdatei %GROOVY_HOME%/conf/groovy-starter.conf aufgeführt sind. Unter anderem finden Sie dort auch diese Zeilen:

# load user specific libraries
load ${user.home}/.groovy/lib/*

Dies bedeutet, dass alle Klassen-, JAR- und Groovy-Dateien, die Sie in das Verzeichnis .groovy/lib/ unterhalb Ihres Benutzerverzeichnisses legen, für alle Groovy-Programme, die Sie ausführen, ohne explizite Benennung im Klassenpfad verfügbar sind.

Tipp: Wenn Sie nicht genau wissen, wo Ihr Benutzerverzeichnis liegt, brauchen Sie nur in groovysh kurz
groovy> System.properties.get('user.home')

eingeben.


Die Möglichkeit, den Klassenpfad auf diese Weise zu erweitern, ist natürlich hilfreich, wenn Sie häufig andere als die standardmäßig eingebundenen Bibliotheken verwenden und diese nicht bei jedem Skript-Aufruf mit angeben möchten. Die Personalisierung des Klassenpfades kann aber auch dazu führen, dass Programme bei Ihnen anders funktionieren als bei anderen oder dass bei von Ihnen geschriebene Programme bei Ihnen laufen und bei anderen nicht. Eine gewissen Vereinheitlichung von gemeinsam genutzten Bibliotheken können Sie in einem Firmennetzwerk dadurch erreichen, dass Sie ein Verzeichnis für gemeinsam genutzte Bibliotheken auf einem Fileserver einrichten und in groovy-starter.conf auf dieses Verweisen.

Als Skript (also als .groovy-Datei) aufgerufene Programme werden bisweilen unhandlich, wenn Sie vor der Verwendung erst dafür sorgen müssen, dass der Klassenpfad richtig gesetzt wird. Es ist aber durchaus möglich, in einem Groovy-Programm den Klassenpfad dynamisch zu erweitern.

Angenommen, Sie möchten ein Skript schreiben, das eine Email versendet, und dazu die Klasse SimpleEmail in der Open-Source-Bibliothek commons-email-1.jar verwenden. Dabei soll aber nicht erst vor Aufruf des Skripts der Klassenpfad gesetzt werden müssen. Um dies zu bewerkstelligen, müssen Sie sich zuerst den aktuellen Root-Loader holen; dies ist eine spezielle, Groovy-eigene Implementierung des ClassLoader, dessen Klassenpfad zur Laufzeit erweiterbar ist.

def rootLoader = this.class.classLoader.rootLoader
assert rootLoader!=null

In normalen, mit groovy gestarteten Skripten oder Programmen ist dieser Root-Loader immer vorhanden; wenn Groovy-Klassen oder -Skripte innerhalb von Java-Programmen verwendet werden, ist dies jedoch nicht unbedingt der Fall; die dynamische Erweiterung des Klassenpfades ist dann nicht möglich.

Sie können nun mit addUrl() dem Classloader das zusätzliche Quellcode-Verzeichnis oder die zusätzliche JAR-Datei in Form einer URL hinzufügen.

rootLoader.addURL(new URL("file:///C:/java/commons-email-1.0/commons-email-1.0.jar"))

Holen Sie sich nun die gewünschte Klasse als Class-Objekt und verwenden Sie es dazu, per Reflection eine Instanz der benötigten Klasse zu erzeugen. Es ist nicht möglich, hier einfach new org.apache.commons.mail.SimpleEmail() den Konstruktor dieser Klasse aufzurufen, denn diese kann dem Compiler ja noch nicht bekannt sein. Nachdem Sie aber erst einmal eine Instanz von SimpleEmail in der Hand haben, können Sie es behandeln wie jedes andere Objekt in Groovy.

def emailClass = Class.forName("org.apache.commons.mail.SimpleEmail")
def email = emailClass.newInstance()
email.addTo("tim@oreilly.de","Tim O'Reillly")
email.hostName = "mail.myserver.com"
email.from = "ich@user.org"
email.subject = "Testnachricht"
email.msg = "Dies ist eine Testnachricht."
email.send()

Klassen definieren und verwenden Bearbeiten

Im Unterschied zu Java müssen Klassennamen und Dateinamen in Groovy nicht unbedingt übereinstimmen. Sie können durchaus mehrere öffentliche Klassen in einer Datei haben, und sie können anders benannt sein als die Datei. Lediglich wenn sich ungebundener Skriptcode in der Quelldatei befindet, darf keine der explizit in der Datei definierten Klassen denselben Namen haben wie die Quelldatei selbst.

Wenn Sie eine Quelldatei mit mehreren Klassendefinitionen kompilieren, entstehen mehrere .class-Dateien mit den Namen der Klassen, die Sie normal verwenden können.

Wenn Sie die Quelldatei nicht kompilieren, sondern mit groovy direkt ausführen, müssen Ihre Klassen allerdings von dem Groovy-eigenen Classloader gefunden werden können, der trotzdem nach einer Datei mit einem Namen der Form Klassenname.groovy sucht. Es genügt aber, wenn nur eine der in einer Datei befindlichen Klassen mit dem Dateinamen übereinstimmt und diese als erste gesucht wird. Groovy kompiliert dann auch die anderen Klassen in der Datei intern, sodass sie anschließend im Programm verwendet werden können.

Die standardmäßige Sichtbarkeit von Klassen ist in Groovy public, Sie brauchen also keinen Sichtbarkeitsmodifikator angeben, wenn eine Klasse aus anderen Packages heraus verwendbar sein soll. Wenn Sie das verhindern möchten, müssen Sie protected voranstellen. Dies hat praktisch dieselbe Auswirkung, als würden Sie in Java gar keinen Sichtbarkeitsmodifikator angeben.[1]

class ErsteKlasse {
//Diese Klasse ist public
}

protected class ZweiteKlasse {
//Diese Klasse ist package-sichtbar
}

Ihre Groovy-Klassen können wie in Java von anderen Klassen abgeleitet sein, dabei gelten dieselben Mechanismen, die Sie von Java her kennen. Die Klassen, von denen Sie ableiten, können natürlich auch in Java programmiert sein. Dasselbe gilt für die Implementierung von Interfaces.

Tipp: Die Verwendung eigener Interfaces innerhalb von Groovy-Programmen ist in der Regel nicht erforderlich, weil das Vorhandensein bestimmter Methoden und Properties ohnehin erst zur Laufzeit am betreffenden Objekt überprüft wird; Interfaces sind in Groovy also kein Mittel, Typsicherheit zur Kompilierzeit herzustellen. Gleichwohl ist es durchaus möglich, Interfaces auch in Groovy zu definieren. Sie entsprechen in jeder Hinsicht ihren Java-Vorbildern; daher werden wir uns mit dem Erstellen von Interfaces in Groovy nicht näher beschäftigen.

Benötigt werden Interfaces in Groovy vor allem, um mit bestehenden Java-Klassen und Klassenbibliothek zusammenzuarbeiten.

Ausführbare Klassen Bearbeiten

Damit eine Groovy-Klasse direkt aus der Quelldatei mit groovy ausgeführt werden kann, muss eine der folgenden Bedingungen erfüllt sein.

Zum einen kann es sich wie bei normalen Java-Programmen um eine Hauptklasse mit einer main()-Methode handeln.

class Hauptklasse {
   static void main(args) {
       println "Hallo Welt"
   }
}

Die zweite Möglichkeit ist eine Klasse, die das Interface Runnable implementiert. Hier wird die Methode run() aufgerufen.[2]

class RunnableKlasse implements Runnable {
  void run() {
    println "Hallo Welt"
  }
}

Drittens kann es eine Klasse sein, die von groovy.util.GroovyTestCase oder groovy.util.GroovyTestSuite abgeleitet ist. In diesem Fall wird die Klasse als JUnit-Test angesehen und durch den Groovy-eigenen Testrunner abgearbeitet (siehe Groovy im Entwicklungsprozess).

class ReverseTest extends GroovyTestCase {
  public void testReverse() {
    assertEquals "tleW ollaH", "Hallo Welt".reverse()
  }
}

Schließlich kann das Programm auch die Form eines Skripts mit ungebundenen Anweisungen haben; in diesem Fall wird der Skriptcode direkt ausgeführt.

Das Interface GroovyObject Bearbeiten

Jedes vom Groovy-Compiler erzeugte Objekt implementiert (neben den im Programm angegebenen Interfaces) das Interface GroovyObject. Da das Objekt nicht von einer Groovy-eigenen Oberklasse abgeleitet sein darf – Sie könnten sonst selbst keine Ableitungshierarchien bilden – müssen die in GroovyObject deklarierten Methoden vom Compiler generiert werden. In der Regel delegieren sie allerdings nur über mehrere Zwischenstationen an ein sogenanntes Metaobjekt weiter, das die eigentliche Arbeit macht. Das Metaobjekt gibt Ihnen diverse Möglichkeiten, in die Funktionalität einer Klasse einzugreifen; mehr dazu erfahren Sie in Dynamisches Programmieren.

Es ist von geradezu fundamentaler Wichtigkeit, dass Sie zumindest die Namen der in GroovyObject deklarierten Methoden kennen, denn wenn Sie diese überschreiben, wird Ihr Programm nicht mehr funktionieren, ohne dass Sie ohne weiteres die Ursache erkennen können. Das Interface GroovyObject ist folgendermaßen definiert:

// Java
package groovy.lang;

public interface GroovyObject {
  Object invokeMethod(String name, Object args);
  Object getProperty(String property);
  void setProperty(String property, Object newValue);
  MetaClass getMetaClass();
  void setMetaClass(MetaClass metaClass);
}

Eine Dokumentation der einzelnen Methoden finden Sie im Anhang Wichtige Klassen und Interfaces.


  1. Wie schon in Die Sprache erwähnt, behandelt Groovy protected-Deklarationen beim Aufruf derzeit nicht anders als public. Das Schlüsselwort protected hat also nur eine Auswirkung, wenn die betreffende Klasse aus einer Java-Klasse benutzt wird.
  2. Das Interface Runnable wird in Java eigentlich für Threads verwendet; in Groovy dient es unabhängig davon aber auch zur Kennzeichnung ausführbarer Klassen.