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
BearbeitenFolgende 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
BearbeitenWenn 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. |
** (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
BearbeitenDas 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.