Die größten Groovy-Fallen
Es gibt ein paar typischer Fehler, die einem bei der Groovy-Programmierung immer wieder passieren und die auch in der Groovy-Mailliste immer wieder thematisiert werden und dort mitunter heftige Diskussionen auslösen.
Wir wollen uns einige dieser Fehler mitsamt ihren typischen Ursachen einmal ansehen und Hinweise geben, wie man sie am besten vermeiden kann.1)
Zeilenende begrenzt Anweisung
In der Regel kann man in Groovy-Programmen die Semikolons zur Begrenzung von Anweisungen einfach weglassen. Für Groovy sind Zeilenenden nicht einfach Leerraum wie bei Java, sondern sie beenden eine Anweisung, wenn diese am Zeilenende syntaktisch komplett ist.
println "Lieber Leser, Sie haben folgenden Betrag gewonnen: " + einsatz * quote
Wenn Sie dieses Skript ausführen, wird zwar der Text, aber nicht die Zahl ausgegeben. Der Grund dafür besteht darin, dass nach dem String ein Zeilenwechsel kommt, und da an dieser Stelle die Anweisung syntaktisch komplett ist, nimmt Groovy an, dass sie dann auch zuende ist. Fatal ist bei einerm Fall wie diesem, dass in der nächsten Zeile ein gültiger Ausdruck steht, der ja in Groovy wie in Java auch immer eine gültige Anweisung ist. Sie bewirkt zwar nichts, führt aber auch nicht zu einer Fehlermeldung.
Was lernen wir: lange ausdrücke am besten Klammern und / oder Operatoren immer an das Ende einer Zeile und nicht an den Anfang der nächsten setzen. Beide folgenden Formen funktionieren korrekt:
println ("Lieber Leser, Sie haben folgenden Betrag gewonnen: " + einsatz * quote) // oder: println "Lieber Leser, Sie haben folgenden Betrag gewonnen: " + einsatz * quote
Methodenargumente ohne Klammern
In einfachen Fällen können Sie bei Methodenaufrufen die Klammern um die Argumentliste weglassen (sofern es Argumente gibt, andernfalls dürfen die Klammern nicht fehlen). Aber was ist ein einfacher Fall. Nehmen Sie dieses Beispiel:
println a*(b+c)
Es funktioniert. Aber dieses nicht:
println (a+b)*c
Das zweite Beispiel liefert Ihnen eine saubere NullPointerException. Warum: Groovy betrachtet die öffnende Klammer hinter dem Namen als Klammer für die Argumentliste, gibt demzufolge den Wert von (a+b) aus und versucht dann, das Ergebnis mit c zu multiplizieren, was natürlich keinen Sinn gibt. In diesem Fall kriegen Sie es sofort zu spüren. Aber bei einem Methodenaufruf, der ein Ergebnis liefert, auf das der Multiplikationsoperator anwendbar ist (und das kann auch ein String sein), merken Sie unter Umständen gar nichts.
Fazit: Auf jeden Fall die Argumentklammern hinzufügen, wenn am Anfang der Argumentliste ein geklammerter Ausdruck steht.
Getter als Properties ansprechen
Man gewöhnt sich beim Arbeiten mit Groovy rasch daran, die Property-Notation anstelle des Aufrufs der Getter-Methoden zu verwenden. Sie schreiben also beispielsweise:
println meinObjekt.class.name
anstelle von
println meinObjekt.getClass().getName()
Die Falle besteht darin, dass Sie typischerweise nicht wissen, was für ein Objekt Sie haben, wenn Sie getClass() aufrufen – sonst müssten Sie ja nicht nach der Klasse fragen. Es kann aber sein, dass das Objekt von einem Typ ist, bei dem die Property-Abfrage verbogen worden ist. Typisches Beispiel dafür sind Maps: bei ihnen erhalten Sie, wenn Sie nach einer Property fragen, immer das Map-Element mit dem entsprechenden Namen. Wenn meinObjekt also etwa eine HashMap ist, ist der obige Aufruf äquivalent zu:
println meinObjekt.get('class').getName()
Und das wird in der Regel zu einem Fehler führen, auf jeden Fall aber nicht zu dem erwartete Ergebnis.
Folgerung: Ersetzen Sie den Aufruf von getClass() am besten nie durch eine Property-Abfrage.
Leider gibt es aber auch dann keine absolute Sicherheit. In einer dynamischen Sprache wie Groovy kann auch die Methode getClass() verbogen worden sein. Fügen Sie einmal folgende Zeile in das Skript ein:
meinObjekt.getClass().metaClass.class = "Unsinn"
Und schon funktioneren weder meinObjekt.class noch meinObjekt.getClass() mehr richtig. Da sind Sie tatsächlich machtlos. Es versteht sich zwar von selbst, dass man so etwas wie hier nicht in einer ernsthaften Anwendung tun sollte, aber man kann natürlich nie wissen…
Was Groovy für wahr hält
In Java sind logische Verzweigungen wie if, for, while, der trinäre Operator usw. nur auf Ausdrücke anwendbar, die vom Typ boolean sind. In Groovy ist das anders. Hier kann jeder beliebige Ausdruck eingesetzt werden. Als unwahr gelten neben false auch Zahlen mit dem Wert 0, leere Strings und Container, erfoglose Pattern-Matches sowie null; alles andere ist gleichbedeutend mit true.
Vorsicht ist auch hier angebracht, wenn auf die Existenz eines Objekts prüfen will und statt:
if (meinObjekt==null) { ... }
einfach, um sich etwas Schreibarbeit zu sparen, schreibt:
if (! meinObjekt) { ... }
Letzterer Ausdruck trifft auch zu, wenn meinObjekt den Wert ““, [] oder [:] hat, und das ist nicht in jedem Fall erwünscht.
Am sichersten Fahren Sie also, wenn Sie auch in Groovy immer echte boolesche Ausdrücke für logische Abfragen verwenden – zumindest wenn Sie nicht ganz sicher sind, welchen Typ der Ausdruck haben kann.
Der genenüber Java etwas vereinfachte Wahrheitsbegriff von Groovy bringt übrigens auch ein altes Problem aus alten C- und C++-Zeiten wieder zum Vorschein: das Verwechseln von Zuweisung (=) und Vergleich (==). Da Java in if und while usw. einen echten booleschan Wert verlangt, passiert es selten, dass man statt if(a==b) versehentlich if(a=b) schreibt, denn der Compiler beschwert sich, sobald a nicht zufällig vom Typ boolean ist.
Groovy macht diesen Fehler zwar für die if-Abfrage unmöglich, denn der Compiler lässt hier keine Wertzuweisungen zu. So etwas wie:
if (a=b) { ... }
lässt sich in Grooyy also überhaupt nicht kompilieren, egal welchen Typs die Variablen a und b sind. Anders sieht es aber bei while, for, dem ternären Operator und dem Groovy-eigenen „Elvis-Operator“ ?: aus. Alle folgenden Zeilen sind in Groovy gültig, unabhängig vom Typ der darin auftretenden Variablen:
while (a=b) { ... } for (a=0; a=b; ) { ... } println a=b ? "gleich" : "ungleich" println a=b ?: "ungleich"
Der Grund besteht natürlich darin, dass man vielleicht wirklich einmal die logische Prüfung gleich mit einer Wertzuweisung verbinden möchte. Und der Preis für diesen Luxus ist, dass Sie etwas mehr auf der Hut sein müssen.
Respektierung der Privatsphäre
Dieses leidige Thema ist eigentlich kein Problem der Sprache Groovy, sondern der momentanen Implementierung, wie sie auch noch in Groovy 1.5.x zu finden ist (und uns wohl noch eine Weile erhalten bleiben wird): Groovy respektiert die Privatsphäre von Objekten nicht. Es ist völlig gleichgültig, ob ein Member als public, protected oder private deklariert ist – in jedem Fall haben Sie aus jedem Groovy-Objekt vollen Zugriff, egal ob es abgeleitet ist, sich im selben Package befindet oder nicht.
Dabei ist es völlig gleichgültig, ob sich das geschützte Member-Element in einer Groovy- oder Java-Klasse befindet. Entscheidend ist, dass die aufrufende Methode Teil einer Groovy-Klasse ist und damit den betreffenden Member nicht direkt, sondern über die Groovy'sche Metaklassenlogik aufruft. Und für die scheint es zu aufwändig zu sein, zur Laufzeit jeweils die Sichtbarkeit aufgerufener Methoden und Felder zu prüfen.
Eines der Resultate ist, dass es in einem Groovy-Programm keine immutablen Objekte gibt. Das eröffnet Ihnen interessante Möglichkeiten, z.B.
"U".value="X" assert "U"=="X"
In diesem Fall ist es natürlich offensichtlich, dass man so etwas nicht machen darf. Aber denken Sie daran, dass man es einer Methode oder einem Feld nicht immer ohne Weiteres ansieht, ob sie für den öffentlchen Zugriff vorgesehen ist. Wissen Sie beispielsweise aus dem Kopf, ob dies hier erlaubt ist oder nicht?
a=[1,2,3]; a.size=2
Und so kann es zu den merkwürdigsten Effekten kommen, wenn Sie nicht selbst auf die nötige Diskretion achten.
Methoden überschreiben mit abweichendem Rückgabetyp
Eine der kleineren Groovy-Tücken wurde zwar inzwischen beseitigt, konnte aber in Groovy 1.0 und noch teilweise sogar noch in den 1.1-RC-Versionen zur Verwirrung führen. Bis dahin wurde nämlich beim vermeintlichen Überschreiben einer Methode mit gleicher Signatur, aber abweichendem Typ des Rückgabewertes eine neue Methode angelegt, die Methode wurde also in Wirklichkeit nur überladen. Da hat dazu geführt, dass bei folgender Deklaration:
def toString() { ... }
nicht etwa, wie erwartet, die von Object geerbte toString()-Methode überschrieben, sondern eine ganz andere Methode angelegt, da ihr Rückgabetyp (Object) nicht mit dem ursprünglichen Rückgabetyp (String) übereinstimmt.
Das ist jetzt vorbei – Groovy verhält sich jetzt wie Java 5.0 und akzeptiert nur das Überschreiben von Methoden, wenn der neue Rückgabetyp zuweisungskompatibel zu dem Rückgabetyp der überschriebenen Methode ist.
Und da toString() als void deklariert ist, muss eine überschreibende Methode ebenfalls als void deklariert sein.
void toString() { ... }
Und so etwas wie das obige def toString() führt jetzt, und so sollte es sein, zu einem Fehler.
