Übersicht:
Methoden-Dispatch
Bei einem Methodenaufruf gibt es in Java die Möglichkeit
Methoden zu überladen, das heißt mehrere Methoden zu
definieren, die den selben Namen, aber unterschiedliche
Parameterlisten besitzen. Es wird dabei immer zur
Kompilierungszeit entschieden, welche der Methoden
tatsächlich aufgerufen wird. In Groovy ist das
Überladen ebenfalls möglich. Es wird hier aber erst
dynamisch (zur Laufzeit) entschieden, also anhand der
Laufzeit-Typen der Parameter, welche Methode aufgerufen
wird. Der folgende Vergleich zwischen einer Java-Datei und der
entsprechenden Groovy-Datei soll dies verdeutlichen.
Java: class Dispatch
{
static int foo(Object o)
{
return 1;
}
static int foo(String s)
{
return 2;
}
public static void main(String[] args)
{
Object o = new Object();
Object s = "schall";
assert( foo(o) == 1);
assert( foo(s) == 1); //!
}
}
Groovy:
class Dispatch
{
static int foo(Object o)
{
return 1;
}
static int foo(String s)
{
return 2;
}
public static void main(String[] args)
{
Object o = new Object();
Object s = "rauch";
assert foo(o) == 1;
assert foo(s) == 2; //!
}
}
Die Methoden
müssen nicht zwingend mit static attribuiert
werden, dies vereinfacht nur das Beispiel. Nicht-statische
Methoden verhalten sich analog.
Das folgende Beispiel zeigt, dass eine DoubleDispatch nur dann
möglich ist, wenn eindeutig entschieden werden kann, welche
Methode aufgerufen werden soll. Eine Fehlermeldung ist hier
natürlich nur zur Laufzeit möglich.
class Dispatch
{
static int foo(Object o, String s)
{
return 1;
}
static int foo(String s, Object o)
{
return 2;
}
public static void main(String[] args)
{
Object o = new Object();
Object s = "rauch";
try {
assert foo(o, o) == 1;
assert false;
} catch(MissingMethodException e) {
assert true;
println "(o,o): ${e.message}\n";
}
try {
assert foo(s, s) == 2;
assert false;
} catch(GroovyRuntimeException e) {
assert true;
println "(s,s): ${e.message}\n";
}
}
}
Bei dem Aufruf mit zwei Object s deutet die
Fehlermeldung darauf, dass es gar keine Methode foo gibt, die
aufgerufen werden soll. Die Meldung beim Aufruf mit
zwei Strings weist dagegen eindeutig darauf hin,
dass nicht entschieden werden kann, welche Methode aufgerufen
werden soll. Zusätzlich erhält man eine Liste von
Methodensignaturen, die alle zu dem Aufruf passen.
In Java sind Methodenaufrufe normalerweise etwas
Statisches. Es gibt aber auch hier die Möglichkeit, mit Hilfe
der Methode
Object invokeMethod(String name, Object[] args)
Methoden dynamisch aufzurufen. In Groovy werden Methoden
implizit immer mittels invokeMethod
aufgerufen. Daher darf der . Operator auch einen
String anstatt einem Methodenbezeichner annehmen:
class A {
public int foo () {
return 42;
}
}
String s = "foo";
A a = new A();
assert 42 == a."foo"();
assert 42 == a.foo();
assert 42 == a."${s}"(); // not yet implemented
Vorsicht: Dieses Feature wurde noch nicht vollständig
implementiert.
Ein einfaches Beispiel, welches schon lauffähig ist:
public int foo () {
return 42;
}
String s = "foo";
assert 42 == "foo"();
assert 42 == "$s"();
Der . Operator wurde in Groovy mit der
Methode invokeMethod überladen, so dass es
möglich ist Methodenaufrufe für Objekte bestimmter Klassen
abzufangen. Dieses Verfahren kann zum Beispiel fürs Logging
nützlich sein. Ein anderer Anwendungsfall ist das Erzeugen
Baum-artiger Datenstrukturen, wie zum Beispiel XML oder
HTML. Ein einfaches, leeres HTML-Dokument, könnte man zum
Beispiel wie folgt erzeugen:
HtmlCreator c = new ...;
String content = c.html {
head {
"der kopf";
}
body {
"der body-inhalt";
}
}
println content
Die Ausgabe des Skripts:
<html>
<head>
"der kopf"
</head>
<body>
"der body-inhalt"
</body>
</html>
html ist dabei keine Methode aus HtmlCreator und
verursacht somit den Aufruf von invokeMethod ,
welches ein HTML-Tag erzeugt, da der Name der Methode "html"
ist. Entsprechendes gilt für die anderen Methoden. Der
interessante Punkt hierbei ist die Möglichkeit, beliebigen
Code innerhalb der verschachtelten Closures einzubetten. Man
muß also nicht mehr verschiedene Sprachen miteinander mischen
(z.B. Javascript und HTML), sondern kann alles in einer
Sprache erledigen. Es besteht auch die Möglichkeit, eigene
"Builder" zu implementieren, um selbst definierte Baum-artige
Strukturen einfacher eingeben zu können.
Properties - Attribute
Wenn es möglich ist, den . Operator für Methoden
zu überladen, dann sollte das gleiche auch mit Attributen
möglich sein. Dazu dienen die
Methoden getProperty
und setProperty , die jede Klasse beliebig
überschreiben
kann. Die Map- und
Closure -Klassen
machen davon Gebrauch.
class Property
{
public Object getProperty(String name)
{
return "Schall";
}
public void setProperty(String name, Object value)
{
println "$name: $value";
}
}
Property p = new Property();
assert p.a == "Schall";
assert p.egalWasHierSteht == "Schall";
p.foo = "Schall";
p.bar = "Rauch";
Das Beispiel erzeugt die Ausgabe:
foo: Schall
bar: Rauch
Dynamisches Hinzufügen von Methoden
Manchmal benötigt man als Programmierer eine Methoden in einer
Klasse, auf die man keinen Zugriff auf Quellcode-Ebene
hat. Wir betrachten hier beispielhaft die
Methode dup() die einen String verdoppeln
soll. In Java müsste man eine Hilfsklasse schreiben, die eine
Klassen-Methode enthält:
class StringFunctions
{
public static String dup(String self)
{
return s + s;
}
}
Dadurch muß man allerdings bei jedem Aufruf von dup folgendes
schreiben:
StringsFunctions.dup("foo")
Es ist nicht möglich, die in der OOP gebräuchliche Syntax zu
nutzen:
"foo".dup()
In Groovy ist es durch so genannte "categories" möglich,
einer Klasse zur Laufzeit Methoden hinzuzufügen, und
somit auch die gewünschte Syntax zu nutzen:
class StringFunctions {
public static def dup(String a) {
return a + a;
}
}
use (StringFunctions.class) {
assert "11" == '1'.dup();
assert "4242" == '42'.dup();
}
Durch das Überschreiben von bestimmten
(oben genannten)
Methoden wird es auch möglich, Operatoren beliebiger Klassen
zu überladen.
Da man durch categories auch sicherheitsrelevante Methoden
überschreiben kann, sind categories nur im aktuellen Thread,
und nur in der an die Methode use übergebene
Closure nutzbar. Categories eignen sich somit zum Loggen eines
bestimmten Quelltextabschnittes.
Evaluieren: Daten als Code
Eine der typischen Operationen in einer Skriptsprache ist das
Interpretieren eines Strings. Eine einfache Möglichkeit zur
Auswertung ist die Methode Object.evaluate . Die
Methode ist mehrfach überladen; als Parameter kann zum Beispiel
ein String, eine Datei oder ein Eingabestrom übergeben werden.
String s = "7**2 - 7;";
assert 42 == evaluate(s);
Der Rückgabewert der eval-Methode richtet sich nach der letzten
ausgeführten Anweisung. Das return Schlüsselwort ist
auch hier optional.
Möchte man dem Skript auch Parameter übergeben, benötigt man
dafür ein Binding Objekt. Ein Binding-Objekt ist
eine Art Scope (Sichtbarkeitsbereich), oder anders
ausgedrückt, eine Tabelle mit Variablennamen und
Variablenwerten. Dem Binding Objekt kann man eine
Map übergeben, in der man Variablen vordefiniert. Das Binding
übergibt man dann einer GrooyShell , welche
wiederum eine evaluate Methode hat.
def binding = new Binding(x: 6, y: 4);
def shell = new GroovyShell(binding);
shell.evaluate('''
xSquare = x * x;
yCube = y * y * y;
''');
assert binding.getVariable("xSquare") == 36;
assert binding.yCube == 64;
Das Beispiel zeigt, dass auf die Variablen
des Binding auch mit der einfachen
Syntax binding.VARIABLEN_NAME zugegriffen werden
kann. Zusätzlich gibt es die
Methoden Object getVariable(String)
und void setVariable(String, Object) , um auf
Variablen zuzugreifen. Im Gegensatz zu Tcl ist die Methode mit
dem zwischengeschalteten Binding zwar etwas mehr
Tipparbeit, der Vorteil ist aber die erhöhte Sicherheit: Es
können keine Variablen des gleichen Gültigkeitsbereichs
überschrieben werden.
|