Groovy: Operatoren

Aus Wikibooks

Operatoren[Bearbeiten]

Groovy kann, was Java nicht kann, was von vielen als eine der wesentlichen Schwächen und von anderen wiederum als wesentlicher Vorteil von Java angesehen wird: Operatoren überladen. Und das noch auf eine geradezu frappierend einfache Weise. Wie vieles in Groovy lädt auch dies dazu ein Unfug anzustellen (und das ist auch genau der Grund, warum die Java-Erfinder es sich verkniffen haben). Aber Groovy ist für Erwachsene, und die können schließlich selbst entscheiden, was gut für sie ist.

Fast alle von Java bekannten arithmetischen und bitweisen Operatoren sowie noch einige weitere stehen zur Disposition. Jedem von ihnen entspricht eine bestimmte Methode; bei binären (zweistelligen) Operatoren hat sie ein Argument, bei unären (einstelligen) jedoch nicht. Wenn ein Objekt eine dieser Methoden ausführen kann, dann ist auch der korrespondierende Operator für die Instanzen dieser Klasse definiert. Wenn also beispielsweise an einem Objekt x der Methodenaufruf

 x.plus(y)

möglich ist, dann können Sie in Groovy stattdessen auch schreiben:

 x + y

Nun ist es aber so, dass die numerischen Wrapper-Klassen von Java ebenso wie die Klasse java.lang.String über keine Methode plus() verfügen, trotzdem soll der +-Operator mit ihnen verwendbar sein. Dieses Problem löst Groovy mit Hilfe seiner vordefinierten Methoden; sie stellen sicher, dass etwa bei einem String die Methode plus() aufgerufen werden kann, obwohl die Klasse String() diese nicht implementiert:

 groovy> println "abc".plus("de")

In Vorlage:Groovy: Link gehen wir genauer auf diesen Mechanismus ein.

Die folgende Tabelle zeigt die Operatoren, denen Methoden zugeordnet sind, sowie jeweils einen Ausdruck mit diesem Operator und die Form, wie dieser in einen Methodenaufruf umgesetzt wird.[1]

Operatoren und Operatormethoden
Operator Bezeichnung Ausdruck Methodenaufruf
Arithmetische Operatoren
- Negation -x x.unaryMinus()
+ Addition x + y x.plus(y)
- Subtraktion x - y x.minus(y)
* Multiplikation x * y x.multiply(y)
/ Division x / y x.divide(y)
% Modulo x % y x.mod(y)
++ Inkrement x++
++x
x = x.next()
-- Dekrement x--
--x
x = x.previous()
Binäre Operatoren
Binäres Oder y x.or(y)
& Binäres Und x & y x.and(y)
~ Komplement ~x x.bitwiseNegate()
>> Rechtsverschiebung x >> y x.rightShift(y)
>>> Vorzeichenlose Rechtsverschiebung x >>> y x.rightShiftUnsigned(y)
<< Linksverschiebung x << y x.leftShift(y)
Vergleichsoperatoren
== Gleichheit x == y x.equals(y) oder x.compareTo(y)
!= Ungleichheit x != y ! x.equals(y)
<=> Vergleich x <=> y x.compareTo(y)
> Größer x > y x.compareTo(y) > 0
< Kleiner x < y x.compareTo(y) < 0
>= Größer oder gleich x >= y x.compareTo(y) >= 0
<= Kleiner oder gleich x <= y x.compareTo(y) <= 0
Sonstige Operatoren
[] Index x[y] x.getAt(y)
[]= Index-Zuweisung x[y] = z x.putAt(y,z)
(Case) Switch-Case switch (x) { case y: } if(y.isCase(x)) {}
in Enthalten x in y x.isCase(y)
as Typanpassung x as y x.asType(y)

Die Tabelle sollte weitestgehend selbsterklärend sein; bezüglich der sonstigen Operatoren wird an anderer Stelle ausführlich auf ihre Anwendung eingegangen.

