Klassen- und Objektnotation, Referenzierungsmöglichkeiten, Operatoren, Kontrollstrukturen und Meta-Objekt-Protokoll

Groovy für Java-Entwickler: Ausdruckskraft durch starke Syntax

Dierk König

Im ersten Teil der Groovy-Serie haben wir gezeigt, wie man Java-Projekte um die dynamischen Eigenschaften von Groovy ergänzen kann. Der zweite Teil stellt nun die ausdrucksstarke Syntax von Groovy vor, die es erlaubt, Sachverhalte mit den denkbar einfachsten Mitteln kompakt und verständlich abzubilden. Zu diesen Mitteln gehören die von Java vertraute Klassen- und Objektnotation, vereinfachte Referenzierungsmöglichkeiten, mächtige Operatoren und erweiterte Kontrollstrukturen und – wie immer in Groovy – das Meta-Objekt-Protokoll.

Man braucht kein Kognitionswissenschaftler zu sein, um zu erkennen, wie wichtig die Darstellung eines Sachverhalts für seine Verständlichkeit ist. Wie sähe unsere Welt wohl heute aus, wenn wir Zahlen nicht in arabischen, sondern nur in römischen Ziffern darstellen könnten? 42 und XLII tragen beide die gleiche Information, aber schon der Versuch, den Wert zu verdoppeln, zeigt den Unterschied. 84 lässt sich direkt aus der Ziffernfolge erschließen, nicht aber LXXXIV. Selbst die einfachsten Berechnungen wären so mühselig, dass jeglicher technologischer Erkenntnisfortschritt massiv behindert wäre.

Zugegeben, nicht ganz so dramatisch, aber doch merklich ist die Vereinfachung der Notation, die Groovy im Vergleich zu Java bringt. So wird zum Beispiel aus dem klassischen Java-Code

public class Hallo {
    public static void main(String[] args) {
        System.out.println("Hallo liebe JavaMagazin Leser!");
    }
}

die denkbar einfachste Groovy-Zeile
println „Hallo liebe JavaMagazin Leser!“
Übrigens ist auch die Hallo-Klassendefinition ein vollständig gültiges Groovy-Programm und ein Beispiel dafür, dass Groovy identisch wie Java aussehen kann, wenn auch unter Preisgabe der Groovy-Errungenschaften. Diese Eigenschaft erleichtert den Einstieg in Groovy, führt aber gerne zu dem Trugschluss, die Groovy-Syntax sei eine Obermenge der Java-Syntax. Das ist nicht der Fall. Einige Java-Konstrukte werden in Groovy nicht benötigt, wie Nested Classes oder die klassische for(init;test;post)-Schleife.

Viele Syntaxelemente, die in Java verlangt sind, sind in Groovy optional:

  • Semikolon als Anweisungsbegrenzer
  • Klammern um nicht leere Argumentlisten bei Top-Level-Ausdrücken
  • explizite return-Anweisung

Einfache Skripte wie oben werden, wenn nötig, transparent in Klassen und Methoden verpackt. Die Deklaration von Checked Exceptions ist nicht notwendig und außerdem ist die Liste der standardmäßig importierten Pakete deutlich länger: groovy.lang.*, groovy.util.*, java.lang.*, java.util.*, java.net.* und java.io.* sowie die Klassen java.math.BigInteger und BigDecimal.

Das Erbe von Java

In den grundlegenden Konzepten folgt Groovy dem Java-Vorbild. Kommentare, Pakete, Imports, Klassen, Methoden, Felder und Initialisierungsblöcke sind in ihrer einfachsten Erscheinungsform identisch mit Java. Für Imports gibt es zusätzlich ein Type Aliasing mit dem as Schlüsselwort. Methodenparameter können auch ohne statischen Typ deklariert werden. Parameter werden optional, wenn ihnen mit = ein Standardwert zugewiesen wird. Durch Angabe des Typs Object[] werden Parameterlisten variabler Länge spezifiziert.

Der Typ eines Feldes, des Rückgabewertes einer Methode oder einer lokalen Variablen kann wie in Java angegeben werden. Ersatzweise setzt das Schlüsselwort def die dynamische Typisierung in Kraft. Sogar def kann ausgelassen werden, wenn schon ein modifier die Deklaration als solche kenntlich macht.

class Hallo {

    static main(args) {
        println "So geht's auch."
        println sum(1,1)
        println sum('x','y','z')
    }

    static sum(a, b, c=0) {
        a + b + c
    }
}

