Zum Inhalt springen

Groovy: Sprachliche Unterschiede zwischen Groovy und Java

Aus Wikibooks

Trotz aller Ähnlichkeiten gibt es einige wenige Abweichungen in der Sprachsyntax zwischen Groovy und Java, die zu beachten sind. Gravierender ist eine Reihe von Unterschieden, die nicht sofort erkennbar sind, weil sie nicht die syntaktische Form der Sprache, sondern die Bedeutung dessen, was Sie programmieren, betrifft. So sieht beispielsweise die Anweisung int i=0; in beiden Sprachen gleich aus, aber die Bedeutung unterscheidet sich gravierend: In Groovy können Sie anschließend i.toString() schreiben, in Java würde dies zu einem Compilerfehler führen.

Wir wollen uns in diesem Abschnitt jene von Java bekannten Sprachelemente ansehen, die es in Groovy ebenfalls gibt, sich dort aber in der Form oder in der Funktionalität unterscheiden.

Vordefinierte Methoden

[Bearbeiten]

In unseren Beispielen haben Sie schon verschiedentlich gesehen, dass wir einfach die Methoden print() oder println() aufgerufen haben und nicht, wie es in Java nötig wäre, System.out.print() oder System.out.println(). Groovy kennt einen Mechanismus, mit dem vorhandene Klassen um zusätzliche Methoden erweitert werden können, und davon wird auch ausgiebig Gebrauch gemacht. Eine ganze Reihe solcher zusätzlicher Methoden sind der Klasse Object zugeordnet, und dies bedeutet, dass sie an jeder Stelle des Programms ohne Qualifizierung durch einen Variablennamen verwendet werden können.

Zu diesen vordefinierten Methoden gehören unter anderen die folgenden:

void print (Object value)
void println (Object value)
void println ()
void printf (String format, Object... values)

Alle delegieren den Aufruf an die gleichnamigen Methoden in System.out weiter, wobei printf() allerdings nur zur Verfügung steht, wenn Groovy unter einer Java-5.0-JVM oder höher läuft.

Ein anderes Beispiel ist die Klasse File, die unter Groovy zahlreiche zusätzliche Methoden hat, die unter Java nicht zur Verfügung stehen. Darunter ist auch die Methode getText(), die den gesamten Inhalt der durch ein File-Objekt bezeichneten Datei in einen String einliest.

winini = new File("C:\\windows\\win.ini").getText()

Der Mechanismus wird in Vordefinierte Methoden ausführlicher erklärt und Beispiele für deren Verwendung der vordefinierten Methoden gibt es überall in diesem Buch.

Typen und Variablen

[Bearbeiten]

Groovy ist eine Sprache, die sowohl statische als auch dynamische Typisierung ermöglicht. Dies hat entsprechende Konsequenzen für den Umgang mit Objekten und Variablen.

Alles ist ein Objekt

[Bearbeiten]

Groovy arbeitet nicht wie Java mit primitiven Datentypen wie boolean, char, byte, int, long, float und double. Allerdings können sie einem Groovy-Programm durchaus vorkommen, denn Groovy muss mit anderen Java-Komponenten kooperieren können, und diese verzichten gewöhnlich nicht auf den Gebrauch von primitiven Datenelementen in ihren Schnittstellen. Groovy erlaubt also die Deklaration von Klassen-Membern, Arrays-Elementen sowie Methoden- und Konstruktor-Parameter mit primitiven Typen. Sobald Sie aber in irgendeiner Weise darauf zugreifen, wird der betreffende Wert erst einmal temporär in ein Objekt des korrespondierenden Wrapper-Typs umgewandelt, als in ein Boolean, Character, Byte, Integer, Float oder Double.

groovy> int i = 100
groovy> println i.getClass()
groovy> char c = 'x'
groovy> println c.getClass()
class java.lang.Integer
class java.lang.Character

Die primitiven Typen mögen in Java zwar ihre – vielleicht eher historisch zu sehende – Berechtigung haben, aber sie bilden dort als nicht-objektorientierte Insel ein Problem, weil ständig eine besondere Behandlung dieser Daten erforderlich ist.

Warnung: Etwas Vorsicht ist bei lokalen Variablen innerhalb von Methoden angebracht. Groovy erlaubt Ihnen auch dort, primitive Variablen zu definieren, verwendet aber in Wirklichkeit Objekte – und die können auch null sein. Denken Sie also daran, solche Variablen immer zu initialiseren.
int i

Die nicht initialisierte lokale Variable i ist in Groovy eine Instanz von Integer und hat den Wert null. Dies gilt aber nicht für nicht initialisierte Member-Variablen; diese werden auch in Groovy als primitive Werte angelegt und standardmäßig initialisert wie in einer normalen Java-Klasse.

Bislang musste man etwa Zahlen, die man in einem Container-Objekt, zum Beispiel in einer Liste, speichern möchte, erst in Wrapper-Objekte hüllen, auf die man dann aber keine arithmetischen Operatoren anwenden kann. Seit Java 5 werden diese Manöver auch hier durch das Auto-Boxing und Auto-Unboxing automatisiert und unsichtbar im Hintergrund vorgenommen.

