Java für Fortgeschrittene Proseminar im Sommersemester 2009

Werbung
Java für Fortgeschrittene
Proseminar im Sommersemester 2009
Funktionale Programmierung &
Skripting mit der JVM, Groovy
Matthias Putz
Technische Universität München
18.05.2009
Zusammenfassung
Diese Arbeit beschäftigt sich mit der Programmiersprache Groovy. Dabei wird zuerst auf das Zusammenspiel von Groovy auf Basis der JVM,
sowie die Realisierung der Sprache selbst eingegangen. Anschließend folgt
eine kurze Einführung in die Sprache, um schließlich die beiden zentralen Themen dieser Arbeit, Skripting und Closures in Groovy, angehen zu
können.
1
Einleitung
Groovy ist eine innovative neue Programmiersprache, die viele neue Elemente für den Java-Entwickler mitbringt: Zum einen erweitert es die bestehenden
Möglichkeiten um neue Methoden, integriert out-of-the-box alltäglich benötigte
Frameworks wie XML und SQL und liefert darüber hinaus noch syntaktische
Vereinfachungen. Es stellt sich somit nicht nur die Frage, was die Sprache selbst
alles bietet, sondern ebenso wie die neue Sprache auf dem Rücken der JVM, der
Heimat von Java, realisiert wird.
Nachdem das geklärt ist und die Grundlagen der Sprache bekannt sind, werden
zwei elementare Eigenschaften herausgegriffen, die es Groovy erlauben ganz
andere Wege als Java zu gehen und sogar eine andere Denkweise vom JavaProgrammierer abzuverlangen: Zum einen ist es die Möglichkeit des Skriptings
und zum anderen ist es das Einführen des funktionalen Konzepts der Closures.
2
2.1
JVM als Plattform für Groovy
Zusammenspiel auf der JVM
Abbildung 1 zeigt, dass die JRE als Grundlage von Java aber auch von Groovy
dient. In Java wird der Code vom Compiler in eine *.class-Datei umgewandelt.
1
Abbildung 1: Groovy und Java basieren auf der JVM [8]
Diese enthält den speziellen Bytecode, den die JRE versteht. Groovy geht genauso vor und kompiliert seine Klassen in Bytecode (der Quellcode wird zumeist in
Dateien mit der Endung .groovy gespeichert; wie Groovy den Bytecode erzeugt
wird in Abschnitt 3 beschrieben). Damit wird ermöglicht, dass man aus Java und
Groovy heraus die Methoden und Klassen der jeweils anderen Sprache verwenden kann, da die JRE keinen Unterschied in der Herkunft des Bytecodes kennt.
Somit ist Groovy von Haus aus schon mit den zahlreichen Java-Bibliotheken
ausgestattet und bietet somit alles an, was Java auch anzubieten hat.
Die Verbindung zwischen Groovy und Java ist aber noch stärker: Java-Code
ist Groovy-Code. Das heißt, selbst im Quellcode kann von Java nach Groovy
kopiert werden (aber nicht andersrum!). Somit ist der Umstieg einfach, weil
man vorerst alles so machen kann, wie man es bisher gelöst hat und erst mit
steigendem Groovy-Wissen zur Ausnutzung der Groovy-Fähigkeiten übergeht.
2.2
Werkzeuge
Die Möglichkeiten Groovy-Code zur Ausführung zu bringen sind vielfältig: Einerseits kann man seinen Quellcode kompilieren und ausführen, andererseits
gibt es noch den Direktmodus. In diesem wird scheinbar unmittelbar GroovyCode ausgeführt. Wie dieser Modus wirklich funktioniert wird in Abschnitt 5
beschrieben.
2.2.1
Direktmodus und Kompilieren
Auf der Homepage von Groovy [2] findet man unter Download die aktuelle
Version als zip-Archiv; mehr ist nicht notwendig. Im /bin Verzeichnis des Archivs
findet man folgende Werkzeuge, die eine direkte Ausführung von Groovy-Code
erlauben:
• groovyConsole und groovysh: Ersteres startet eine graphische Oberfläche,
in der Groovy-Code direkt eingegeben und ausgeführt werden kann. Zudem bietet die groovyConsole einige komfortable Funktionalitäten wie Kopieren & Einfügen, Suchen & Ersetzen, Laden & Speichern von Dateien.
Im Prinzip bietet groovysh dasselbe nur auf Konsolenebene.
• groovy: Das ist der Skript-Interpreter. Das heißt er erwartet ein GroovySkript mit der Endung *.groovy und führt dieses aus.
2
Diese Werkzeuge sind für den Einstieg und zum Testen gut geeignet. Zudem
kann jedes Beispiel dieser Arbeit in eine Datei mit der Endung groovy (z.B.
code.groovy) gespeichert werden und dann mit groovy code.groovy ausgeführt
werden. Auch wenn die Beispiele keine main-Methode wie in Java haben, ist das
möglich: Die kurzen Code-Stücke werden als Skript interpretiert und ausgeführt;
grob gesagt werden sie einfach zeilenweise ausgeführt (mehr in Abschnitt 5).
Möchte man dagegen den Groovy-Code kompilieren um ihn dann im classFormat auch direkt von einer JVM ausführen zu lassen, verwendet man den
Compiler groovyc. Dieser erstellt aus *.groovy Dateien *.class Dateien. Diese
können dann wie gewohnt mit dem Programm java (aus der JRE) ausgeführt
werden, sofern man im Classpath die groovy-all-*.jar (in /embeddable des ZipDownloads) aufnimmt.
3
Groovy Bytecode
Die gerade vorgestellten Programme sind für den normalen Programmierer lediglich Werkzeuge, die benutzt werden. Doch besonders bei Groovy - einer Sprache, die ausführbare Programme für die JVM erzeugt - ist es interessant zu erfahren, was die Werkzeuge eigentlich leisten und wie sie es tun. Dieses Kapitel
widmet sich ganz diesem Thema, dem Geschehen hinter der großen GroovyBühne. Es geht hier also nicht um die Verwendung, sondern um die Umsetzung
von Groovy (mehr dazu auf der Groovy-Homepage [2] unter Developers). Dabei
wird die Art und Weise betrachtet, in der die Sprache Groovy auf der JVM
definiert und realisiert wird.
Besonders zu beachten ist, dass Groovy nicht einfach Java-Code erstellt und
dieser dann ausgeführt wird. Zwar entstehen *.class-Dateien, die auch in *.java
zurückkompiliert werden können (z.B. mit [4]) und halbwegs sinnvolle JavaKlassen liefern, doch das heißt nicht dass der Weg von *.groovy-Quelldateien
zu den *.class-Dateien über die Generierung von Java-Code führt.
3.1
Sprache definieren
Die Grammatik von Groovy ist mit Hilfe von ANTLR (ANother Tool for Language Recognition[1]) definiert. Dies ist ein Werkzeug, das darauf ausgelegt ist
die Grammatik von eigenen/neuen Programmiersprachen zu erfassen und zu
spezifizieren.
Die Grammatik ist in einer Datei namens groovy.g abgelegt (zu finden in den
Quelldateien von Groovy). Prinzipiell ist die Datei so aufgebaut, dass in ihr
Regeln definiert sind, zu denen jeweils eine Aktion festgelegt werden kann. Diese
wird beim Auftreten der Regel ausgelöst.
ANTLR nimmt die Sprachdefinition aus groovy.g und erstellt damit automatisch
zwei Klassen: GroovyLexer und GroovyRecognizer. Die Kombination dieser beiden Klassen kann als der Groovy-Parser verstanden werden.
3.2
Quellcode in Bytecode
Um von der Quelltext-Datei *.groovy zu einer *.class-Datei zu kommen, die
die JVM versteht, sind in Groovy vier Zwischenschritte notwendig, die den
Quelltext immer in eine andere Darstellung überführen:
3
Abbildung 2: Aus einem Quellcode entstehende Tokens, angezeigt mit LexerFrame.
1. GroovyLexer: Quellcode *.groovy zu Tokens
2. GroovyRecognizer: Tokens zu ANTLR AST
3. AntlrParserPlugin: ANTLR AST zu GroovyAST
4. AsmClassGenerator: GroovyAST zu Bytecode *.class
Als Beispielcode wollen wir folgendes Codeschnipsel vom Quellcode bis zum
Bytecode verfolgen (gespeichert in der Datei jvm sample.groovy):
GroovyMusic gm = new GroovyMusic ()
gm . isGroovy = true
class GroovyMusic {
boolean isGroovy = false
}
3.2.1
Parsen: Von Quellcode zu Tokens und zum Syntaxbaum
Für die erste Transformation kommt der GroovyLexer zum Einsatz: Er
ist dafür zuständig aus einem Quellcode sogenannte Tokens zu erstellen.
Um dies nachzuvollziehen, ist im Groovy-jar-Archiv die ausführbare Klasse
org.codehaus.groovy.antlr.LexerFrame, die eine Oberfläche startet in der Quellcode direkt als Token-Strom dargestellt werden kann. Das Resultat für unser Beispiel
zeigt Abbildung 2. Wenn man im Programm auf die Tokens klickt, wird der
dazugehörige String im Quelltext markiert. Im Prinzip sind die Leerzeichen des
Quelltextes die Token-Trenner.
Die nächste Stufe übernimmt die Klasse GroovyRecognizer: Die Tokens
werden in einen ANTLR-AST (Abstract Syntax Tree) umgewandelt. Auch
hierfür hat Groovy eine Oberfläche im jar-Archiv. Durch Aufruf von
org.codehaus.groovy.antlr.Main -showtree jvm_sample.groovy wird die übergebene Datei in
einen Syntaxbaum umgewandelt. Abbildung 4 zeigt das Beispiel als AST.
4
Abbildung 3: Aus den ANTLR Tokens entstandener Syntax-Baum
3.2.2
Vom Syntaxbaum zum Bytecode
Nun muss dieser ANTLR-AST noch in einen GroovyAST umgewandelt werden.
Dafür gibt es die Klasse AntlrParserPlugin. Diese Umwandlung ist wegen der
Standisierung von Groovy in einem JSRs notwendig, aber im Prinzip entsteht
keine neue Darstellung, sondern nur ein etwas veränderter Syntaxbaum.
Von der GroovyAST-Darstellung des Groovy-Codes ist es dann möglich Bytecode zu erzeugen: Entweder in Form einer vorkompilierten *.class Datei oder
als Code, der direkt im laufenden Betrieb als Class-Objekt zugänglich gemacht
wird. Diese Funktionalität, aus dem AST den Bytecode zu generieren, erledigt das Tool ASM (Einstiegspunkt für die Bytecode-Generierung ist die Klasse
AsmClassGenerator).
Damit ist grob gezeigt, wie Groovy den Bytecode erzeugt. Zudem ist darauf
hinzuweisen, dass wir auf zwei verschiedene Weisen den Bytecode erhalten. Die
Einbindung von Quellcode zur Laufzeit, ohne eine Datei zu erzeugen, ist Bestandteil der Skriptfähigkeit von Groovy (siehe Abschnitt 5).
Anmerkung: Für Java7 ist ein neues Feature mit dem Namen InvokeDynamic
für die JVM geplant, um dynamische und funktionalen Sprachen besser zu unterstützen. Dabei geht es darum, dass für diese Art von Sprachen eine Möglichkeit geschaffen wird, nicht nur statisch sondern auch dynamisch zu typisieren.
Das bedeutet dass nicht wie in Java schon vor der Laufzeit die Datentypen
für Variablen, Parameter und Rückgabewerte feststehen, sondern erst während
der Laufzeit ermittelt werden. Das sogenannte Duck Typing, das Groovy schon
jetzt erlaubt, zeigt, wann eine dynamische Typisierung nützlich sein kann (siehe
Abschnitt 4.2.2 und 6).
5
Abbildung 4: Fähigkeiten von Groovy [8]
4
Einführung in die Sprache Groovy
Das vorherige Kapitel hat sich mit der Umgebung von Groovy und der Einordnung in die Java-Welt beschäftigt. Nun wird die Programmierung mit der
Sprache näher betrachtet. Die Abbildung 4 zeigt die drei Gruppen/Fachgebiete in denen sich Groovy hervorhebt: GDK, Bibliothek und Sprache. Für diese
Einführung vernachlässigen wir vor allem den Bereich der Bibliotheken. Aus
dem GDK und der Sprachfähigkeit werden nur die notwendigen Grundlagen
herausgegriffen.
Prinzipiell hat man bereits mit Einsteiger-Wissen über Java die Grundlagen von
Groovy, denn alles was Java-Code ist, ist auch Groovy-Code. Das heißt wenn
man nicht weiß, wie man es am besten in Groovy löst, schreibt man einfach
Java-Code (es gibt ein paar Ausnahmen wie die Syntax der for-Schleife siehe
Abschnitt 4.4). Insbesondere muss man sich auch an das aus der funktionalen Programmierung und für Java-Programmierer neue Konzept der Closures
gewöhnen.
4.1
Closures - ein Primer
Vorab sei die Definition für Closures gegeben: Closures sind Code als Objekt.
Bildlich gesprochen heißt das: Wir kaufen einem Freund zum Geburtstag eine
Geburtstagskarte, die beim Öffnen Happy Birthday“ spielt. Dann ist Abspie”
”
len des Liedes“ der Code. Der Briefumschlag, in den wir die Karte legen, ist das
Objekt. Jetzt können wir mit dem Brief alles machen, was wir wollen; wir senden ihn an einen Freund, der Geburtstag hat. Wenn der Freund nun das Kuvert
und den Brief öffnet, wird er das Lied hören (der Code wird ausgeführt).
Die Definition von Closures würde in Groovy wie folgt aussehen:
// neues Closure erstellen
Closure bi rthdayL etter = {
// hier stehen die Anweisungen ; Text soll ausgegeben werden
println " Happy Biiiirthday tooooo Youuuou ... "
}
6
// Closure an Methode übergeben
a d d T o F r i e n d s L e t t e r B o x ( birthda yLetter )
// Methode , die ein Closure erwartet und dieses ausführt
void a d d T o F r i e n d s L e t t e r B o x ( Closure letter ) {
// Ausführen der Closure , Text wird ausgegeben
letter . call ()
}
Sehr gewöhnungsbedürftig ist, dass direkt hinter dem = -Zeichen die geschweif” ”
ten Klammern folgen. Doch das ist das Merkmal, dass es sich um ein Closure
handelt. Häufig erstellt man das Closure jedoch direkt im Methoden-Aufruf.
Dafür sei auf Abschnitt 4.3 verwiesen. Dieses Basiswissen ist ausreichend, um
Closures in Beispiel-Code zu erkennen und zu wissen, was sie für eine Funktion
haben.
4.2
4.2.1
Sprachelemente
Allgemeine Anmerkungen
Im Folgenden werden kurz einige Regeln erläutert, die vor allem das Lesen
von Beispiel-Code erleichtern sollen. Die Anwendung der Regeln findet sich in
nahezu jedem Beispielcode. Deswegen wird auf ein eigenes Beispiel verzichtet.
In vielen Fällen gibt es Ausnahmen und Besonderheiten, die aber dann ggf. in
der angegebenen Literatur [8] nachgelesen werden können.
default Package-Import Ohne eigene import-Anweisung können Klassen aus
den Packages java.lang.*, java.util.*, java.net.*, java.io.* sowie von groovy.lang.* und groovy.util.* verwendet werden.
Kommentare Kommentare können in Groovy genauso wie in Java verfasst
werden.
Semikolon Groovy versucht selbst das Ende einer Anweisung zu erkennen;
damit entfallen die Semikolons am Ende jeder Zeile (empfehlenswert ist
bei komplizierteren einzeiligen Anweisungen den Semicolon einfach wie in
Java zu setzen)
Optionale Klammern Klammern können in manchen Situationen, z.B. bei
einem Methoden-Aufruf, weggelassen werden; es ist jedoch zu empfehlen
das nur bei sehr offensichtlichen Anweisungen zu verwenden!
Standard-Sichtbarkeit public Klassen werden in Groovy wie in Java geschrieben. Groovy geht jedoch davon aus, dass jede Klasse, Methode und
Klassenvariable die Sichtbarkeit public als Standard hat. Damit kann dieses Schlüsselwort weggelassen werden.
Testen In Groovy gibt es ebenfalls das assert-Statement, welches den dahinter
folgenden Ausdruck auf Wahrheit prüft und bei false eine Exception wirft;
von diesem werden die Beispiel-Codes regen Gebrauch machen. Aber Vorsicht: Groovy kennt mehr Wahrheiten als Java (siehe Abschnitt 4.4).
7
4.2.2
Klassen, Objekte & Datentypen
Nun wollen wir einen Schritt weiter gehen und eigene Klassen erstellen. Groovy
bietet uns auch hierfür vorteilhafte Features:
Klassen & GroovyBeans In Java sind Beans Klassen, die Properties (Variablen) haben und zu diesen Getter- und Setter-Methoden bereitstellen.
Das heißt eine Klasse mit dem Property title hat eine getTitle() und eine
setTitle(String) Methode. Groovy erleichtert uns den Umgang mit Beans
in erster Linie dadurch, dass es erstens automatisch die Getter- & SetterMethoden erstellt und zweitens den Zugriff auf das Property so erlaubt als
würde man auf die Variable direkt zugreifen. Das ist vor allem dann wichtig, wenn man in setTitle noch andere Operationen durchführen möchte
wie z.B. den neuen Wert auf seine Gültigkeit zu überprüfen.
Hierzu ein kleines Beispiel, das den Umgang mit Beans und deren Zugriffsmethoden demonstriert.
// Standard - Sichtbarkeit + Properties band , title , mp3
class Song {
String band
String title
// kein Import notwendig
File mp3
// Setter überschreiben , verändert den Zugriff auf Variable band
void setBand ( String band ) {
this . band = band . toUpperCase ()
}
}
// kein ; notwendig
Song song = new Song ()
// Setzen der Properties
song . band = " Eagles "
song . title = " Hotel California "
// Lesen - Bandname ist in Groß buchstab en
assert ( song . band + " " + song . title ) == " EAGLES Hotel California "
Dynamischer Datentyp In Groovy gibt es neben der expliziten Typisierung
(Deklaration mit konkretem Datentyp wie String) auch die implizite Typisierung mit def. Dieses Schlüsselwort ist nicht nur für faule Programmierer
gedacht, sondern ermöglicht auch das sogenannte Duck Typing (Kurzerklärung: Erst zur Laufzeit wird überprüft, ob ein Objekt die erforderlichen
Merkmale wie Methoden, Operatoren, Properties usw., aufweist).
GStrings und Strings GStrings werden mit doppelten Anführungszeichen erstellt und können im Gegensatz zu Strings im Stile von Templates Variablen im Text auflösen (dazu verwendet man das $ Zeichen vor dem Variablennamen; die Variablen werden aus der aktuellen Umgebung genommen,
es müssen keine Variablen zur Auflösung bestimmt werden). Darüber hinaus gibt es die aus Java bekannten Strings, die mit einfachen Anführungsstrichen erstellt werden. Möchte man mehrzeilige (G)Strings erstellen,
muss man seinen Text in drei (doppelte) Anführungsstriche schreiben.
Operatoren überladen Groovy ermöglicht das in Java oft vermisste Überladen von Operatoren. Dazu müssen lediglich Methoden mit einer bestimmten Signatur, die einem Operator zugewiesen sind, überschrieben werden
8
(eigene Operatoren sind somit nicht möglich). Im Folgenden ein kleiner
Ausschnitt aus der Zuordnungstabelle (eine vollständigere Tabelle ist in
[8] Abschnitt 3.3.1 zu finden).
Gruppe
Vergleich
Mathematik
Iterator
Zugriff
Operator
a == b
a <=> b
a+b
a-b
a*b
a\b
a++, ++a
a−−, −−a
a[b]
a[b] = c
Methode
a.equals(b)
a.compareTo(b)
a.plus(b)
a.minus(b)
a.multiply(b)
a.div(b)
a.next()
a.previous()
a.getAt(b)
a.putAt(b, c)
Eine kleine Anwendung zeigt dieser Beispielcode:
// Speichert die 5 Top Songs von bestsongsever . com
class BestSongsEver {
// Liste mit allen Songs erstellen
List top5 = [ " Queen - Bohemian Rhapsody " ,
" Led Zeppelin - Stairway To Heaven " ,
" Pink Floyd - Wish You Were Here " ,
" John Lennon - Imagine Rock " ,
" Pink Floyd - Comfortably Numb " ]
// Lese - Operator [.] überladen
String getAt ( int index ) {
return top5 . get ( index )
}
// Sc h re ib op e ra to r [.] überladen
void putAt ( int index , String song ) {
top5 [ index ] = song ;
}
}
// Objekt erzeugen und mit Operator lesen / schreiben
BestSongsEver top = new BestSongsEver ()
if (! top [0]. startsWith ( " Eagles " )) top [0] = " Eagels - Hotel California "
assert top [0] == " Eagels - Hotel California "
Das sind die wichtigsten Anmerkungen, die auf Sprachebene existieren. Nun
folgt die Unterstützung von Listen und Maps, sowie die hinzugefügten Erweiterungen zum JDK.
4.3
Listen, Maps und das GDK
Eine reichhaltige Erweiterung bietet das GDK[3] vor allem im Umgang mit
Listen und Maps. Die Einführung soll die geläufigsten und interessantesten Methoden im Umgang mit diesen zwei Datenstrukturen zeigen.
4.3.1
Listen
Listen können einfach mit dem Schema [item, item, item] erstellt werden. Dann
wird standardmäßig eine neue ArrayList angelegt. Möchte man eine andere Implementierung verwenden oder einen generischen Typ festlegen, so kann dieses
Schema nicht verwendet werden. Dann muss zuerst über den Aufruf des Konstruktors die Liste erstellt werden: def list = new LinkedList<String>().
9
Das folgende Beispiel zeigt die üblichsten Operationen auf Listen:
// Erstellen einer Liste
def list = [ " Queen " , " Led Zeppelin " , " Pink Floyd " , " John Lennon " ]
// bestimmte Elemente herausfiltern
assert list [2..3] == [ " Pink Floyd " , " John Lennon " ]
assert list [0 , 3] == [ " Queen " , " John Lennon " ]
// Elemente löschen und am Ende hinzufügen
list -= " Pink Floyd "
list += " Eagles "
assert list [ list . size -1] == " Eagles "
// GDK - Methoden : Herausfiltern mit find - Methode
// Achtung : erwartet Closure ; it nimmt jeden Wert der Liste an ,
//
Closure gibt true / false zurück
assert list . find { it -> return ! it . contains ( " " ) } == " Queen "
// collect : it nimmt jeden Listenwert an , Closure bearbeitet Wert
//
und gibt ihn zurück - veränderte Liste ist Ausgabe
assert ( list . collect { it -> return it [0] }) == [ " Q " , " L " , " J " , " E " ]
Für das Verhalten von find(..) und collect(..) würde man in Java eine Schleife
benötigen, doch durch diese und viele weitere Methoden von diesem Stil im
GDK kann man in Groovy oft auf Schleifen verzichten.
Anmerkung: Groovy führt sogenannte Ranges (Bereiche) ein, die mit dem
Range-Operator .. gebildet werden. Nützlich ist dieses Konstrukt, wie bereits
gezeigt, bei Abfragen mit Listen über Indizes. Jedoch können diese nicht nur mit
Zahlen gebildet werden, sondern auch mit Buchstaben und jeder eigenen Klassen
durch Überladen der entsprechenden Operatoren; siehe Abschnitt 4.2.2.
4.3.2
Maps
Maps sind die zweite Datenstruktur, die von Programmierern sehr häufig verwendet wird und deshalb wird sie ebenfalls bereits auf Sprachebene von Groovy
unterstützt. Das Schemata hierfür ist [key:value, key:value, key:value]; man erhält
eine HashMap. Das folgende Beispiel zeigt wie mit einer Map gearbeitet werden
kann:
// Erstellen einer Map ; Keys werden als String interpretiert
def bandbirth = [ " Queen " :1970 , " Beethoven " : -1 , " Sunrise Avenue " :2002]
// Element - Abfrage
assert bandbirth [ " Queen " ] == 1970
// Elemente ändern , hinzufügen und löschen
bandbirth [ " Beethoven " ] = 1770
bandbirth [ " U2 " ] = 1976
bandbirth . remove ( " Queen " )
// Überprüfen
assert bandbirth [ " Beethoven " ] == 1770 && bandbirth [ " U2 " ] == 1976
assert bandbirth [ " Queen " ] == null
// GDK - Methoden : any liefert true , wenn das Closure für irgendein Element
// true liefert
assert bandbirth . any { band , year -> return year > 1990 } == true
4.4
Kontrollstrukturen & Schleifen
Wie in Java gibt es auch in Groovy die if und switch Anweisung, sowie die while
und for-Schleife. Die Verwendung ist im wesentlichen dieselbe, es gibt lediglich
folgende Anmerkungen:
10
• Groovy kennt mehr Wahrheiten als Java. Es wertet jedes Objekt zu einem
Wahrheitswert aus: So ist zum Beispiel ein null-Objekt immer false, genau
so wie eine leere Collection oder ein leerer String zu false ausgewertet wird
(siehe [8] Abschnitt 6.1).
• Die switch-Anweisung ist mächtiger als in Java, da sie mehr Datentypen
für die Unterscheidung erlaubt (eigene Datentypen sind durch Überschreiben der isCase(..)-Methode möglich).
• Ab Java5 gibt es bereits die for(.. : ..)-Schleife, diese kennt Groovy als
for(.. in ..).
4.5
Was gibt es noch?
Die noch folgende Themen, Skripting und Closures, sind nur zwei von vielen
mächtigen Fähigkeiten von Groovy (siehe Abbildung 4). Für den interessierten
Leser sind vor allem die Unterstützung von regulären Ausdrücken auf Sprachebene, dynamische OOP, sowie Support von Bibliotheken wie SQL, XML und
Swing zu nennen. Als erste Anlaufstelle empfiehlt sich die Groovy-Homepage
[2], sowie Literatur aus dem angegebenen Literaturverzeichnis.
5
Skripting
5.1
Skripteigenschaften
Die Grundlagen der Sprache Groovy verführen einen dazu, selbst aktiv zu werden. Der schnellste Weg zur Ausführung von Groovy-Code geht über dessen
Skriptingfähigkeit. Die Werkzeuge wurden bereits eingeführt (siehe Abschnitt
2.2), selbst deren Arbeitsweise, um vom Quellcode zum Bytecode zu kommen,
ist bekannt (siehe Abschnitt 3). Um dem ganzen noch eins drauf zu setzen: Alle
Beispiele aus dieser Arbeit sind Groovy-Skripte. Das heißt, dass wir eigentlich
schon vieles über das Skripting wissen. Doch die Antwort auf die Frage wie gut
Groovy die Anforderungen an eine Skriptsprache erfüllt steht noch aus.
Oft wird Groovy als Java-Skriptsprache beschrieben. Dies deutet schon an, dass
Groovy nicht nur eine halbherzig implementierte Alternative zu Skriptsprachen
ist. Auch wenn die Reduktion auf eine Skriptsprache Groovy nicht gerecht wird,
ist diese Fähigkeit nicht zu verachten. Doch was sind die markanten Merkmale
einer Skriptsprache? Wie in einschlägiger Literatur nachzulesen ist, sind im
Wesentlichen folgende Eigenschaften einer Skriptsprache zuzuordnen:
• Für kleinere Programme wie Automatisierungen und Routinen gedacht,
eher schnell als strukturiert programmieren.
• Deklarationen können weggelassen werden (Bestimmung des Datentyps
wird von Sprache vorgenommen); genauso werden unnötig lange Ausdrücke vermieden
• Auslieferung der Skripte als Quellcode (ermöglicht einfaches Ändern/Anpassen); Enthält Sequenz von abzuarbeitenden Anweisungen
11
Die Einführung in die Sprache hat gezeigt, dass Groovy oft Annahmen trifft
und dadurch bestimmte Schlüsselwörter weggelassen werden können (import,
public, Getter- & Setter-Methoden, . . . ). Oder es ist der Fall, dass Groovy durch
Sprachelemente z.B. lange Methodennamen verkürzt (bei Listen und Maps).
Zudem gibt es das Schlüsselwort def, welches uns eine implizite Deklaration
erlaubt. Somit ist der erste und zweite Punkt im Sinne der Definition erfüllt.
Auch der dritte Punkt kann abgehakt werden: Das Programm groovy wurde vorgestellt, das direkt groovy-Quellcode-Dateien ausführt und somit kein Kompilieren verlangt. Zudem wissen wir, dass jedes Beispiel dieser Arbeit ein Skript ist
und somit versteht Groovy eine Sequenz von Befehlen und kann diese ausführen;
in einer Datei wird keine Klassen-Deklaration wie in Java verlangt.
// Objekte von Artist einfügen
def john = new Artist ( " John Lennon " , " www . johnlennon . com " )
def elvis = new Artist ( " Elvis " , " www . elvis . de " )
// neue Liste erstellen
def legends = [ john , elvis ]
assert legends . size == 2
// verwendete Klasse ; ist auch im Skript enthalten !
class Artist {
def name
def website
Artist ( def name , def website ) {
this . name = name
this . website = website
}
}
Zu beachten ist die Verwendung der Klasse Artist noch vor der Deklaration.
Das ist deswegen möglich, weil das Skript nicht einfach Zeile für Zeile ausgeführt wird - also nicht so, wie sonst übliche Skriptsprachen ihre Aufgaben
abarbeiten: Groovy liest zuerst das gesamte Skript ein, verarbeitet alles (z.B.
Klassen-Deklarationen im Code) und führt dann erst die Befehle aus.
Wenn man sich die erstellten *.class-Dateien nach dem Kompilieren eines Skripts
anschaut, so entstehen immer pro Klasse eine eigene Datei, für Closures entstehen eigene Klassen und für das Skript selbst entsteht eine *.class mit dem Namen
der Skriptdatei.
5.2
Integration: Erweiterung von Java-Anwendungen
Groovy geht aber noch weiter: Es lassen sich Skripte als Zeichenketten erstellen
und dann auch ausführen. Dazu gibt es die Klasse GroovyShell. Über die Methode
getContext() von GroovyShell lassen sich Variablen für das Skript vorbelegen.
def shell = new GroovyShell ()
shell . getContext (). setVariable ( " x " , 3)
assert shell . evaluate ( " 1 + 2 + x " ) == 6
Spinnen wir den Faden weiter, dann muss das auch in Java möglich sein. Schließlich greifen wir nur auf eine Java-Klasse GroovyShell zu und übergeben einen
String, der Groovy-Code enthält. Das stimmt auch, das obige Beispiel ist genau so in Java auszuführen (vorausgesetzt das Archiv groovy-all-*.jar ist im
Classpath):
import groovy . lang . GroovyShell ;
public class ScriptShell {
public static void main ( String [] args ) {
12
GroovyShell shell = new GroovyShell ();
shell . getContext (). setVariable ( " x " , 3);
Object result = shell . evaluate ( " 1 + 2 + x " );
assert new Integer (6). equals ( result );
}
}
Möchte man mehr Variablen verwenden und nach der Auswertung wieder auslesen, so gibt es die Klasse Binding, die die Werte von Variablen speichert und
dem Skript zur Veränderung bereitstellt.
Dieses Skript-Feature erlaubt es uns im Stil von VisualBasic unsere Applikation
mit eigenen Erweiterungen und Plugins zu bereichern. Zudem ermöglicht die
GroovyShell seine Skripte aus verschiedenen Quellen zu beziehen. InputStreams
und Dateien sind weitere Möglichkeiten.
Stellen wir uns die Frage nach der Realisierung, so muss zur Laufzeit neuer
Code und neue Klassen eingebunden werden. Die Vorgehensweise wurde bereits ausführlich in Abschnitt 3 gezeigt. Angestoßen wird die Integration in der
GroovyShell über den Groovy-eigenen GroovyClassLoader. Jedoch ist das Thema Classloader ein umfangreiches und tiefgehendes Thema, das hier nicht weiter
ausgeführt werden kann.
Stattdessen wird nun die funktionale Programmierung mit Groovy in Form von
Closures betrachtet.
6
Funktionale Aspekte
Das Konzept Closures wurde bereits eingeführt. Was dieses Konzept bietet
und wann es große Vorteile bringt, zeigt die Problemstellung Sortieren einer
”
Liste“: In Java muss man zum Sortieren den Weg über die statische Methode Collections.sort(List l, Comparator c) nehmen. Das unbequeme ist der Comparator,
den man als Parameter übergeben muss. Die einzige Methode des Comparator ist
compare(o1,o2), sie wird auf Kombinationen der Elemente der Liste aufgerufen,
um so die Ordnung der Elemente zu bestimmen. Im Prinzip gibt es nun zwei
Möglichkeiten das Objekt zu erstellen: Entweder muss zuerst eine neue Klasse erstellt werden, die das Comparator-Interface implementiert, oder es muss
eine anonyme Klasse verwendet werden. Beides sind Konstrukte die relativ unhandlich sind: Erstere führt zu unnötig vielen Klassen, zweitere ist nicht nur
unschön, sondern bietet auch nur beschränkten Zugriff auf Variablen außerhalb
der Klasse.
Besser ist es in Groovy realisiert: Statt des Comparators wird ein Closure erstellt
und ausgeführt. Das geschieht genau an der Stelle, wo wir sortieren möchten.
// unsortierte Listen
List list = [5 ,2 ,1 ,3 ,4]
// erstelle eines Closures , das 2 Paramter erwartet
def sortClosure = { o1 , o2 ->
if ( o1 < o2 ) return -1
else if ( o1 > o2 ) return +1
else return 0
}
// sort - Methode von list erwartet ein Closure , dass die Sortierung vornimmt
list . sort ( sortClosure )
assert list == [1 ,2 ,3 ,4 ,5]
Das Beispiel zeigt noch mehr über Closures:
13
• Die Methode sort(Closure) wird direkt auf der Variable list ausgeführt. Der
einzige Parameter der Methode ist ein Closure, das den Vergleich zweier
Elemente aus der Liste durchführt (sort ist Bestandteil der Erweiterung
des JDK, um Closures sinnvoll einsetzen zu können).
• Das Closure erwartet zwei Parameter o1 und o2, dessen Typ nicht angegeben wurde, und somit behandelt werden, als wären sie mit def angegeben
worden (hier sehen wir ein Beispiel von Duck Typing: Ohne den Typ von
o1 und o2 zu wissen, verwenden wir die Operatoren bzw. Methoden < und
>).
• Der Code beginnt erst nach der Parameterliste, also nach dem Pfeil
->
• Closures können Rückgabe Werte haben - hier sind es Integer-Werte für
den Vergleich
Es gibt noch ein paar weitere funktionale Eigenschaften, die Groovy aufweist
und mit deren Hilfe man im Stile funktionaler Programmiersprachen seinen Code verfassen kann. Diese werden aber von anderen JVM-Programmiersprachen
wie Clojure oder Skala besser umgesetzt und sollen deshalb hier nur am Rande
erwähnt werden. Falls man auf der JVM funktional programmieren möchte, sind
diese Sprachen besser geeignet.
6.1
6.1.1
Erstellung und Verwenden von Closures
Mit GDK-Methoden aufrufen
Es gibt in Groovy eine Klasse Closure, die ein Code-Objekt aufnimmt. Das CodeObjekt selbst wird nicht mit dem new-Schlüsselwort sondern durch direktes Angeben des Closure-Code-Blocks in geschweiften Klammern erzeugt. Ferner gibt
es einige Abkürzungsregeln, die man bei Closures anwenden kann und die im
nachfolgenden Beispiel Schritt für Schritt durchgeführt werden.
1. Die letzte Anweisung entspricht dem Rückgabewert (kein return notwendig; gilt auch für Methoden).
2. Wenn Closure nur einen Parameter hat, dann ist dies automatisch die
Variable it.
3. Erwartet eine Methode nur einen Parameter und dieser ist ein Closure,
kann dieses direkt hinter dem Methodennamen angegeben werden (ohne
Klammern).
Das folgende Beispiel vereinfacht einen ausführlichen Closure-Aufruf nach obenstehenden Regeln. Die Methode grep stammt aus dem GDK: Es durchläuft bei
Listen alle Elemente, ruft das Closure mit dem Element auf und greift sich für
die Ergebnis-Liste alle Elemente, bei denen das Closure true liefert.
// Liste für das Beispiel
def list = [1 , 2 , 3 , 4 , 5]
// 0) ausführliche Closure - Erstellung
Closure c0 = { item ->
if ( item > 2) {
return true
} else {
14
return false
}
}
def l0 = list . grep ( c0 )
// 1) letzte Anweisung entspricht Rückgabe
Closure c1 = { item ->
item > 2
}
def l1 = list . grep ( c1 )
// 2) nur ein Parameter , dann it
Closure c2 = { it > 2 }
def l2 = list . grep ( c2 )
// 3) direkt in Methode ; wenn nur ein Parameter , dann ohne Methoden - Klammern
def l3 = list . grep { it > 2 }
// überprüfen - mit einem noch ungewohnten 1 - Zeiler
[ l0 , l1 , l2 , l3 ]. each { assert it == [3 , 4 , 5] }
Werden vom Closure mehrere Parameter erwartet, muss man die Parameter
benennen. Ebenso ist ein Methodenaufruf mit einem Closure und weiteren Parametern möglich. Diese beiden Anmerkungen zeigt folgender Code:
// Map map für Quelle , Liste res für Ergebnis
def map = [ a :1 , b :2 , c :3]
def res = []
// Methode mit mehreren Parameter + Closure mit 2 Parametern
map . collect ( res , { key , value -> value * 2 })
assert res == [2 , 4 , 6]
6.1.2
Closures ausführen & Currying
Möchte man selbst ein Closure-Objekt c ausführen, so kann das durch Aufruf
der Methode call() oder durch c() durchgeführt werden. Dabei können benötigte
Parameter in den Klammern übergeben werden.
In funktionalen Sprachen gibt es die Möglichkeit des Currying. Damit ist die
partielle Auswertung einer Funktion gemeint. Das heißt in der Groovy-Sprache,
dass wir ein Closure-Objekt nicht vollständig ausführen wollen, sondern lediglich
ein paar Parameter setzen wollen. Dazu stellt uns Groovy die Methode curry(..)
zur Verfügung. Wenn man dieser N Parameter mitgibt, dann werden die ersten
N Parameter festgesetzt. Das heißt curry(..) gibt eine Kopie des Closures zurück,
das jetzt aber nur noch die verbleibende Anzahl an Parametern erwartet.
Einen Anwendungsfall sehen wir im Beispiel. So können wir aus einem allgemeinen Zahlen-Multiplikations-Closure ein Verdoppler-Closure machen.
// Closure definieren
def c = { x , y -> x * y }
// Currying mit ersten Parameter , erhalten Closure das Zahl y verdoppelt
def curry = c . curry (2)
// prüfen des neuen Closures
assert curry (3) == 6
6.2
Sichtbarkeit - Realisierung
Ein wichtiger und zentraler Punkt ist die Sichtbarkeit, Scope oder auch Gültigkeit eines Closures: Aufgrund der Tatsache, dass Closures auch Variablen aus
15
ihrer Umgebung verwenden dürfen, stellt sich die Frage, welche Umgebung das
ist. In Groovy ist es die Umgebung des Deklarationszeitpunkts und nicht die
Umgebung des Ausführungszeitpunkts.
Demnach muss ein Closure eine Referenz auf die Umgebung speichern, um so
später bei der Ausführung auf die Variablen und Methoden zugreifen zu können.
Das folgende Beispiel erstellt in der Klassenmethode ein Closure, das eine Liste
von Umgebungsvariablen, sowie die this-Referenz, von der wir noch nicht wissen,
auf was sie sich bezieht, zurückgibt:
// Klasse , die Umgebung für Closure darstellt
class Besitzer {
// Kl as s en va ri a bl e
int varClass = 1
// Funktion in der Umgebung
int funClass () {
return 2
}
// Funktionsaufruf , der ein Closure deklariert und zurückgibt
// ( aber nicht ausführt !)
def createClo () {
// lokale Variable
int varLocal = 3
// Closure erstellen : D e k l a r a t i o n s z e i t p u n k t !
def clo = { callOwner ->
int varClo = 4
[ this , callOwner , varClass , funClass () , varLocal , varClo ]
}
// lokale Variable ändern ; bemerkt das Closure die Änderung ?
varLocal = 42
return clo
}
}
Aufgrund der Tatsache, dass ein Aufruf von var dasselbe wie this.var liefern
soll, bezieht sich das Schlüsselwort this auf das Objekt, das das Closure zur
Deklarationszeit beinhaltet. Zusammen mit der oben gezeigten Besitzer-Klasse
können wir folgende Aussagen über die Liste der Closure-Umgebungsvariablen
machen:
// von Besitzer - Objekt das Closure holen
def b = new Besitzer ();
def c = b . createClo ()
// Closure ausführen : A u s f ü h r u n g s z e i t p u n k t 1!
def result = c . call ( this )
assert result == [b , this , 1 , 2 , 42 , 4]
// zur V erdeutl ichung : this des Closures ( index 0) ist ungleich übergebenem
// this ( index 1)
assert result [0] != result [1]
// Veränderung der Umgebung
b . varClass = 42
// Closure ausführen : A u s f ü h r u n g s z e i t p u n k t 2!
assert c . call ()[2] == 42
Die Closure-Umgebung wird also so realisiert, dass das Objekt intern eine Referenz auf das Besitzer-Objekt hält.
16
7
Demoprojekt: GroovyMusic
Das Demoprojekt zu dieser Arbeit trägt den Namen GroovyMusic und ist ein
Programm, das für Hobby-Gitarristen gedacht ist. Es besteht aus drei Komponenten: Einem Parser, einer Musik-Minisprache sowie einem Abspielprogramm.
Alle Teile können als Skripte ausgeführt werden. Das Augenmerk wurde bei
diesem Demoprojekt auf das Zeigen von Features für erfahrenere GroovyProgrammierer gelegt.
Der erste Programmteil, der Parser, erlaubt es dem Hobbymusiker von einer Seite wie www.ultimate-guitar.com sogenannte Tabulator-Darstellungen von Lieder mit Hilfe von regulären Ausdrücken einzulesen. Diese werden von Groovy auf
Sprachebene unterstützt. Dagegen erlaubt die Musik-Minisprache, eine Domain
Specific Language, das Komponieren eigener Stücke durch einfache Angaben
auf welcher Seite welcher Ton auf der Gitarre gegriffen und angeschlagen werden soll. Als Ergänzung gibt es noch das Abspielprogramm, das ein bestehendes
Framework, JFugue [5], in Groovy integriert und erweitert, um so die Lieder,
die entweder durch Parsen oder durch eigenes Komponieren entstanden sind,
auch abspielen zu können.
8
Schluss
Zusammenfassend ist in dieser Arbeit eine Grundlage für die Programmierung
mit Groovy gegeben worden. Zudem wurde zu Beginn bereits ein Blick hinter
die Entwicklung von Groovy geworfen, indem wir die Bytecode-Generierung betrachtet haben. Nach einer kurzen Spracheinführung konnten dann die Themen
Skripting und Closures näher betrachtet werden - beides Konzepte, die die JVM
mit Java nicht ermöglicht.
Somit bleibt zu sagen, dass Groovy selbst mit den hier erwähnten Fähigkeiten
bereits die Mächtigkeit von Java enorm erweitert. Obwohl andere Leistungsmerkmale der Sprache wie RegEx, XML, SQL-Support, sowie die dynamische
OOP nicht erwähnt wurden. Zudem stehen für die Integration von Groovy in
existierende Java-Anwendungen und Frameworks noch unendlich viele Möglichkeiten offen, die man ausprobieren kann und darauf warten implementiert zu
werden. Eine große Auswahl findet man bereits auf der Groovy-Homepage am
rechten Rand unter Modules.
Darüber hinaus geht die Entwicklung von Groovy selbst immer weiter: Neue
Features erweitern auf elegante Weise mit jeder neuen Version die Nützlichkeit
von Groovy. Zu nennen sind für die kommende Version eine übersichtlichere
Meldung bei assert-Fehlern sowie die Unterstützung von Closures in Annotations.
Literatur
[1] Antlr. http://www.antlr.org/. [Online; letzter Zugriff 14.05.2009].
[2] Groovy homepage. http://groovy.codehaus.org/. [Online; letzter Zugriff
14.05.2009].
[3] Groovy jdk - gdk apidoc. http://groovy.codehaus.org/groovy-jdk/.
[Online; letzter Zugriff 14.05.2009].
17
[4] Java decompiler jd. http://java.decompiler.free.fr/. [Online; letzter
Zugriff 14.05.2009].
[5] Jfugue. http://www.jfugue.org/. [Online; letzter Zugriff 14.05.2009].
[6] Practically groovy: Smooth operators.
http://www.ibm.com/
developerworks/java/library/j-pg10255.html. [Online; letzter Zugriff
14.05.2009].
[7] Practically groovy: Stir some groovy in your java apps.
http:
//www.ibm.com/developerworks/java/library/j-pg05245/index.
html?S_TACT=105AGX02&S_CMP=EDU. [Online; letzter Zugriff 14.05.2009].
[8] Dierk Koenig. Groovy im Einsatz. Carl Hanser Verlag, 2007.
[9] Christian Ullenboom. Java ist auch eine Insel (8. Auflage). Galileo Computing, 2007.
18
Herunterladen