Die Wahlmöglichkeit zwischen statischer und dynamischer Typisierung ist eine wichtige Eigenschaft von Groovy. Statische Typen haben viele Vorteile und erst sie ermöglichen das Überladen von Methoden, was eine zwingende Voraussetzung für eine nahtlose Integration mit Java ist. Mit dynamischer Typisierung lassen sich andererseits sehr elegante und universelle Konstruktionen bauen [1]. Als Daumenregel kann man sich merken, dass man statische Typen dann nutzen sollte, wenn man in statischen Typen denkt. Für den Umgang mit arbiträren Objekten – und für vergängliche Ad-hoc-Aufgaben – ist die dynamische Typisierung angebracht.

Die Schlüsselworte static, final, public, private, protected und synchronized werden mit leichten Abweichungen wie in Java verwendet. Die Standardsichtbarkeit von Klassen und Methoden ist public und wenn für Felder keine Sichtbarkeit spezifiziert wird, werden sie als Properties behandelt. Properties bestehen aus einem privaten Feld und zugehörigen getter- und setter-Methoden. Diese Methoden werden implizit im Bytecode generiert. Man kann sie jedoch überscheiben, indem man sie explizit ausprogrammiert. Für finale Properties werden sinnvollerweise keine setter-Methoden erzeugt.

class Spieler {
    String name
    def    nummer, mannschaft
}

So entstehen JavaBeans, für die außerdem ein Konstruktor bereitgestellt wird, der Instanziierungen in der Form new Spieler(name: ‚Schweini‘) erlaubt.

Vom Umgang mit Bohnen

Wenn wie im obigen Beispiel Bean-konforme Zugriffsmethoden verfügbar sind, bietet Groovy die Möglichkeit, auf die Properties in vereinfachter Notation zuzugreifen.

def spieler = new Spieler()
spieler.name = "Poldi"
println spieler.name

Was wie ein Feldzugriff aussieht, ruft in Wirklichkeit die getName()-Methode auf. Das zugehörige Feld ist private und damit nicht zugreifbar. Auf diese Art lassen sich Aufrufe sehr übersichtlich verketten.
println spieler.mannschaft.liga.obmann.name
Weiterhin werden Listen beim Property-Zugriff transparent ausgewertet. Zum Beispiel kann man die Liste aller Methodenobjekte der Spieler-Klasse durchlaufen und auf jedem Objekt getName() aufrufen, sodass eine Liste aller Methodennamen ausgegeben wird.
println Spieler.methods.name
Damit ist der Grundstein für GPath-Ausdrücke gelegt, die wir in der nächsten Folge genauer betrachten werden. Gerade in verketteten Zugriffen ist es besonders aufwendig, sich gegen NullPointerExceptions zu schützen. Groovy erledigt dass mit dem safe dereferencing-Operator: spieler?.mannschaft?.liga evaluiert zu null, falls spieler oder mannschaft null sind.

Anwendung für XML

Der aufmerksame Java Magazin-Leser (und das sind ja alle) mag sich bei der Form dieser Ausdrücke an E4X erinnern [3]. In der Tat erlaubt Groovy eine ganz ähnliche Zugriffsform auf XML Daten.

def text = ""
def mannschaft = new XmlParser().parseText(text)
println mannschaft.spieler.'@name'

schreibt
[Peer, Olli]
auf die Konsole. Das erledigt Groovy mit seinen üblichen Sprachmitteln ganz ohne jede Erweiterung. Man kann sich also darauf verlassen, dass diese Funktionalität bei jedem Einsatz von Groovy zur Verfügung steht.

Der Attributzugriff „@name“ wird übrigens deshalb in Hochkommata gestellt, um ihn von einem Feldzugriff zu unterscheiden. Er wird damit zu einem Property-Zugriff, und dass Properties, die mit dem @-Zeichen beginnen, auf den Attributzugriff abgebildet werden, ist eine Konvention, die speziell für „Knoten“-Konstruktionen à la DOM gilt.

Abläufe flexibel steuern

Die von Java bekannten Mechanismen zur Ablaufsteuerung wie if, while, switch, return, break, continue, try, catch, finally, synchronized haben auch in Groovy ihren Platz, sind jedoch deutlich flexibler. Boolesche Tests werden anhand der „Groovy Truth“ ausgewertet. Zu false evaluieren z.B.: null, 0, leere Strings, leere Collections, leere Maps und fehlschlagende Matcher regulärer Ausdrücke.

Ein Groovy switch kann anhand beliebiger Kandidatenobjekte operieren, wobei jeder case-Teil einen Klassifikator benutzt. Als Klassifkator kann jedes Objekt dienen, dass die Methode Boolean isCase(kandidat) implementiert.

