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