First class citizens: Zahlen, Strings, Reguläre Ausdrücke, Listen, Maps, Ranges und Closures

Groovy-Datentypen

Dierk König

Die Teile 1 und 2 der Groovy-Serie haben gezeigt, wie man Groovy in Java-Projekten einsetzt, um von Groovys dynamischen Eigenschaften und seiner ausdrucksstarken Syntax zu profitieren. Diese Folge stellt die Datentypen vor, die Groovy als „first class citizens“ unterstützt: Zahlen, Strings, Reguläre Ausdrücke, Listen, Maps, Ranges und Closures. Die bevorzugte Stellung dieser Typen ergibt sich aus den literalen Deklarationsmöglichkeiten und den standardmäßig verfügbaren Operatoren. Duch diese bevorzugte Behandlung gehen gerade die häufigen Programmieraufgaben sehr viel leichter von der Hand.

Groovy – die Serie:

Java kennt primitive Datentypen und Referenzdatentypen. Literale Deklarationsmöglichkeiten und Operatoren gibt es nur für primitive Datentypen (mit der Ausnahme von java.lang.String und einer Spezialsyntax für Array-Deklarationen). Natürlich gibt es gute Gründe für dieses Design, aber schön ist es nicht. Im Gegensatz dazu verzichtet Groovy ganz auf primitive Datentypen und stellt ausschließlich Referenzdatentypen bereit. Das hat einige Auswirkungen. Erstens wäre es natürlich furchtbar umständlich, statt z.B. dem Literal 1 immer new Integer(1) zu schreiben. Deshalb unterstützt Groovy die literale Deklaration für eine Auswahl bestimmter Typen. Für Groovy ist 1 lediglich eine Kurzschreibweise für new Integer(1).

Zweitens muss folglich in Ausdrücken wie 1+1 der plus-Operator für den Typ Integer verfügbar gemacht werden. Das geschieht durch den Aufruf der zugehörigen Operatormethode. In diesem Fall führt Groovy new Integer(1).plus(new Integer(1)) aus. Weil plus eine normale Methode ist, kann man sie in jeder Klasse implementieren oder gegebenenfalls die Implementierung der Oberklasse überschreiben. Die Beschreibung aller verfügbaren Operatormethoden findet sich in der Dokumentation [1] und [2].

Drittens operiert Groovy ja auf Java-Objekten, die beim Methodenaufruf primitive Datentypen als Argumente erwarten können. Diese müssen für den Methodendispatch bereitgestellt werden. Abbildung 1 zeigt dies an dem String ‚ABCDE‘, der – obwohl in Groovy deklariert – vom Typ java.lang.String ist. Für den Aufruf der Methode indexOf wird der primitive Typ int erwartet. Groovy versteht unseren Wert 67 für den Buchstaben „C“ aber als new Integer(67). Analoges gilt für den Rückgabewert.

Abb. 1: Groovy Autoboxing

Auf diese Art wird es erst möglich, dass sich Groovy- und Java-Implementierungen transparent ersetzen können. Wenn zum Beispiel eine Methode mit mehreren Typen überladen ist, wird die statische Typinformation für die Disambiguierung benötigt. Für Skriptsprachen, die keine solche Information tragen, ist die Integration mit Java stark eingeschränkt. Mit Groovy hat man die Möglichkeit, die statische Typinformation dort bereitzustellen, wo sie benötigt oder gewünscht wird, und sie dort wegzulassen, wo sie stört.

Zahlen und Arithmetik

In Groovy sind alle Zahlen Objekte – aber von welchem Typ? Nun, man kann den Typ explizit angeben. Andernfalls kümmert sich Groovy darum, einen passenden Typ zu finden. Wie erwartet wird 1 ein Integer, 1.0 oder 1f ein Float, 1L ein Long, usw. Über Java hinaus kommen noch 1G für BigInteger und 1.0G für BigDecimal hinzu. Die vollständige Liste ist in der Dokumentation.

