Anhang A: Kurze Einführung in Groovy
Groovy ist eine Programmiersprache auf der JVM, die das Ziel verfolgt, sich perfekt in des Java-Ökosystem zu integrieren, den Übergang für Java-Entwickler nahtlos zu gestalten und trotzdem so viele neue Fähigkeiten mitzubringen, dass fast alles einfacher und manches überhaupt erst möglich wird.
Bad old Java
Fangen wir mit einer einfachen Java-Klasse an und verwandeln diese Schritt für Schritt in idiomatisches Groovy.
1 public class HumanBeing {
2 private String name;
3
4 public String getName() {
5 return name;
6 }
7
8 public void setName(String newName) {
9 this.name = newName;
10 }
11
12 public HumanBeing(String name) {
13 this.name = name;
14 }
15
16 public String computeFromName(DoWithName code) {
17 return code.withName(name);
18 }
19
20 public static void main(String[] args) {
21 HumanBeing son = new HumanBeing("Jannek");
22 son.setName("Niklas");
23 DoWithName code = new DoWithName() {
24 @Override
25 public String withName(String name) {
26 return name + name;
27 }
28 };
29 System.out.println(son.computeFromName(code));
30 }
31
32 interface DoWithName {
33 String withName(String name);
34 }
35 }
Diese Klasse erlaubt die Instanziierung menschlicher Wesen mit einer einzigen Property, welche den Konventionen für JavaBeans Zugriffsoperationen folgt.
Erster Schritt: *.java -> *.groovy
Im ersten Schritt unserer Metamorphose benennen wir einfach die Datei HumanBeing.java in HumanBeing.groovy um und benutzen für die Kompilierung groovyc statt javac. Dabei machen wir uns die Tatsache zu nutze, dass 99% allen Java-Codes auch gültigen Groovy-Code darstellt.
Doch Vorsicht, manchmal ändert sich bei dieser Umstellung die Semantik des Programms, z.B. weil Groovys Method-Dispatching normalerweise1 dynamisch anhand der Laufzeit-Typen der Parameter stattfindet. In unserem einfachen Beispiel ist dieser Unterschied jedoch bedeutungslos.
Zweiter Schritt: Überflüssiges
Die einfachsten Maßnahmen zur Verschlankung des Codes ist das Weglassen von Elementen, die in Groovy optional sind, und die Benutzung von masse-reduzierenden Spracheigenschaften:
- “
public” ist der Default-Modifier und kann weggelassen werden. - “
return” an letzter Stelle einer Methode benötigt man nicht, wenn die letzte Expression (z.B. eine Variable) zurückgegeben werden soll. - ”
;” am Zeilenende ist überflüssig. - Normale Strings werden mit einfachen statt doppelten Hochkommata gebildet. Doppelte Hochkommata haben eine besondere Bedeutung.
- Member-Variablen ohne die Modifier
public,privateoderprotectedwerden automatisch als Properties interpretiert. Dies bedeutet, dass Getter- und Setter-Methoden im Byte-Code generiert werden, und dass Zugriffe auf die Property automatisch auf diese Methoden umgelenkt werden.
Durch die Umsetzung dieser Vereinfachungen wird das Ergebnis bereits kürzer und – meiner Meinung nach – besser lesbar:
1 class HumanBeing {
2 String name
3
4 public HumanBeing(String name) {
5 this.name = name
6 }
7
8 String computeFromName(DoWithName code) {
9 code.withName(name)
10 }
11
12 public static void main(String[] args) {
13 HumanBeing son = new HumanBeing('Jannek')
14 son.name = 'Niklas'
15 DoWithName code = new DoWithName() {
16 @Override
17 String withName(String name) {
18 name + name
19 }
20 }
21 System.out.println(son.computeFromName(code))
22 }
23
24 interface DoWithName {
25 String withName(String name)
26 }
27 }
Dritter Schritt: Optionale Typen
Die Angabe von Typen für Variablen und Parameter ist in Groovy freiwillig. Aus diesem Grund benötigt Groovy ein weiteres Schlüsselwort def, um Variablen ohne Typ und ohne Modifier deklarieren zu können. Hier die neue Fassung – ohne explizite Typen und ohne System.out, da println als globale Funktion verfügbar ist:
1 class HumanBeing {
2 def name
3
4 public HumanBeing(name) {
5 this.name = name
6 }
7
8 def computeFromName(code) {
9 code.withName(name)
10 }
11
12 public static void main(args) {
13 HumanBeing son = new HumanBeing('Jannek')
14 son.name = 'Niklas'
15 def code = new DoWithName() {
16 @Override
17 def withName(name) {
18 name + name
19 }
20 }
21 println(son.computeFromName(code))
22 }
23
24 interface DoWithName {
25 def withName(name)
26 }
27 }
Statische Typisierung
Seit Groovy 2 erfüllt die Sprache auch die Wünsche derer, die aus Sicherheits- oder Performance-Gründen nicht auf strenge Typprüfung verzichten wollen. Es existieren zwei Annotationen, die wahlweise für eine vollständige Typdeklaration oder für einzelne Methoden verwendet werden können:
-
@TypeCheckedbehält die dynamischen Eigenschaften von Groovy bei, überprüft aber, ob alle Parameter und Variablen auch konform zu ihrer Typdeklaration verwendet werden. Diese Annotation hat keine Auswirkung auf den generierten Byte-Code. -
@CompileStaticsorgt tatsächlich für Byte-Code, der zusätzlich zur Typprüfung auch zur Generierung von statisch gebundenen Methodenaufrufen führt. Daher kann man mit Hilfe dieser Annotation Groovy-Programme schreiben, welche die gleiche Performance-Charakteristik aufweisen wie äquivalente Java-Programme. Meist bedeutet dies: Sie sind schneller.
Die statische Typprüfung bei Groovy ist um einiges rafinierter als ein Java-Compiler. Viele Typen kann Groovy statisch auflösen, die bei Java einen Cast erfordern. Beispielsweise ist
def son = new HumanBeing(name: 'Jannek')
println son.name
statisch compilierbar, da der Groovy-Compiler weiß, dass die Variable son eine Instanz der Klasse
HumanBeing enthält.
Vierter Schritt: Default-Konstruktoren
Ohne dass man etwas tun müsste, erzeugt Groovy für jede Klasse einen Konstruktor, der das initiale Setzen einzelner oder aller Properties mit Hilfe von benannten Parametern. Damit können wir auf den HumanBeing-Konstruktor ganz verzichten, müssen jedoch die Instanziierung ein klein wenig verändern:
HumanBeing son = new HumanBeing(name: 'Jannek')
Auch die in Java verbreitete Form von Konstruktoren mit der Aufzählung aller Properties im Konstruktor, können wir bei Bedarf mit der Annotation @TupleConstructor erzeugen.
Letzte Schritte: Closures
Erst mit JDK 1.8 hat Java Lambda-Ausdrücke spendiert bekommen. Groovy besitzt dieses wichtige Konstrukt schon von Beginn an – unter dem Namen Closures. Closures sind nichts anderes als “Codeblöcke”, die – genau wie andere Objekte auch – zugewiesen, als Berechnungsergebnis zurückgegeben und als Parameter übergeben werden können. Ausführen kann man diese Codeblöcke, indem man sie wie Funktionen aufruft, gegebenenfalls mit Parametern.
In Groovy werden Closures durch geschweifte Klammern gekennzeichnet. Hier ein kurzes Beispiel:
def istTeilbar = {zahl, teiler -> (zahl % teiler) == 0}
assert istTeilbar(35, 7) == true
Gekoppelt mit der Möglichkeit eine Closure als letzten Parameter außerhalb der Klammern zu schreiben, werden wir nicht nur das Interface DoWithName los, sondern können den Aufruf der computeFromName-Method ohne temporäre Variable vornehmen. Hier der Code nach der abermaligen Verkürzung:
1 class HumanBeing {
2 def name
3
4 def computeFromName(code) {
5 code(name)
6 }
7
8 public static void main(args) {
9 HumanBeing son = new HumanBeing(name: 'Jannek')
10 son.name = 'Niklas'
11 println(son.computeFromName() { name -> name + name } )
12 }
13 }
Tatsächlich lässt sich die letzte Zeile der main-Methode noch weiter vereinfachen, wenn man auf den Default-Parameter it von Closures zurückgreift und ein paar unnötige Klammern weglässt:
println son.computeFromName { it + it }
Closures im GDK
Closures werden dann zu einem mächtigen Sprachmittel, wenn auch die eingesetzten Bibliotheken deren Einsatz fördern. Das Groovy Development Kit (abgekürzt GDK) tut genau das. So existieren für alle Collections zahlreiche Methoden, die mit Closures arbeiten, z.B.:
[1, 2, 3, 4].findAll {it % 2 == 0}.each {println it}
Doch auch an zahllosen anderen Stellen des GDK kommen Closures zum Einsatz, wie z.B. beim sicheren Zugriff auf Ressourcen oder beim Einsatz von Nebenläufigkeitsparadigmen wie Aktoren, Agenten und Dataflows.
Echte Assertions
Auf den ersten Blick könnte man vermuten, dass Groovys assert Schlüsselwort das gleiche tut wie Java – und in erster Näherung ist das auch richtig: Ein assert erhält als Parameter einen booleschen Ausdruck, der zur Laufzeit ausgewertet wird und im false-Falle zum Abbruch des Programms mit einem AssertionError führt. Assert-Ausdrücke in Groovy haben jedoch zwei grundlegende Unterschiede:
- Sie müssen nicht eingeschaltet und können auch nicht ausgeschaltet werden2. Dadurch wird
assertzu einem Partner, auf den man sich verlassen kann. - Die von einem fehlschlagenden Assert gelieferten Fehlermeldungen sind sehr detailliert und aussagekräftig, man nennt dieses Feature daher Power Assertions. Ein Beispiel:
def actual = [1, 2, 3] def expected = [1, 2, 4] assert actual == expected
ergibt die folgende Fehlermeldung:
Assertion failed: assert actual == expected | | | | | [1, 2, 4] | false [1, 2, 3] at MyProgramm.aMethod(MyProgramm:42)
Diese beiden Eigenschaften machen assert zum Assertion-Konstrukt erster Wahl; und das auch in [JUnit]{#anhang-junit}-Testfällen, in denen man in Java ein Assert.assertEquals oder ähnliches einsetzen würde.
Meta-Programmierung und weitere Features
Groovy hat noch zahlreiche weitere nützliche Features; viele davon fallen in das Gebiet der Metaprogrammierung:
- Analyse und Veränderung von Programmverhalten zur Laufzeit, auch Dynamic Groovy genannt. Beispielsweise kann man jeder Klasse zusätzliche Methoden spendieren:
String.metaClass.hellofy = { "Hello, $delegate!" } assert 'Johannes'.hellofy() == 'Hello, Johannes!' -
AST Transformationen werden vom Compiler ausgewertet und erlauben fast beliebige Veränderung des abstrakten Syntaxbaums eines Programmes vor dessen Ausführung. Zahlreiche nützliche Transformationen sind bereits in Groovy eingebaut, wie z.B.
@TailRecursive:@TailRecursive long sizeOfList(list, counter = 0) { if (list.size() == 0) return counter return sizeOfList(list.tail(), counter + 1) } // Without @TailRecursive a StackOverFlowError // is thrown. assert sizeOfList(1..10000) == 10000
Hier noch eine kleine, hochgradig unvollständige Featureliste, um dem Leser den Mund wässrig zu machen:
-
GStrings sind Strings in doppelten Hochkommata, die durch
$oder${}gekennzeichneten Code enthalten können. - Literale Erzeugung
- von Listen:
[1, 'hallo']oder[1, 2, [31, 32]] - von Maps:
[a:1, b:2, 'äöü': ['ä', 'ö']] - von Bereichen:
5..8oder'a'..'f'
- von Listen:
- Fähigkeit sämtliche Operatoren zu überladen
- Native Unterstützung von regulären Ausdrücken
- Ein mächtiges Builder-Konzept zum Bauen von GUIs, Markup und anderen komplexen Objekten
- Viel Unterstützung für das Schreiben interner DSLs (domain specific languages)
Eine kurzweilige Variante, um sich mit Groovy vertraut zu machen, ist das Projekt GroovyKoans: eine Sammlung von fehlschlagenden Testfällen, die man zum Laufen bringen soll. Viel Spaß!
- Groovy macht das immer, es sei denn, eine Datei, eine Klasse oder eine Methode wurde explizit mit
@CompileStaticmarkiert, was zu statischer Kompilierung à la Java führt – und damit auch zu Method-Dispatching anhand des statischen Compiletime-Typs.↩ - In Java ist die Auswertung von asserts per Default ausgeschaltet und muss über die Schalter
-eabzw.-enableassertionsexplizit aktiviert werden. Die Folge: asserts werden in “normalen” Java-Programmen kaum eingesetzt.↩