Projekt: Rechnen mit physikalischen Einheiten[Bearbeiten]

Wer schon mal damit zu tun gehabt hat, weiß, dass das Rechnen mit dimensionierten Größen eine höchst knifflige und fehlerträchtige Angelegenheit sein kann. Weit verbreitete Beispiele sind Währungsbeträge und physikalische Größen. Es gibt Java Lösungen dafür: man definiert Klassen für die Beträge, die ihre jeweilige Dimension kennen und die sich gegeneinander umrechnen lassen. Das Problem ist nur, dass auf die Objekte dieser Klassen keine Operatoren angewendet werden können, und mathematische Berechnungen dadurch schnell sehr unübersichtlich werden.

In Groovy ist das einfacher. Wir wollen uns ein paar Typen definieren, die mit den verschiedenen Einheiten für Energie rechnen können: Joule (J), Kilowattstunde (kWh), Elektronenvolt (eV), Kilopondmeter (kpm), Kilokalorie (kcal). Als Basisgröße verwenden wir Kilowattstunden; alle Umrechnungen erfolgen in der Weise, dass ein Wert zunächst in kWh und dann in die Zielgröße umgerechnet wird (die daraus resultierende Ungenauigkeit nehmen wir billigend in Kauf). Die anderen Größen lassen sich aufgrund der folgenden Formeln ableiten:

  • 1 eV = 4,45 * 10-26 kWh
  • 1 J = 2,778 * 10-7 kWh
  • 1 kpm = 2,724 * 10-6 kWh
  • 1 kcal = 1,163 * 10-3 kWh

Mit Objekten dieser Typen soll man normal in Formeln rechnen können, und bei einer Zuweisung zwischen Objekten verschiedener Typen soll automatisch eine Umrechnung erfolgen, z.B. soll Folgendes möglich sein:

 def v1 = new KWh(1) // 1 kWh
 Joule v2 = [2] // 2 Joule über List-Konstruktor
 def v3 = (v1*2) as Joule // 2 * 1 kWh, in Joule umgerechnet
 def v4 = (new Kcal(1000) + new KWh(1))*3 // 1000 Kcal + 1 KwH, multipliziert mit 3

Wir implementieren dazu eine abstrakte Klasse WertMitEinheit für dimensionierte Werte. Die einzelnen Ausprägungen (hier nur für kWh, Joule und Kcal realisiert), brauchen nur noch im Konstruktor das Symbol für die Einheit und den Umrechnungsfaktor angeben.

 protected abstract class WertMitEinheit {

   final String symbol // Symbol für die Dimension, z.B. "kWh"
   final Number faktor // Umrechnungsfaktor vom normalisierten Wert
   final Number wert // Anzahl der Einheiten
   
   // Konstruktor für Symbol, Umrechnungsfaktor und Wert
   // Dabei kann Wert ein Skalar oder ein anderer WertMitEinheit sein
   protected WertMitEinheit(String symbol, Number faktor, arg) {
     this.symbol = symbol
     this.faktor = faktor
     switch (arg) {
     case Number:
       // Wenn es ein Skalarwert ist, als Wert übernehmen
       wert = arg
       break;
     case WertMitEinheit:
       // Wenn es ein anderer WertMitEinheit ist, umrechnen
       wert = arg.normalwert * faktor
       break
     default:
       // Falscher Argumenttyp
       throw new IllegalArgumentException()
     }
   }
   
   // Liefert der normalisierten Wert
   public getNormalwert() { wert / faktor }
   
   // Setzt den Wert anhand des Normalwerts
   public void setNormalwert (Number arg) { wert = arg * faktor }
   
   // Wandelt den Wert in einen anderen WertMitEinheit um.
   // Wenn es derselbe Typ ist, können wir this zurückgeben,
   // da WertMitEinheit immutabel ist.
   final asType(Class type) {
     type==this.class ? this : neuerWert(type,this)
   }
   
   // Addiert anderen WertMitEinheit
   final plus(WertMitEinheit e) {
     neuerWert (this.class, this.wert + e.normalwert * faktor )
   }
   
   // Subtrahiert anderen WertMitEinheit
   final minus(WertMitEinheit e) {
     neuerWert (this.class, this.wert - e.normalwert * faktor )
   }
   
   // Multipliziert mit Skalar
   final multiply (Number n) {
     neuerWert(this.class, this.wert*n)
   }
   
   // Dividiert durch Skalar
   final divide (Number n) {
     neuerWert(this.class, this.wert/n)
   }
   
   // Erzeugt per Reflection eine neue Instanz
   // des angegebenen Typs mit Konstruktor-Argument
   private WertMitEinheit neuerWert(Class type, wert) {
     type.getConstructor(Object).newInstance(wert)
   }
   
   // Anzeige Wert und Symbol
   String toString() { "$wert $symbol" }
 }

 // Kilowattstunde soll Normalwert sein
 class KWh extends WertMitEinheit {
   public KWh (wert) { super('kWh',1,wert) }
 }

 // 1 Kilowattstunde sind 3,6E6 Joule
 class Joule extends WertMitEinheit {
   public Joule (wert) { super('J',3.6e6,wert) }
 }

 // 1 Kilowattstunde sind 860,1 Kilokalorien
 class Kcal extends WertMitEinheit {
   public Kcal (wert) { super('kcal',860.1,wert) }
 }

