Groovy: Zahlen und Arithmetik

Während sich der normale Umgang mit numerischen Werten und Rechenoperationen nicht wesentlich von Java unterscheidet, gibt es doch einige gravierende Unterschiede, die sich daraus ergeben, dass Groovy grundsätzlich nur mit Objekten rechnet und nicht mit primitiven Typen und dass standardmäßig BigInteger und BigDecimal anstelle von Float und Double verwendet werden. Außerdem stehen durch Groovys Laufzeitbibliothek eine Reihe zusätzlicher oder vereinfachter Funktionalitäten zur Verfügung.

Numerische Datentypen

Bearbeiten

Folgende Tabelle zeigt eine Übersicht der vordefinierten numerischen Datentypen (alle sind vorzeichenbehaftet).

Klasse Suffix Bedeutung Beispiel-Literal
java.lang.Byte 8-Bit-Ganzzahl (Byte)100
java.lang.Short 16-Bit-Ganzzahl (Short)100
java.lang.Integer 32-Bit-Ganzzahl 100
java.lang.Long L, l (kleines L) 64-Bit-Ganzzahl 100L
10000000000
java.lang.Float F, f 32-Bit-Fließkommazahl 100F
100.0f
1e2f
java.lang.Double D, d 64-Bit-Fließkommazahl 100D
100.0d
1e2d
java.math.BigInteger G, g Ganzzahl beliebiger Länge 100G
10000000000000000000
java.math.BigDecimal G, g Dezimalzahl beliebiger Länge 100.0
100.0G
1.0000e2

Der sicher bedeutendste Unterschied zu Java besteht sicher darin, dass Groovy sehr große und gebrochene Zahlen standardmäßig als BigInteger- und BigDecimal-Objekte speichert und auch die Anwendung der üblichen arithmetischen Operationen mit solchen Werten ermöglicht. Das hat den Vorteil, dass es keine Genauigkeitsprobleme gibt, die bei Fließkommazahlen bisweilen zu überraschenden Ergebnissen führen. So ergibt die Addition der Zahlen 1.7 und 1.9 in Java, mit double-Zahlen berechnet, das Ergebnis 3.5999999999999996. In Groovy, mit BigDecimal berechnet, erhalten wir den eher erwartungskonformen Wert 3.6.

Auch Groovy-Fließkommazahlen haben eine Rundung entsprechend der Java-Klasse BigDecimal, z.B. 2/3 == 0.6666666667, wobei die Genauigkeit über scale abgefragt werden kann, z.B. (2/3).scale == 10.

Es ist auch weiterhin möglich, Fließkommazahlen zu benutzen, nur müssen bei den Literalen die entsprechenden Suffixe (F, f, D, d) angebracht werden oder die Variablen entsprechend typisiert werden. Beide folgenden Variablen werden mit einem Double-Wert initialisiert.

def x = 100d
Double y = 100

Berechnungen mit BigInteger und BigDecimal gelten als relativ langsam, daher kann es sinnvoll sein, in rechenintensiven Programmen, bei denen der Genauigkeitsverlust nicht wesentlich ist oder durch geeignete Maßnahmen abgefangen wird, lieber bei Fließkommazahlen zu bleiben.

Typanpassungen bei arithmetischen Operationen

Bearbeiten

Wenn mehrere numerische Werte in Rechenoperationen miteinander verknüpft werden, stellt sich die Frage, mit welcher Genauigkeit die jeweilige Operation ausgeführt wird und von welchem Typ das Ergebnis ist. Java hat die einfache Regel, dass bei Operationen zwischen verschiedenen Typen die Berechnung in dem genaueren Typ von beiden Typ ausgeführt wird und das Ergebnis auch diesen Typ hat ‒ mit der einzigen Ausnahme, dass ganzzahlige Operationen mindestens in Integer-Genauigkeit, also mit mindestens 32 Bit ‒ ausgeführt wird. Das führt zu kurios erscheinenden Situationen; z.B. ist hat das Ergebnis der Berechnung 1/3 den Wert 0, da die Division zwischen zwei Integer-Zahlen eine ganzzahlige Division ist.

Groovy verhält sich auch beim Dividieren erwartungskonformer; das es das Ergebnis einer Division zwischen zwei ganzen Zahlen in einen BigDecimal-Wert speichert. Wenn Sie eine ganzzahlige Division wie in Java benötigen, müssen Sie eine Typumwandlung vornehmen (z.B. 1/3 as Integer) Einige andere Probleme, die es bei arithmetischen Operationen in Java gibt, bleiben uns aber auch in Groovy erhalten. Folgende Tabelle zeigt die Regeln für die Ermittlung des Ergebnistyps bei den Grundrechenarten und der Potenzierung.

Operation Ergebnistyp
/ (Division) Double, wenn mindestens einer der Operanden Float oder Double ist.

BigDecimal in allen übrigen Fällen.

+ (Addition)
- (Subtraktion)
* (Multiplikation)
Double, wenn mindestens einer der Operanden Float oder Double ist.

BigDecimal, wenn mindestens einer der Operanden BigDecimal ist.
BigInteger, wenn mindestens einer der Operanden BigInteger ist.
Long, wenn mindestens einer der Operanden Long ist.
Integer in allen übrigen Fällen.