Die Operatoren für Addition, Subtraktion und Multiplikation geben grob gesagt jeweils ein Ergebnis vom Typ des Operanden zurück, der den größeren Wertebereich hat. Im Gegensatz zu anderen Sprachen kann es weiterhin einen Überlauf geben, wenn zum Beispiel zwei Integer-Zahlen addiert werden und das Ergebnis der Wertebereich von Integer überschreitet. Lediglich der Exponenzialoperator gibt – wenn nötig – ein Ergebnis mit größerem Wertebereich zurück.

Divisionen sind besonders schlau. Wenn ein Operand ein Float oder Double ist, dann ist das Ergebnis ein Double, anderenfalls ein normalisiertes BigDecimal. Damit gilt in Groovy (1/2==0.5), während in Java (1/2==0) gilt. Diese Eigenschaft ist extrem hilfreich, wenn Fachbereichsmitarbeiter Formeln erfassen.

Zeichenketten ohne Ketten

Programmieren in Groovy soll groovy sein: angenehm, stressfrei, einfach passend. Strings leisten einen erheblichen Beitrag dazu, weil so viel Programmieraktivität mit diesem Datentyp verbunden ist und sich schon kleine Erleichterungen entsprechen multiplizieren.

Literale Groovy-Strings kann man in einfachen oder doppelten Hochkommata deklarieren: ‚Hallo‘ oder „Freunde“. In doppelten Hochkommata können Platzhalter verwendet werden in der Form

psenv::pushli(); eval($_oclass[„referenz“]); psenv::popli(); ?>

“ oder

psenv::pushli(); eval($_oclass[„{„]); psenv::popli(); ?>

ausdruck}“, ähnlich zu interpolierten Strings in anderen Sprachen. Diese Konstruktion nennt sich GString, was im Englischen auch Tanga bedeuten kann.

GStrings kapseln alle statischen Textbestandteile und Referenzen auf die Resultate der Platzhalter. Erst wenn der GString als String verwendet wird, werden die Teile zusammengesetzt. Das erlaubt sehr fortgeschrittene Verwendungen bis hin zu flexiblen Vorlagen (Template Engines). Die Groovy-SQL-Unterstützung kann aus GStrings zum Beispiel echte PreparedStatements erzeugen, ohne die lästige Fragezeichen-Syntax: sql.execute „delete from PERSON where id=

psenv::pushli(); eval($_oclass[„id“]); psenv::popli(); ?>


String-Deklarationen können sich in Groovy auch über mehrere Zeilen erstrecken, wenn die Hochkommata verdreifacht werden, ähnlich wie HERE-Dokumente in anderen Sprachen:

def zweiZeilen = """eins
und zwei"""

In Groovy gelten die für Java üblichen Escaping-Regeln für Sonderzeichen mittels Backslash. Diese Regeln machen die Arbeit mit Regulären Ausdrücken in Java sehr unübersichtlich. Deshalb bietet Groovy eine Möglichkeit, Strings auch ohne Escaping in Slashes zu deklarieren: String aWort = /aw*/. Mit dem Negationsoperator (Methode negate()) kann man aus einem beliebigen String ein java.util.regex.Pattern-Objekt machen: def aWortPattern = ~/aw*/. Darüber hinaus erleichtert Groovy den Umgang mit Regulären Ausdrücken mit dem find-Operator =~ und dem match-Operator ==~.

def text = 'aWort'
if (text =~  /aw*/) 
    println 'Text enthaelt ein aWort.'
if (text ==~ /aw*/) 
    println 'Der ganze Text ist ein aWort.'

In der Dokumentation kann man ein ganzes Arsenal an Methoden nachlesen, mit denen man Texte mittels Regulärer Ausdrücke untersuchen und bearbeiten kann, inklusive Ersetzung aller matches durch fixe Werte, backmatches, Gruppierungen oder berechnete Werte.