Der Konstruktor von WertMitEinheit nimmt neben dem Absolutwert auch einen beliebigen anderen WertMitEinheit an und wandelt dessen Wert anhand der beiden Umrechnungsfaktoren um die virtuelle Property normalwert hilft bei der Umrechnung.

Beachten Sie aber insbesondere die Operatormethoden plus(), minus(), multiply() und divide(), mit denen die Grundrechenarten implementiert sind, sowie asType(), die die Umwandlung in einen anderen WertMitEinheit-Subtyp ermöglicht.

Das Einfachste ist jetzt, diese vier Groovy-Klassen in einer Datei zu speichern und mit groovyc zu übersetzen, wobei vier getrennte .class-Dateien entstehen. Wenn wir sie nicht übersetzen, sondern als Skript verwenden wollen, müssen wir sie als vier getrennte .groovy-Dateien unter ihrem jeweiligen Klassennamen speichern, damit die Klassen zur Laufzeit gefunden werden können. Die folgenden interaktiven Experimente funktionieren in beiden Fällen.

 groovy> def v1 = new KWh(1)
 groovy> println v1
 1 kWh
 groovy> Joule v2 = [2]
 groovy> println v2
 2 J
 groovy> def v3 = (v1*2) as Joule
 groovy> println v3
 7.2E+6 J
 groovy> Joule v4 = [v1*2]
 groovy> println v4
 7.2E+6 J
 groovy> def v5 = (new Kcal(1000) + new KWh(1))*3
 groovy> println v5 
 5580.3 kcal

Die Ergebnisse stimmen, prüfen Sie ruhig nach. Sie sehen, dass man mit diesen dimensionierten Werten schon ganz gut rechnen kann. Die WertMitEinheit-Klasse ist ganz abstrakt geraten, sie kann auch für diverse andere vergleichbare Umrechnungsprobleme verwendet werden. Zwei Dinge stören freilich noch: die umständlichen Konstruktor-Aufrufe in den Formeln und die Tatsache, dass WertMitEinheit nur mit Größen arbeiten kann, die multiplikativ abbildbar sind. Damit scheitern wir schon bei einer einfachen Umrechnung von Fahrenheit nach Celsius. Aber auch diese beiden Schwächen lassen sich noch mit den Mitteln beseitigen, die wir aber in Dynamisches Programmieren behandeln.


  1. Manche dieser Operatoren sind für bestimmte Operandentypen fest im Framework implementiert; dies ist z.B. bei ~ der Fall.