** (Potenz) Integer
Long
Double in Abhängigkeit von der Größe und Genauigkeit des Ergebnisses.

Die Tatsache, dass der Ergebnistyp bei Additionen, Subtraktionen und Multiplikationen allein aufgrund der beteiligten Operandentypen bestimmt wird, bringt es mit sich, dass man die gleiche Vorsicht vor Überlaufen walten lassen muss wie bei Java auch. Auf die Möglichkeit, den Ergebnistyp anhand der Größe und der Genauigkeit des Ergebnisses zu wählen, wie es etwa bei Ruby der Fall ist, hat man aus Performance-Gründen verzichtet.

Das ist insbesondere dann tückisch, wenn es in Zwischenwerten einen unerwarteten Überlauf geben kann:

groovy> println 123456789/100*100
groovy> println 123456789*100/100
123456789.00
-5392229.88

Beide Berechnungen sollten den Wert 123456789 liefern. Leider kommt es aber bei der zweiten Formel zu einem Überlauf nach der Multiplikation, der zu einem völlig unsinnigen Ergebnis führt. Würde Groovy das Zwischenergebnis, das in einer Integer nicht mehr unterzubringen ist, in einer Long- oder einer BigInteger-Variablen speichern, wäre das Ergebnis korrekt. Immerhin ist das Verhalten aber besser als in Java, wo bei der ersten Berechnung aufgrund der Ganzzahl-Division zwei Stellen verloren gehen würden und das Ergebnis 123456700 wäre.

Hier müssen Sie also weiterhin selbst darauf achten, ob die von Ihnen gewählten Datentypen für die zu erwartenden Werte auch ausreichen im Zweifelsfall gleich mit BigInteger oder BigDecimal rechnen, was Groovy Ihnen ja sehr leicht macht.

Vordefinierte numerische Methoden

Bearbeiten

Das GDK enthält eine Reihe von vordefinierten Methoden für den Typ Number bzw. Spezialisierungen für einzelne numerische Typen. Viele von ihnen dienen dazu, die numerischen Operatoren zu implementieren. Von den übrigen vordefinierten Methoden wollen wir einige wichtigere hier kurz erläutern.

Number abs()
Bildet den Absolutwert und liefert das Ergebnis als neue Instanz. Wird von den Klassen BigInteger und BigDecimal selbst implementiert.
void downto (Number n, Closure c)
Ruft die Closure einmal mit dem aktuellen Objekt und danach mit jeweils um 1 verminderten Werten, bis die als Argument angegebene Zahl erreicht ist. Der letzte übergebene Wert ist also grlößer oder gleich n. Der Wert von n muss kleiner oder gleich dem aktuellen Objekt sein.
Beispiel: 1.1.downto(-3) { println it } gibt nacheindander die Werte 1.1, 0.1, -0.9, ‑1.9 und -2.9 aus.
Number intdiv (Number n)
Führt eine ganzzahlige Division des aktuellen Objekts durch das angegebene Argument durch und liefert das Ergebnis als neue Instanz. Beide Werte müssen von einem ganzzahligen Typ (also weder Float noch Double noch BigDecimal) sein.
Beispiel: 4.intdiv(3) hat das Ergebnis 1.
Number multiply (Number n)
Implementiert den Operator *. Multipliziert das aktuelle Objekt mit dem Argument und liefert das Ergebnis als neues Objekt zurück.
void step (Number bis, Number schrittweite, Closure c)
Durchläuft in einer Schleife die Werte vom aktuellen Objekt bis vor dem ersten Argument mit der Schritteweite des zweiten Arguments. Bei jedem Schritt wird die Closure aufgerufen.
Beispiel: 10.step(-10,-5) {println it} gibt die Werte 10, 5, 0 und -5 aus.
void times (Closure c)
Durchläuft in einer Schleife die Werte von 0 bis zum aktuellen Objekt mit der Schrittweite 1 und ruft bei jedem Schritt die Closure mit dem jeweiligen Wert auf.
Beispiel: 5.times { println it } gibt die Werte 0, 1, 2, 3 und 4 aus.
BigDecimal toBigDecimal ()
BigInteger toBigInteger ()
Double toDouble ()
Float toFloat ()
Integer toInteger ()
Long toLong ()
Diese Methoden wandeln das aktuelle Objekt in den durch den Methodennamen bezeichneten Typ um. Überzählige Stellen hinter dem Komma werden ohne Rundung abgeschnitten. Auf den möglichen arithmetischen Überlauf beim Wandeln in kleinere Typen müssen Sie selbst achten.
void upto (Number n, Closure c)
Ruft die Closure erst mit dem aktuellen Objekt und danach mit jeweils um 1 erhöhten Werten auf, bis die als Argument angegebene Zahl erreicht ist. Der letzte übergebene Wert ist also kleiner oder gleich n. Der Wert von n muss größer oder gleich dem aktuellen Objekt sein.
Beispiel: 0.1.upto(3) { println it } gibt nacheindander die Werte 0.1, 1.1 und 2.1 aus.
Number xor (Number n)
Führt ein bitweises exklusives Oder zwischen dem aktuelle Objekt und dem Argument durch und liefert das Ergebnis als neue Instanz.