Listen, Maps und Ranges

Auf jedes Objekt list vom Typ java.util.List kann man in Groovy per list[0] zugreifen, um das erste Element zu bekommen. Eine ArrayList kann man einfach als [1,2,3] anlegen. Konsequenterweise erzeugt man eine leere Liste per []. Zuweisungen erfolgen per list[3]=4.

Auf jedes Objekt map vom Typ java.util.Map kann man in Groovy per map[key] zugreifen, um den Wert von key zu bekommen. Eine HashMap kann man einfach als [a:1,b:2] anlegen. Konsequenterweise erzeugt man eine leere Map per [:]. Zuweisungen erfolgen per list[key]=value. Bei Maps kann man sogar statt der Klammern die Property-Notation verwenden: def value=map.key; map.key=value.
Ranges haben keine Entsprechung in Java. Sie werden als 0..9 oder gestern..morgen deklariert. Sie haben eine linke und rechte Grenze und das Wissen, wie man den aufgespannten Bereich durchschreitet. Die Elemente müssen Comparable sein und previous() und next() implementieren (die Operatoren ++ und –). Mit diesen Voraussetzungen kann jeder Typ als Grenze für einen Range verwendet werden. Die linke Grenze eines Ranges darf auch größer sein als die rechte. Halbexklusive Ranges lassen sich per 0.. deklarieren, sodass die rechte Grenze nicht zum Range gehört. Ihre Anwendung finden Ranges einerseits beim Iterieren wie in for (i in ‚a‘..’z‘) { println i }, aber auch gerade im Zusammenhang mit Listen, wenn auf Teile der Liste zugegriffen oder zugewiesen wird. Im letzten Fall können die Ranges auch unterschiedlich groß sein, sodass die Liste durch die Zuweisung wächst oder schrumpft.

def list    =  [0, 1, 3]
assert list[0..1] == [0, 1]
list[0..1]  =  [0, 1, 2]
assert list == [0, 1, 2, 3]

Für Listen, Maps und Ranges bietet Groovy eine große Vielfalt an Methoden und Operatoren, teils für jeden einzelnen Typ, teils in Kombination. Auf diese Art werden Vereinigung, Differenzmenge, Intersektion usw. auf natürliche Weise abgebildet. Dazu kommen die vielen Möglichkeiten, über diese Datentypen zu iterieren oder ihren Inhalt zu verändern. Als Standard-Java-Datentypen sind für Listen und Maps auch weiterhin alle Methoden aus dem JDK einsetzbar.

Typisch Closure

Typen beschreiben Zustand und Verhalten. Eine Closure ist ein Typ, der selbst keinen Zustand im üblichen Sinne hat, sondern nur Verhalten. Das macht die Klassifizierung als Datentyp etwas ungewöhnlich. Selbst ohne eigenen Zustand operiert die Closure doch auf Referenzen ihrer Umgebung: ihrem Kontext oder auch scope. Closures werden deklariert, indem man die Anweisungen, die das Verhalten ausmachen, in geschweifte Klammern fasst. Ausführen kann man die Closure wie eine Methode oder durch call().

def x = 0 // im Deklarations-scope der Closure
Closure printer = { println x }
printer() // alternativ: printer.call()

Closures können genau wie Methoden Parameter deklarieren. Diese werden mit dem -> Zeichen vom Rumpf getrennt.

Closure printer = { line -> println line }
printer(0)