groovy> List zahlen = [] // erzeugt neue ArrayList
groovy> zahlen.add 100
groovy> zahlen.add 1.5
groovy> println zahlen[0] * zahlen[1]
150.0

Jedes Datenelement ist ein Objekt, und trotzdem sind arithmetische Operatoren anwendbar. Alle Daten können ohne Umwandlung in Container gespeichert werden, und auch sonst werden sie in konsistenter Weise behandelt. Beispielsweise ist ein Aufruf von toString() bei jedem beliebigen Datenelement möglich, denn es ist in jedem Fall von der Klasse Object abgeleitet, in der diese Methode definiert ist.

Dasselbe gilt auch für die Literale dieser Datentypen. Was Sie in Java nur mit String-Literalen machen können, nämlich direkt Methoden an ihnen aufrufen, z.B. "abc".toUpperCase(), geht bei Groovy auch mit Zahlen sowie mit true und false – und im begrenzten Umfang sogar mit null. Alle die folgenden Anweisungen sind korrekt, denn wir rufen Methoden auf, die für java.lang.Object definiert sind und daher für alle Klassen gelten müssen.

groovy> Class c = 1.getClass()
groovy> String s = (1.2).toString()
groovy> Integer i = true.hashCode()
groovy> Boolean b = null.equals(s)

Wie bereits erwähnt, können Sie durchaus Arrays mit primitiven Typen definieren. Da Arrays auch in Java Objekte sind, ist hier auch für Groovy die Welt in Ordnung.