switch(x) {
    case "cool" : println "switch is cool!"
                  break
    case String : println "x ist ein String"
                  break
    default     : println "x ist etwas anderes"
}

Für den Typ Object ist isCase als equals implementiert. Wenn x als „cool“ ist, greift der erste Zweig. String ist vom Typ java.lang.Class. Für diesen Typ ist isCase als isAssignableFrom implementiert. Die Vielseitigkeit der switch-Anweisung ergibt sich aus der Menge an verfügbaren isCase-Implementierungen für Collections, Ranges, Patterns usw., die wir in der nächsten Folge kennen lernen werden.

Groovys for-Schleife folgt der Form for(identifier in iterable){…} und ist der neuen Schleife aus Java 5 äußerlich ähnlich. In Groovy kann jedoch jedes beliebige Objekt als iterable verwendet werden: Iterators, Enumerations, Collections, Maps, Files, Ranges, aber auch Exoten wie eine org.w3c.dom.NodeList oder eine Methodenreferenz.

File datei = new File("/etc/passwd")
for (zeile in datei) {
    println zeile
}

Sollte Groovy beim besten Willen keine Möglichkeit entdecken, wie man dem iterable eine Iterationslogik entlocken kann, dann wird als Rückfalllösung das iterable selbst an den Identifier gebunden und dem Schleifenkörper übergeben.

Zu guter Letzt hat auch assert Einfluss auf die Ablaufsteuerung. Groovy hat gegenüber Java hier den Vorteil, dass man Assertions nicht abschalten kann. Man kann sich auf ihre Ausführung verlassen. Optionale Meldungen werden nicht mit Doppelpunkt, sondern mit Komma angefügt.
assert schokoriegel, „Ohne Verpflegung gehe ich nicht weiter!“

Operatoren selbst gemacht

Groovy operiert ausschließlich auf Objekten, nicht auf primitiven Datentypen. So ist 1 kein int, sondern ein java.lang.Integer und in 1+2 muss der plus-Operator auf diesem Objekt aufgerufen werden. Das geschieht durch den Aufruf der plus-Methode, die Groovy den Nummerntypen hinzufügt. Entsprechende Methoden gibt es für alle Operatoren. Die lange Liste findet sich bei [2].

Das Schöne ist, dass man als Programmierer bei diesem Spiel mitmachen kann, indem man einfach die entsprechende Methode implementiert.

class Euro {
    def menge
    def plus(Euro e) { 
        new Euro(menge: menge + e.menge) 
    }
    boolean equals(e) {
        menge == e.menge
    }
}

def fuffi = new Euro(menge: 50)
assert new Euro(menge: 100) == fuffi + fuffi

Wie man sieht verbindet Groovy den ==-Operator mit der equals-Methode, womit Groovy generell den Äquivalenzvergleich dem Identitätsvergleich vorzieht. Die Identität kann man per x.is(y) vergleichen.

Der gesamte Ansatz wäre wenig nützlich, wenn Groovy nicht eine erhebliche Menge von Operatormethoden für Java Standardtypen Object, List, Map, Number, String etc. bereitstellen würde. Diese Methoden sind Teil des GDK, der Groovy-Erweiterung für die Java-Standardbibliothek. Dort sind neben den Operatormethoden auch Implementierungen von z.B. Object#println() vorhanden, so dass man println() überall im Code verwenden kann.

Das Hinzufügen von neuen Methoden zu bestehenden Klassen bewerkstelligt Groovy über sein Meta-Objekt-Protokoll. Man kann es so sehen, als ob Groovy den Punkt-Operator für die Methoden-Derefenzierung überschreiben würde.

… und so geht’s weiter

In der nächsten Folge werden wir den GDK-Methoden wieder begegnen, wenn wir uns mit der speziellen Unterstützung befassen, die Groovy für die Verwendung von Number, String, List, Map und regulären Ausdrücken bietet. Neu hinzu kommt der Typ Range und das Konzept von Closures, das wesentlich zur dynamischen Natur von Groovy beiträgt.

Wir werden erleben, wie sich alle Konzepte gegenseitig zu einem Gesamtbild ergänzen: die ausdrucksstarke Syntax, das reichhaltige GDK und Closures für bewegliche Logik.

Dierk König ist Softwareentwickler bei der Canoo Engineering AG in Basel, Committer in den Projekten Groovy und Grails, sowie Autor des Buchs „Groovy in Action“.
Geschrieben von
Dierk König
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.