Für den häufigen Fall eines einzelnen Parameters kann man stattdessen it verwenden: Closure printer = { println it }. Im Gegensatz zu einer Methode ist eine Closure keine Kontrollstruktur, sondern ein ganz gewöhnliches Objekt, das man z.B. als Argument für einen Methodenaufruf verwenden kann. So nimmt die each-Methode für Ranges eine Closure als Argument und führt diese für jedes Element des Ranges aus: 0..9.each(printer). Den gleichen Effekt errreicht man mit 0..9.each { println it }. Der große Vorteil liegt darin, dass man Methoden wie each nur einmal implementieren muss und damit strukturelle Duplikation vermeidet. Noch deutlicher wird das bei diesem Beispiel: new File(‚test.txt‘).eachLine { println it }
Hier übergibt die eachLine-Methode Zeile für Zeile des Files zur Ausführung an die Closure. Die zentrale eachLine-Methode sorgt für eine korrekte Dateibehandlung wie das Schließen des File-Handles in einem Finally Block. Das wird häufig vergessen, ist aber nur die Spitze des Eisbergs. Das korrekte Beenden von Transaktionen, Rückgabe von Ressourcen in einen Pool, Entfernen von Observern, Neuzeichnen des GUIs und viele andere Arten der Ressourcenbehandlung werden vor allem deshalb falsch gemacht, weil der Code so massiv dupliziert ist.

Auch in Java kann man eine zentrale Ressourcensteuerung mit entsprechenden Interfaces und anonymen inner classes implementieren. Warum wird das so selten gemacht? Weil es sich eben in Java so extrem sperrig liest.

Für die Stabilität und Verlässlichkeit eines Softwaresystems ist die zentrale Behandlung von Ressourcen von größter Bedeutung. Sie ermöglicht auch eine zentrale Fehlerbehandlung und eine deutlich bessere Testbarkeit. Schauen Sie in Ihre Test Coverage: Wie viele der nicht abgedeckten Zeilen gehören zur Fehlerbehandlung? Oft lassen sich diese Fehler für den Test nicht oder nur mit prohibitivem Aufwand erzeugen. Eine zentrale Fehlerbehandlung ist trivial zu testen:

recourceHandler.withResource { resource -> 
    throw new WhateverException() 
}

Sprachen, die Konzepte wie Closures (oder Methodenreferenzen oder Funktions-Pointer) in einer einfach zugänglichen Form unterstützen, tragen damit zur Sicherheit ihrer Software bei.

Iterationen und gesicherter Zugang zu Ressourcen sind die augenfälligen und unmittelbar einleuchtenden Vorteile von Closures. Sie spielen aber auch eine wichtige Rolle bei der Verwendung von Groovy als dynamischer Sprache. Man kann sie für die spätere Ausführung speichern, z.B. in eigenen oder fremden Properties. Gespeicherte Closures kann man wieder ersetzen. Man kann sie als transparente EventHandler an GroovyBeans zuweisen. Man kann ihren Ausführungskontext beeinflussen. Man kann Methoden als Closures referenzieren.

Closures erlauben sogar den Einstieg in die funktionale Programmierung mit Groovy. Mit der curry()-Methode lassen sich Referenzen an Parameter binden, was besonders interessant wird, wenn diese Parameter wiederum Closures sind. Auf diese Art lassen sich Vorlagen von Algorithmen erstellen, die durch das Injizieren von Logik-Bestandteilen zum Leben erweckt werden.

… und so geht’s weiter

In der nächsten Folge werden wir das gesammelte Wissen aus den ersten drei Folgen zusammentragen, um einige alltägliche und auch einige nicht so alltägliche Aufgaben mit Groovy zu realisieren. Wir begegnen wieder den dynamischen Eigenschaften von Groovy und natürlich den Datentypen erster Klasse, ohne die kein Groovy-Programm auskommt.

Natürlich lässt sich in einem Artikel nicht die gesamte Reichhaltigkeit der Groovy-Bibliothek vorführen und erklären. Dafür sind andere Dokumentationen verfügbar. Wir haben aber den Grundstein für das Verständnis der Datentypen gewonnen und können darauf aufbauend unseren Kenntnisstand in der täglichen Arbeit vervollständigen. Viel Spaß dabei!

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“.
Dierk König et al.: Groovy in Action, Manning, 2006
Geschrieben von
Dierk König
Kommentare

Schreibe einen Kommentar

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