groovy> int[] i = new int[100]
groovy> i.getClass().getName()
==> [I

Wie Sie an dem etwas kryptischen Klassennamen [I erkennen können, ist i tatsächlich ein Array aus einfachen int-Zahlen. Beim Auslesen einzelner Werte aus einem solchen Array oder beim Speichern eines einzelnen Wertes in ein solches Array findet eine automatische Typumwandlung statt.

groovy> i[0].getClass().getName()
==> java.lang.Integer

Der Typ des Elements mit dem Index 0 scheint java.lang.Integer zu sein; in Wirklichkeit ist dies aber der Wert, den das Element nach der Umwandlung annimmt. Derselbe Mechanismus wird übrigens beim Aufruf von Methoden fremder Java-Klassen angewendet, deren Schnittstellen primitive Typen umfassen (siehe unten).

Dynamische Typisierung

[Bearbeiten]

Jede Variable in Groovy ist also, konsequenter als bei Java, eine Objektreferenz. Allerdings ist diese Typisierung von dynamischer Natur: Sie können Variablen definieren, deren Typ unbestimmt ist. Der Typ einer solchen Objektreferenz wird erst zur Laufzeit dadurch festgelegt, dass ein Objekt eines bestimmten Typs zugewiesen wird. Zur Definition einer typlosen Variablen geben Sie statt des Typnamens das Schlüsselwort def an.

groovy> def v = "Ein Text"
groovy> println v.toUpperCase()
groovy> v = 125
groovy> println v.doubleValue()
EIN TEXT
125.0

Wie Sie sehen, ist v anfangs vom Typ String, weil wir gleich bei der Definition einen String zugewiesen haben, und nachdem wir eine Zahl zugewiesen haben, hat v den Typ Integer. Obwohl die Variable selbst nicht mit einem Typ deklariert ist, können wir die für den jeweiligen Typ definierten Methoden toUpperCase() bzw. doubleValue() aufrufen, ohne vorher einen Typecast vornehmen zu müssen.


Typisch Ente

Diese Art der Typisierung wird gelegentlich als duck typing bezeichnet: Es sieht aus wie eine Ente, schnattert wie eine Ente und schwimmt wie eine Ente - also ist es für uns auch eine Ente. Groovy ruft eine Methode eines Objektes auf, wenn es diese in einer passenden Signatur an diesem Objekt gibt und nicht nur, wenn die Variable entsprechend deklariert ist. Übertragen auf den Zoo heißt dies, wir betrachten ein Tier als Ente, wenn es so aussieht und sich so verhält wie eine Ente, und gehen nicht nur danach, was auf dem Schild am Gitter steht.

Der Ausspruch ist übrigens eine im englischen Sprachraum verbreitete Redewendung, die dem amerikanischen Dichter J.W. Riley zugeschrieben wird.

Das Schlüsselwort def können Sie einfach weglassen, wenn Sie einen Typmodifikator wie static, private, final usw. verwenden. Das def ist also nur dann erforderlich, wenn andernfalls nicht klar ist, dass hier eine Variable deklariert werden soll.

groovy> static a = "Hallo" // statische untypisierte Variable 

Untypisierte Methoden definierten Sie in der gleichen Weise, also mit dem Wort <def anstelle des Rückgabetyps. Bei den Parametern von Methoden ist das def nicht erforderlich, da der Compiler auch so weiß, dass hier Parameter deklariert werden.

groovy> def addiere (a, b) {
groovy>     a+b
groovy> }
groovy> println addiere ("Hallo", "drian")
groovy> println addiere (100, 0.1)
groovy> println addiere (new Date(), 1)
Hallodrian
100.1
Mon Nov 06 15:52:56 CET 2006

In diesem Beispiel definieren wir eine Methode, die zwei Argumente jeden beliebigen Typs annimmt und ein Ergebnis liefert, dessen Typ auch nicht definiert ist. Das Beispiel selbst ist vielleicht etwas banal, aber es zeigt Ihnen, wie einfach es bei dynamischer Typisierung ist, völlig generische Methoden zu schreiben. Diese Methode addiert einfach zwei beliebige Werte, sofern bei ihnen der Plus-Operator definiert ist. In allen anderen Fällen gibt es eine Exception – die wir in einem ordentlichen Programm abfangen sollten.[1]

Nicht nur die mit def definierten Variablen sind dynamisch typisiert. Sehen Sie sich folgendes Beispiel an:

groovy> List meineListe = new Stack()
groovy> meineListe.push("Hallo")
groovy> println meineListe
===> [Hallo]

Die Variable meineListe ist als List deklariert. Wir weisen ihr ein Objekt des Typs Stack zu, der das Interface List implementiert, und rufen dann an s die Methode push() auf, die zwar für Stack, nicht aber für List definiert ist. Sie erkennen, dass die Variable meineListe dynamisch ihren Typ verändert und zu einer Stack-Referenz wird. In einem Java-Programm, in dem die Variablen immer nur entsprechend ihres deklarierten Typs behandelt werden, müssten wir erst einen Typecast auf Stack durchführen.

Allerdings bleibt auch in Groovy bei typisierten Variablen die Typsicherheit gewahrt: Sie können einer Variablen nur ein Objekt zuweisen, das von ihrem Typ abgeleitet ist bzw. ihn implementiert. Folgende Anweisung ist also auch in Groovy nicht erlaubt, allerdings ist sie im Gegensatz zu Java kompilierbar und führt erst zur Laufzeit zu einer Fehlermeldung:

groovy> List meineListe = "ein String"
ERROR org.codehaus.groovy.runtime.typehandling.GroovyCastException:
Cannot cast object 'ein String' with class 'java.lang.String' to class 'java.util.List' 

Automatische Typanpassung

[Bearbeiten]

Das Nebeneinander von typisierten und untypisierten Variablen in Groovy erfordert einen Mechanismus zur automatischen Typanpassung, der im Englischen als coercion (Nötigung) bezeichnet wird. Wir „nötigen“ einen Wert also dazu, einen bestimmten Typ anzunehmen, wenn er einer typisierten Variablen oder einem typisierten Parameter zugewiesen wird, und die Typen nicht kompatibel sind. Dies erspart Ihnen nicht nur die Typecasts; in vielen Fällen brauchen Sie sich überhaupt keine Gedanken mehr darüber machen, mit welchen Datentypen Sie es gerade im Einzelnen zu tun haben.

groovy> def var1 = 10000
groovy> def var2 = 100000000000

Hier ist var1 automatisch vom Typ Integer, dem Standardtyp für ganzzahlige Literale, und var2 ist vom Typ Long, denn Integer reicht für eine so große Zahl nicht aus. Das macht das Leben einfacher, zumal die mathematischen Operationen in der Regel auf beide Typen gleichermaßen anwendbar sind.

Es kann aber auch komplizierter werden. Sehen Sie sich folgenden Programmausschnitt in Java an:

// Java
long longPrimitiv = 100000000000L;
int intPrimitiv = (int)longPrimitiv; // Typecast erforderlich
Long longObjekt = Long.valueOf(100000000000L);
Integer intObjekt = (Integer)longObjekt; // Compiler-Fehler

Die Zuweisung eines long-Wertes an eine int-Variable ist möglich, erfordert wegen des möglichen Informationsverlustes aber einen Typecast. Die direkte Zuweisung eines Long-Objekts an eine Integer-Variable erlaubt Java überhaupt nicht, da die eine Klasse nicht von der anderen abgeleitet ist. Hier müssten Sie longObjekt.intValue() anwenden, um die Zuweisung zu ermöglichen.

In einem Groovy-Programm gestaltet sich dies viel einfacher. Da können Sie Folgendes schreiben:

// Groovy
Long longObjekt = 100000000000L
Integer intObjekt = longObjekt // Ohne Typecast direkt zuweisen.

Der Coercion-Mechanismus führt dazu, dass das Objekt der einen Klasse so gut es geht in ein Objekt der anderen Klasse umgewandelt wird.

Warnung: Die automatische Typanpassung spart Tipparbeit, birgt aber auch Gefahren. In beiden Beispielen hat intObjekt am Ende den Wert 1215752192, da beim Umwandeln in das kürzere Integer ein paar Bits des Long-Objekts verloren gehen. Im Java-Programm weist das erforderliche explizite Typecast Sie noch auf den möglichen Informationsverlust hin, dass hier Gefahr droht; bei Groovy müssen Sie selbst aufpassen...

Sie können die Typanpassung auch explizit steuern, indem Sie hinter einem Ausdruck das Schlüsselwort as und den gewünschten Zieltyp angeben.

groovy> var = 123 as Long
groovy> var.getClass().getName()
java.lang.Long

Denselben Effekt haben Sie übrigens auch, wenn Sie den Typecast-Operator, z.B. (Long)123, anwenden, der in Groovy ungefähr dieselbe Bedeutung wie as hat.

Natürlich lässt sich durch Coercion nicht jeder Wert in jeden beliebigen Typ umwandeln. Bei den Standardtypen weiß Groovy, welche Zuweisungen zulässig sind und wie sie durch geführt werden müssen. In eigenen Klassen können Sie die Typumwandlung mit Hilfe einer Methode asType() selbst steuern, die Groovy aufruft, sobald eine Typumwandlung mit as gefordert ist; die Methode erhält den Zieltyp als Parameter übergeben und liefert ein Objekt dieses (oder eines davon abgeleiteten) Typs.

Standardmäßige Sichtbarkeit von Klassen und Member

[Bearbeiten]

Groovy verwendet dieselben Schlüsselwörter für die Sichtbarkeit wie Java: public, protected, private, und deren Bedeutung unterscheidet sich prinzipiell nicht von Java. Ein wesentlicher Unterschied besteht für den Fall, dass überhaupt kein Modifikator für die Sichtbarkeit angegeben ist.

  • Die standardmäßige Sichtbarkeit für Klassen und Methoden ist public. Dieses Schlüsselwort muss also vor öffentlichen Klassen- und Methodendefinitionen nicht angegeben werden. Die Package-Sichtbarkeit, wie sie in Java beim Fehlen eines Sichtbarkeits-Modifikators gilt, wird von Groovy nicht unterstützt; stattdessen können Sie aber protected verwenden.
  • Die Sichtbarkeit von Feldern innerhalb von Klassen ist private. Allerdings generiert Groovy eigenständig öffentliche Getter- und Setter-Methoden, sofern diese nicht explizit programmiert worden sind. Felder ohne Sichtbarkeits-Modifikator werden also automatisch zu Properties im Sinne der JavaBean-Spezifikation. Mehr dazu weiter unten unter JavaBeans.

Hinweis: Wir müssen der Ordnung halber darauf hinweisen, dass Groovy die Privatsphäre von Objekten zur Zeit wenig achtet. Das merken Sie, wenn Sie beispielsweise einmal dies hier eingeben:

groovy> println "abc".value
abc

Wir greifen hier munter auf das private Feld value des String-Objekts zu, ohne dass Groovy uns daran in irgendeiner Weise hindert. Dieses Verhalten soll sich erst zur Groovy-Version 2.0 ändern, was bedeutet, dass Sie noch auf absehbare Zeit selbst auf die ordnungsgemäße Kapselung von Klassen-Interna achten müssen.


Elementare Datentypen

[Bearbeiten]

Groovy arbeitet mit elementaren Datentypen für Zahlen und Texte etwas anders als Java. Die Unterschiede fallen nicht unbedingt gleich auf, führen aber dazu, dass Groovy-Programme unter Umständen andere Ergebnisse haben können als gleich aussehende Java-Programme.

Brüche und große Zahlen

[Bearbeiten]

Wenn eine Zahl (Literal oder Rechenergebnis) nicht mehr durch ein Integer oder Long abbildbar ist, weil sie zu groß oder weil sie nicht ganzzahlig ist, bedient sich Groovy stattdessen automatisch der Typen BigInteger oder BigDecimal. Dadurch können Rechenergebnisse geringfügig von den Werten abweichen, die man in Java bei Anwendung derselben Formeln erhält. Sie können aber auch wie gewohnt mit Float und Double rechnen, nur müssen Sie die Literale entsprechend mit den Kennbuchstaben f/F oder d/D kennzeichnen.

Die BigInteger- und BigDecimal-Objekte können Sie in arithmetischen Formeln einsetzen wie andere Zahlen auch.

groovy> println 1.1 + 1.7
2.8
groovy> println ((1.1 + 1.7).getClass())
class java.math.BigDecimal

Sie sehen, dass hier die Addition von 1.1 und 1.7 ein exaktes Ergebnis liefert, da die Berechnung mit BigDecimal ausgeführt wird. In kommerziellen Zusammenhängen ist dies im Allgemeinen angemessener, aber für technisch-wissenschaftliche Berechnungen können Sie nach wie vor auch die bekannten Fließkommatypen benutzen, indem Sie die Variablen entsprechend deklarieren und die Konstanten mit den Buchstaben d oder f kennzeichnen. Hier ist die gleiche Berechnung mit Double-Zahlen:

groovy> println 1.3d + 0.4d
1.7000000000000002
groovy> println ((1.3d + 0.4d).getClass())
class java.lang.Double

Dem Thema Zahlen und Arithmetik widmen wir uns noch ausführlich in Zahlen und Arithmetik.

Strings und Zeichen

[Bearbeiten]

Normale String-Literale werden in Groovy in einfache oder doppelte Hochkommas eingeschlossen, also etwa so:

String s1 = 'Dies ist ein String-Literal'
String s2 = "Dies ist ein String-Literal"

Wenn Sie doppelte Anführungszeichen verwenden, können Sie im String interpolieren, das heißt beliebige Groovy-Variablen oder -Ausdrücke einfügen; sie werden dann zu "GString"-Objekten.

String s3 = "Die Zeit ist: ${new Date()}"

Groovy kennt auch String-Literale, die über mehrere Zeilen reichen. Schließen Sie diese auf beiden Seiten durch drei einfache Anführungszeichen ab.

String s = '''Dies ist ein
besonders langer String.'''

Dieses Literal ist gleichwertig wie 'Dies ist ein\n besonders langer String'. Beachten Sie, dass der Zeilenumbruch unabhängig von der Systemplattform immer in ein \n-Zeichen übersetzt wird und dass die Leerzeichen vor und hinter den Zeilenumbrüchen erhalten bleiben.

In Java dient das einfache Hochkomma als spezielles Begrenzungszeichen für char-Werte. In Groovy wird es nicht benötigt, verwenden Sie einfach String-Literale mit einem Zeichen; die Umwandlung nimmt Groovy bei Bedarf automatisch vor.

Operatoren

[Bearbeiten]

Die Operatoren lassen sich in Groovy größtenteils in der gleichen Weise nutzen wie in Java. Allerdings gibt es in Groovy einige zusätzliche Operatoren, und die von Java bekannten Operatoren können in vielen weiteren Zusammenhängen oder in anderer Weise verwendet werden. Beispielsweise dienen die eckigen Klammern in Java nur zur Indizierung von Arrays; in Groovy können damit auch Listen und Maps indiziert werden, und es können sogar Indexlisten und negative Indices angegeben werden.

Überladen durch spezifische Methoden

[Bearbeiten]

Der entscheidende semantische Unterschied besteht darin, dass Groovy die meisten Operatoren nicht direkt in die korrespondierenden Operationen im Bytecode übersetzt, sondern in Methodenaufrufe. So sind in Groovy beispielsweise die folgenden beiden Ausdrücke vollständig äquivalent:

x + 1
x.plus(1)

Dies hat zur Folge, dass Sie, wenn Sie versuchen, einen Operator mit Objekten zu verwenden, für die er nicht definiert ist, keinen Compilerfehler bekommen. Stattdessen gibt es zur Laufzeit eine Exception, die das Fehlen der betreffenden Methode meldet:

groovy:000> [:] * 3
ERROR groovy.lang.MissingMethodException:
No signature of method: java.util.LinkedHashMap.multiply() is applicable for argument types: (java.lang.Integer) values: [3]

Groovy setzt den Stern-Operator in einen Methodenaufruf von multiply(), und diese Methode ist für eine LinkedHashMap nicht definiert.

Dies ermöglicht Ihnen, jede beliebige Klasse mit Operatoren zu versehen; sie brauchen nur die entsprechenden Methoden zu implementieren.

Die Groovy-Standardbibliotheken machen extensiv Gebrauch von dieser Möglichkeit, indem sie vorhandenen Klassen und Interfaces die zu den Operatoren gehörenden Methoden als vordefinierte Methoden zuordnet. So ist beispielsweise die obige Addition mit Integer-Zahlen in Groovy-Programmen nur dadurch möglich, dass der Klasse Integer die vordefinierte Methode plus() zugeordnet ist.

Dies beschränkt sich in Groovy aber keinesfalls auf die Typen, für die die Operatoren in Java definiert sind. Alle folgenden Beispiele beruhen auf vordefinierten Methoden:

wert = meineMap['schlüssel']// Element einer Map lesen
meineMap['schlüssel'] = wert // Element einer Map schreiben
System.out << 'daten' // Schreiben in einen PrintStream
morgen = new Date() + 1 // Anzahl Tage zu einem Date-Objekt zählen

Im Operatoren überladen finden Sie weitere Erläuterungen und eine vollständige Übersicht aller überladbaren Operatoren und der zugehörigen Methoden.

Gleichheit und Identität

[Bearbeiten]

Die Implementierung von Operatoren über Methoden gilt auch für das doppelte Gleichheitszeichen (==). Groovy bildet es auf einen Aufruf der Methode equals() beim linken Operanden ab. Daraus folgt, dass der Operator == in Groovy prüft, ob zwei Objekte den gleichen Wert haben, während er in Java prüft, ob zwei Objekte identisch sind.

Wenn Sie möchten, können Sie übrigens auch jedes Objekt mit null vergleichen; ein Vergleich mit null ergibt immer false, einzig null==null ergibt true.

Warnung: Die Abbildung des Gleichheitsoperators auf eine Methode eines der Operanden führt dazu, dass die Ausdrücke x==y und y==x nicht unbedingt dasselbe Ergebnis liefern. Diese Gefahr ist ganz real, wenn man den Fall bedenkt, dass x und y Objekte zweier verschiedener Klassen sind und dass die Klasse von x möglicherweise den Typ von y kennt, dies umgekehrt aber nicht unbedingt zutrifft. Allerdings sind die Klassen der Groovy-Standardbibliothek nach festgelegten Konventionen programmiert, die sicherstellen, dass die Ergebnisse von Vergleichsoperationen in der Regel nicht von der Reihenfolge der Operanden abhängig sind.

Wenn Sie doch einmal die Identität zweier Objekte prüfen möchten, dann können Sie die Methode is() verwenden. Sie ist für jede Klasse definiert, akzeptiert jedes Argument und liefert tatsächlich nur dann true, wenn das übergebene Objekt identisch mit dem aktuellen Objekt ist.

Somit können sie also davon ausgehen, dass, obwohl auch Zahlen immer Objekte sind, der Ausdruck

123==123

immer true liefert, während dies bei

123.is(123)

nicht unbedingt der Fall ist, denn die beiden Integer-Objekte mit demselben Wert können durchaus verschieden sein.

Tipp: Sie wundern sich vielleicht über die Behauptung, dass die Methode is() für jede Klasse definiert ist. Ein Blick ins JavaDoc zu J2SE zeigt schließlich, dass keine einzige Klasse der Standard-APIs eine Methode dieses Namens hat. Trotzdem funktioniert so etwas wie:
new Object().is(125)

Tatsächlich ist die Methode is() genau wie println() eine Groovy-eigene Erweiterung der Java-Standardbibliothek, die für alle Instanzen aller Klassen verfügbar ist.

Programmlogik

[Bearbeiten]

Während sich die Groovy-Befehle für Schleifen und Verzweigungen syntaktisch nicht wesentlich unterscheiden, so können sie sich trotzdem unterschiedlich verhalten, da sich der Umgang mit der Wahrheit deutlich zwischen Groovy und Java unterscheidet.

Logische Ausdrücke und die „Groovy-Wahrheit“

[Bearbeiten]

Logische Prüfungen, wie sie in if, while, assert usw. vorgenommen werden, erwarten anders als bei Java nicht unbedingt einen Booleschen Wert:

int i = 1
String s = 
assert i
if (s) { println 'ausgeführt' }

In diesem Beispiel läuft die Assert-Anweisung erfolgreich durch und die Anweisung im if-Block wird nicht ausgeführt. Bei Java hätte Ihnen schon der Compiler zwei Fehler angezeigt, da die zu prüfenden Ausdrücke in assert und if einen Booleschen Wert ergeben müssen, was hier nicht der Fall ist.

Wenn Groovy die Wahrheit ermitteln will, prüft es in der folgenden Reihenfolge:

  1. Es ist ein Boolescher Wert und hat den Wert true.
  2. Es ist das Ergebnis eines erfolgreichen Pattern-Match.
  3. Es ist eine Collection oder eine Map und ist nicht leer.
  4. Es ist ein String und ist nicht leer.
  5. Es ist eine Zahl und ist ungleich 0.
  6. Es ist keines dieser Typen und ist nicht null.

Das Bedeutet beispielsweise, dass ein Objekt normalerweise als true gilt, sofern es nicht null ist. Sobald es aber – und sei es auf Umwegen, sodass Sie es ihm nicht ohne Weiteres ansehen – ein von Collection oder Map abgeleitetes Interface implementiert und der Aufruf von size() den Wert 0 ergibt, ist es false. Vorsicht ist also angesagt.

Warnung: Eine unangenehme Nebenwirkung dieses flexibleren Umgangs mit der Wahrheit besteht darin, dass eine Verwechselung von einfachem und doppeltem Gleichheitszeichen nicht mehr auffällt. Eine Abfrage wie
while (x = 1) { ... }

bei der eigentlich geprüft werden sollte ob x gleich 1 ist, würde in Java sofort zu einem Compiler-Fehler führen, da die Zuweisung eines numerischen Wertes kein Boolesches Ergebnis haben kann. Bei if- und assert-Anweisungen ist dies zwar kein Problem, da dort die logischen Bedingungen nicht aus einer Zusweisung bestehen dürfen, bei while findet eine solche Prüfung jedoch nicht statt. Die Folge ist: Obige Anweisung führt zu einer Endlosschleife, da die Zuweisung 1 ergibt, und das ist gleichwertig mit true.

for-Schleifen

[Bearbeiten]

Die von Java und seinen Vorgänger-Sprachen bekannte klassische for-Schleife in der Form, wie sie das folgende Beispiel zeigt, kennt Groovy nur in einer leicht vereinfachten Form, bei des nicht mehrere Laufvariablen geben kann. Groovy bietet aber eine "verbesserte" for-Schleife im Stil von Java 5.0, auf die wir weiter unten noch zu sprechen kommen, sowie verschiedene andere, sehr kompakte Möglichkeiten für die Implementierung von Zählschleifen.

// Groovy – als for-Schleife mit einem Wertebereich
for (i in (0..<10)) {println i }
// Groovy – als Closure-Methode eines Intervalls
(0..<10).each { println it }
// Groovy – als Closure-Methode eines numerischen Objekts
0.upto(9) { println it }

Die switch-Verzweigung

[Bearbeiten]

Die switch-Anweisung hat zwar dieselbe Form wie in Java – auch hier darf man das lästige break nach jedem Zweig nicht vergessen – bietet aber viel mehr Möglichkeiten. Grundsätzlich ist switch nicht auf ganzzahlige Werte beschränkt, was mangels primitiver Typen auch keinen Sinn ergäbe, und es kann auf alles Mögliche getestet werden. Sehen Sie sich dieses Beispiel an:

switch (meineVariable) {
  case 100: // Integer-Zahl
    println "Die Zahl 100"
    break
  case "ABC": // String
    println "Der String ABC"
    break
  case Long: // Klasse
    println "Ein Long-Wert"
    break
  case ['alpha','beta','gamma']: // Liste
    println "alpha, beta oder gamma"
    break
  case {it > -0.1 && it < 0.1}: // Closure
    println "Eine Zahl nahe Null"
    break
  case null: // null
    println "Ein leerer Wert"
    break
  default:
    println "Etwas ganz anderes"
}

In diesem Beispiel wird der switch-Wert gegen fünf völlig verschiedene Dinge geprüft. Es sind Objekte – was kein Wunder ist, das es ja in Groovy keine einzeln stehenden primitiven Werte gibt –, es können durchaus Variablen sein und nicht nur Konstante, und es sind sogar ein Klassenname und eine Closure dabei. Tatsächlich können Sie in einem Groovy-Switch neben Klassennamen gegen jeden beliebigen Wert prüfen, dabei wird normalerweise auf Gleichheit geprüft, in einigen Fällen führt der case-Ausdruck aber eine abweichende Prüfung durch:

  • Eine Collection oder ein Array prüft, ob das Objekt darin enthalten ist.
  • Ein Pattern prüft, ob ein toString() von dem Objekt mit dem Muster übereinstimmt.
  • Eine Closure prüft, ob ihr Aufruf mit dem Objekt als Argument das Ergebnis true (im Sinne der „Groovy-Wahrheit“) ergibt.
  • Eine Class prüft, ob das Objekt eine Instanz der Klasse oder von ihr abgeleitet ist oder das Interface implementiert.

Maßgeblich dafür ist, wie ein Wert die Methode isCase() implementiert, die standardmäßig nur equals() aufruft, für die oben aufgeführten Typen aber überschrieben ist. Sie können also durchaus eigene Typen schreiben, die sich in der Case-Verzweigung in spezieller Weise verhalten.

Asserts

[Bearbeiten]

Das assert-Statement hat eine etwas andere Syntax als in Java: Wenn Sie einen erläuternden Text hinzufügen wollen, trennen Sie ihn von dem zu prüfenden Ausdruck durch ein Komma ab und nicht wie in Java durch einen Doppelpunkt:

assert c=='Z', 'Der Wert von c sollte Z sein'

Wie in Java wird bei einem Fehlschlagen der Prüfung ein java.lang.AssertionError ausgelöst. Der Unterschied besteht darin, dass die Auswertung von Asserts in Groovy weder aus- noch eingeschaltet werden kann und immer aktiv ist. Der Grund dafür besteht in dem dynamischen Charakter von Groovy: Der Compiler kann in Groovy viel weniger Fehler finden als in Java, daher ist es ratsam, mit Hilfe zahlreicher Assert-Anweisungen dafür zu sorgen, dass Fehler zur Laufzeit schnell gefunden werden können, und diese Möglichkeit auch nicht zu deaktivieren.

Dynamische Methoden

[Bearbeiten]

Wenn Sie den Aufruf einer Objektmethode programmieren, z.B.

meinObjekt = new MeineKlasse()
meinObjekt.tuDies()

dann können Sie sich in Java darauf verlassen, dass eben diese Methode ihres Objekts aufgerufen wird, sei es, dass sie vom Objekt selbst implementiert oder von einer übergeordneten Klasse geerbt ist. Wenn dies nicht möglich ist, erhalten Sie bereits beim Kompilieren einen Fehler. In Groovy können daneben noch ganz andere Dinge passieren:

  • Das Objekt kann eine Property namens tuDies haben, der eine Closure zugewiesen worden ist. Dann wird diese aufgerufen.
  • Die Methode tuDies() könnte von der Groovy-Laufzeitbibliothek für die Klasse MeineKlasse oder eine übergeordnete Klasse oder ein von ihr implementiertes Interface vordefiniert sein. Dann wird die vordefinierte Methode aufgerufen.
  • Sie können eine sogenannte Kategorienklasse definiert und aktiviert haben, die eine Methode namens tuDies() für MeineKlasse oder eine übergeordnete Klasse oder ein von ihr implementiertes Interface enthält. Dann wird diese aufgerufen.
  • Die Klasse MeineKlasse kann die Methode invokeMethod() überschreiben und somit dafür sorgen, dass beim vermeintlichen Methodenaufruf etwas ganz anderes geschieht.
  • Sie können die Metaklasse zu MeineKlasse oder zu dem Objekt meinObjekt manipulieren und auf diese Weise ebenfalls dafür sorgen, dass etwas ganz anderes geschieht.

Sie werden diese verschiedenen Möglichkeiten der Beeinflussung des Objektverhaltens und die Möglichkeiten, die sich daraus ergeben, im Laufe der Lektüre dieses Buches im Einzelnen kennen lernen. An dieser Stelle soll der Hinweis genügen, dass in Groovy-Programmen Schein und Sein ziemlich weit auseinander liegen können.

Sonstige Abweichungen gegenüber Java

[Bearbeiten]

Es gibt noch einige weitere Unterschiede zwischen Groovy und Java, die von untergeordneter Bedeutung sind, aber hier der Vollständigkeit halber aufgeführt sein sollen.

Klassen und Quelldateien

[Bearbeiten]

Anders als in Java können in einer Groovy-Quelldatei mehrere öffentliche Klassen definiert sein, und – was zwangsläufig daraus folgt – die Namen der Klassen müssen nicht unbedingt mit den Dateinamen übereinstimmen. Beim Übersetzen mit groovyc entsteht für jede Klasse eine eigene .class-Datei, die den Namen der Klasse hat.

Innere Klassen

[Bearbeiten]

Sie können in Groovy keine Klassen innerhalb von anderen Klassen und damit auch keine anonymen Klassen definieren. Sie werden sie auch kaum vermissen, da der häufigste Anwendungsfall von inneren Klassen, nämlich als Event-Handler zu dienen, in Groovy viel besser durch Closures erfüllt wird. (Seit der Version 1.7 gibt es auch in Groovy innere Klassen, allerdings ist deren Implementierung nur sehr eingeschränkt.)

Klassennamen

[Bearbeiten]

Der Name einer Klasse ist in Groovy immer auch zugleich eine Referenz auf das entsprechende Class-Objekt. Wo Sie in Java beispielsweise String.class schreiben müssen, reicht in Groovy String.

Statisches this

[Bearbeiten]

In Groovy-Programmen können Sie das reservierte Wort this auch im statischen Kontext verwenden. In diesem Fall referenziert es nicht auf die aktuelle Instanz (die es ja nicht gibt) sondern auf die Instanz des aktuellen Klassenobjekts. Das assert in der folgenden Main-Methode ist also erfolgreich.

class EineKlasse {
  static void main(args) {
    assert this == EineKlasse
  }
}

Array-Initialisierung

[Bearbeiten]

Groovy kennt keine Array-Initialisierung, wie sie in Java mit Listen in geschweiften Klassen möglich ist:

// Java
int[] i = new int[] {1,2,3,4,5};

Stattdessen können Sie in Groovy Arrays mit Hilfe von Listen-Literalen passenden Typs initialisieren. Wenn die zu initialisierende Variable explizit typisiert ist, wird der Typ der Liste automatisch angepasst, andernfalls muss die Typanpassung mit dem Schlüsselwort as erzwungen werden.

int[] i = [1,2,3,4,5] // mit impliziter Typanpassung
j = [1,2,3,4,5] as int[] // mit expliziter Typanpassung

Generische Datentypen

[Bearbeiten]

Die mit Java 5 eingeführten generischen Typen gibt es in Groovy auch. Sie können in Java programmierte parametrisierte Typen instanziieren und auch neue parametrisierte Typen schreiben. Allerdings werden die Typparameter bei der Verwendung der Objekte nicht geprüft, d.h. Sie können beispielsweise einer mit new ArrayList<String>() instanziierten String-Liste ohne Weiteres irgendein anderes Objekt zuweisen.

Annotationen

[Bearbeiten]

Annotationen können in Groovy angewendet werden und werden auch korrekt verarbeitet. Die in Java vordefinierten Annotationen wie @Override und @Deprecated gibt es in Groovy jedoch nicht. Die Syntax unterscheidet sich geringfügig: Wenn ein Array als Parameter angegeben wird, muss es in eckige statt geschweifte Klammern gesetzt werden.

Seit der Version 1.6 kann man in Groovy auch Annotationen definieren, und inzwischen gibt es auch verschiedene vordefinierte Annotationen, mit denen man beispielsweise Singleton-Objekte erstellen kann.

For-Schleifen

[Bearbeiten]

Die "verbesserte" for-Schleife gibt es in Groovy schon länger als in Java, allerdings in einer leicht veränderten Form mit dem Schlüsselwort an der Stelle, wo bei Java der Doppelpunkt steht. Kurz vor der Fertigstellung der Groovy-Version 1.0 hat man sich allerdings entschlossen, die Form mit Doppelpunkt auch in Groovy zu ermöglichen. Während in Java nur diese Formulierung möglich ist

for (String s : stringList) {...} // Java und Groovy

können Sie in Groovy auch so schreiben

for (String s in stringList) {...} // nur Groovy

Die zweite Form ist in Groovy insofern etwas passender, weil in hier ein allgemeiner Operator mit der Bedeutung "enthalten in" ist; außerdem brauchen Sie in dieser Variante die Laufvariable nicht zu deklarieren, for (s in stringList) würde es also auch tun.

Auto-Boxing

[Bearbeiten]

Das Auto-Boxing und Auto-Unboxing nimmt Groovy genau wie Java 5 automatisch vor, sobald eine Typumwandlung zwischen einem primitiven Datentyp (z.B. int) und dem korrespondierenden Wrapper-Typ (z.B. Integer) erforderlich ist.


Sie kennen nun die wesentlichen Dinge, die Groovy und Java hinsichtlich der Programmiersprache unterscheiden. Das sollte Sie schon jetzt in die Lage versetzen, mit Hilfe Ihrer vorhandenen Java-Kenntnisse die ersten Probleme auch schon mit Groovy – und etwas weniger Tipparbeit – lösen zu können. Sie haben damit aber auch das notwendige Verständnis für die folgenden Kapitel, in denen wir noch etwas tiefer in die Möglichkeiten einsteigen, die Groovy Ihnen beim Programmieren bietet.


  1. Falls Sie sich jetzt wundern, dass wir auch ein Datum und eine Integer-Zahl addieren können (die Operation addiert die entsprechende Anzahl von Tagen hinzu), müssen wir Sie auf Operatoren Überladen und Vordefinierte Methoden vertrösten, wo das Überladen von Operatoren und die Erweiterungen der JDK-Bibliotheken erklärt werden.