Numbers & Numeric Literals
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0 (6.0.0, released 2026-09-22) and was checked on 2026-10-14. Every
example was run with Groovy 6.0.0 on JDK 26. Groovy numbers are the ordinary Java number types, but the language picks
different defaults from Java in three places: a decimal literal such as 3.14 is a BigDecimal, the / operator
does not truncate integers, and an operation with a BigInteger operand is computed exactly, while int and long
arithmetic still wraps as in Java.
The greenhouse running scenario uses numbers for temperatures, watering volumes and moisture levels.
How these rules combine with the other conversions (assigning a number to a byte, coercion with as, closures to
types) is covered on Type Coercion and Promotion.
The operators themselves (+, -, %, bit shifts, the assignment forms) are described on
Operators: Arithmetic, Relational
and Logical.
Integral literals
The integral types are byte, char, short, int, long and java.math.BigInteger. When the type is def (or the
literal is used as an expression), Groovy chooses the smallest of Integer, Long and BigInteger that holds the
value, for positive and negative numbers alike:
// types chosen for def: the smallest of Integer, Long, BigInteger that holds the value
assert 1 instanceof Integer
assert 2147483647 instanceof Integer
assert 2147483648 instanceof Long
assert 9223372036854775807 instanceof Long
assert 9223372036854775808 instanceof BigInteger
assert -2147483648 instanceof Integer
assert -2147483649 instanceof Long
If you declare a primitive type, the literal must fit it. byte b = 1, short s = 3 and long l = 5 are all
accepted and the literal is narrowed at compile time. BigInteger bi = 6 stores an exact value of unlimited size.
Source: Groovy documentation: Numbers
Binary, octal and hexadecimal
A prefix selects the base:
| Base | Prefix | Example | Value |
|---|---|---|---|
Binary |
|
|
175 |
Octal |
|
|
493 |
Hexadecimal |
|
|
16746496 |
Decimal |
none |
|
16746496 |
// bases
int litres = 0b10101111 // binary -> 175
int mode = 0755 // octal -> 493
int hexColor = 0xFF8800 // hex -> 16746496
assert litres == 175 && mode == 493 && hexColor == 16_746_496
assert -0x77 == -119
A minus sign in front of a literal is the unary operator applied to the literal, so -0x77 is -119.
Two details differ from Java. A hexadecimal literal that does not fit an int is still positive and becomes a long:
0xFFFFFFFF is 4294967295 of type Long, not -1 as in Java. And a number with a leading zero is octal: 08 and 09
are compile errors because 8 and 9 are not octal digits, which the 6.0 grammar lists as a lexer diagnostic:
$ groovyc --error-format short Oct.groovy
Oct.groovy:1:9: error: Invalid octal number
Decimal numbers (those with a fractional part or exponent) cannot be written in binary, octal or hexadecimal.
Decimal literals
The decimal types are float, double and java.math.BigDecimal. The default for a literal such as 1.234, 0.5 or
2.5e3 is BigDecimal: the value is stored exactly as written, so 0.1 + 0.2 == 0.3. float and double need an
explicit type, a coercion or a suffix. A BigDecimal literal is nevertheless accepted wherever a method or closure
expects a float or double parameter.
float f = 1.234
double d = 2.345
BigDecimal bd = 3.456 // the default
Exponents use e or E, an optional sign and an integral exponent. The value remains a BigDecimal:
assert 1e3 == 1_000.0 && 4E-2 == 0.04
assert 1e3.class == BigDecimal
Decimal literals without a leading zero (Groovy 4.0)
Before 4.0 a fraction had to begin with a digit (0.5). Since Groovy 4.0 (GROOVY-9649) the leading zero may be left
out. These are all BigDecimal unless a suffix says otherwise:
// Groovy 4.0: decimal literals without a leading zero
def half = .5
double third = .333d
def fractions = [.1, .2, .3]
assert half == 0.5 && third == 0.333d && fractions.every { it instanceof BigDecimal }
The form is convenient in list and range literals.
Underscores in literals
Underscores group the digits of long literals, as in Java 7. They may appear between digits in every base (and in decimals), but not at the end of the number, and not directly next to the prefix. They were added in Groovy 2.0 along with binary literals.
// underscores for readability (not at the end, not next to the radix prefix letter)
long seeds = 1_250_000L
long mask = 0b1101_0010_0110_1001
double dollars = 12_345_132.12
assert seeds == 1250000 && mask == 53865 && dollars == 12345132.12d
A literal ending in _ is a lexer diagnostic:
$ groovyc --error-format short Under.groovy
Under.groovy:1:10: error: Number ending with underscores is invalid
Type suffixes
A suffix forces the type of any literal, including binary, octal and hexadecimal ones. Upper case and lower case are
both accepted; lower-case i and upper-case L are the more readable choices.
| Type | Suffix |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
The same letter G yields a BigInteger after an integral literal and a BigDecimal after a decimal one. The 6.0
grammar notes that G is not a separate token: the choice is made when the syntax tree is built.
// suffixes force the type, upper or lower case
assert 42i.class == Integer
assert 123L.class == Long
assert 456g.class == BigInteger
assert 123.45.class == BigDecimal // default for decimals
assert 123.45g.class == BigDecimal
assert 1.5d.class == Double
assert 1.5f.class == Float
assert 0xFFi.class == Integer && 0b1111L.class == Long && 034G.class == BigInteger
The full literal run (this and the other literal examples on the page, as one script) printed
literals ok: [Integer, Long, BigInteger, Double, Float, BigDecimal]
Math operations: result types
The result type of a binary arithmetic operation depends on the types of both operands. Division and power have their
own rules (below). For the others (+, -, *, % and the compound assignments):
-
byte,char,shortandintwith each other giveint. -
longwith any of those giveslong. -
BigIntegerwith any other integral type givesBigInteger. -
BigDecimalwithbyte,char,short,intorBigIntegergivesBigDecimal. -
float,doubleandBigDecimal(in any combination with each other) givedouble. Note that this includesBigDecimalmixed withfloatordouble, a common source of surprise. -
Two
BigDecimaloperands giveBigDecimal.
The flowchart below applies the rules in order; the first matching question decides the type.
(The flowchart’s first question also covers the mixed case: when a float or double meets a BigDecimal, the
answer is double. An operand typed as Number under static type checking infers Number for a division, because it
may hold a Float or Double at run time.)
// the type of a binary operation
assert (1 + 1).class == Integer
assert (1 + 1L).class == Long
assert (1L + 1g).class == BigInteger
assert (1 + 1.5).class == BigDecimal // int + BigDecimal
assert (1g + 1.5).class == BigDecimal // BigInteger + BigDecimal
assert (1.5 + 1.5f).class == Double // BigDecimal + float -> double
assert (1.5f + 1).class == Double // float + int -> double
assert (1.5 + 1.5d).class == Double
assert ((byte) 1 + (short) 2).class == Integer
assert (0.1 + 0.2) == 0.3 // BigDecimal: exact
assert (0.1d + 0.2d) != 0.3d // double: binary rounding
println "0.1d + 0.2d = ${0.1d + 0.2d}"
Output of the script’s println:
0.1d + 0.2d = 0.30000000000000004
Because float + BigDecimal is double, even 0.1f + 0.2f prints a double (0.30000000447034836). For money,
measurements that need exact decimals and anything compared with ==, keep the values as BigDecimal and avoid
float and double suffixes.
Division
The / operator produces a double if either operand is a float or double, and a BigDecimal otherwise: when
both operands are any combination of short, char, byte, int, long, BigInteger or BigDecimal. That is the
biggest difference from Java: 7 / 2 is 3.5, not 3.
For BigDecimal results Groovy calls divide() when the division is exact. When it is not, it uses a MathContext
whose precision is the larger of the two operands' precisions plus 10, and whose scale is at least 10. In practice
non-terminating results such as 1 / 3 get ten decimal digits.
For integer division, as in Java, use intdiv(). Groovy has no dedicated integer-division operator. % and mod()
give the remainder:
// division
assert (7 / 2) == 3.5 && (7 / 2).class == BigDecimal
assert (6 / 3) == 2 && (6 / 3).class == BigDecimal // exact, still BigDecimal
assert (1 / 3) == 0.3333333333g // 10 extra digits of scale
println "1/3 = ${1 / 3} (${(1 / 3).class.simpleName}); 1/3d = ${1 / 3d}"
assert (7.0d / 2).class == Double && (7f / 2).class == Double
assert 7.intdiv(2) == 3 && (-7).intdiv(2) == -3 // truncates toward zero, like Java
assert 7 % 3 == 1 && (-7 % 3) == -1 && (-7).mod(3) == 2
assert (7 / 2).toInteger() == 3 // truncates
1/3 = 0.3333333333 (BigDecimal); 1/3d = 0.3333333333333333
intdiv truncates toward zero, so (-7).intdiv(2) is -3; % takes the sign of the dividend (-7 % 3 is -1), while
mod() returns a non-negative remainder ((-7).mod(3) is 2). Division by zero is an ArithmeticException for
integers and BigDecimal (1 / 0 and 1.intdiv(0)), but yields Infinity or NaN for double: 1 / 0d is
Infinity and 0d / 0d is NaN.
The power operator
The power operator ** takes a base and an exponent. The result type is the narrowest type that can hold the result:
-
Decimal exponent:
Integerif the result fits, otherwiseLongif it fits, otherwiseDouble. -
Negative integral exponent:
Integer,LongorDoubleif the result fits that type (so10 ** -1is aDouble0.1, while0.5 ** -2is theInteger4). -
Zero or positive integral exponent:
-
a
BigDecimalbase givesBigDecimal; -
a
BigIntegerbase givesBigInteger; -
an
Integerbase givesIntegerif the result fits, otherwiseBigInteger; -
a
Longbase givesLongif the result fits, otherwiseBigInteger.
-
// power
assert 2 ** 3 instanceof Integer
assert 5L ** 2 instanceof Long
assert 100 ** 10 instanceof BigInteger
assert 0.5 ** -2 instanceof Integer
assert 10 ** -1 instanceof Double && 10 ** -1 == 0.1d
assert 1.2 ** 10 instanceof BigDecimal && 1.2 ** 10 == 6.1917364224g
assert 3.4f ** 5 instanceof Double
assert 7.8 ** 1.9 instanceof Double
assert -2 ** 2 == -4 // -(2 ** 2)
assert 2 ** 3 ** 2 == 64 // left associative: (2 ** 3) ** 2
println "-2 ** 2 = ${-2 ** 2}; 2 ** 3 ** 2 = ${2 ** 3 ** 2}; 2 ** 0.5 = ${2 ** 0.5}"
-2 ** 2 = -4; 2 ** 3 ** 2 = 64; 2 ** 0.5 = 1.4142135623730951
Two precedence details follow from the 6.0 grammar: a unary minus binds more loosely than ** (-2 ** 2 is
-(2 ** 2), as in Python), and ** is left-associative, unlike mathematics and unlike Python (2 ** 3 ** 2 is 64, not
512). Use parentheses when you mix them.
Overflow and BigInteger promotion
Groovy does not widen ordinary arithmetic: int + int is an int and wraps on overflow, exactly as in Java. The
promotion described above applies when an operand is already a wider type (a long, a BigInteger) and in the cases
where the language itself computes the result type (power, literals, division). Promotion into BigInteger therefore
happens in these places:
-
a literal too large for
Longis aBigInteger; -
**with a result that does not fitintorlong; -
any operation with a
BigIntegeroperand.
// overflow: int arithmetic wraps, BigInteger does not
int big = Integer.MAX_VALUE
assert big + 1 == Integer.MIN_VALUE // int + int stays int and wraps
assert (big + 1L) == 2147483648L // long arithmetic
assert (Long.MAX_VALUE + 1g) == 9223372036854775808g // BigInteger operand: promoted, exact
assert (Long.MAX_VALUE + 1) == Long.MIN_VALUE // long + int stays long: wraps
assert 100 ** 10 == 10g ** 20
println "int wrap: ${big + 1}; ** promotes: ${(2 ** 31).class.simpleName}, ${(2 ** 63).class.simpleName}"
int wrap: -2147483648; ** promotes: BigInteger, BigInteger
The pitfall to remember is a fold whose seed and elements are int. This factorial of 25 silently wraps, while
the same fold with a BigInteger seed is exact (both were run):
def exact = (1g..25g).inject(1g) { acc, n -> acc * n }
def wrapped = (1..25).inject(1) { acc, n -> acc * n }
println "exact: ${exact}"
println "wrapped: ${wrapped} (${wrapped.class.simpleName})"
exact: 15511210043330985984000000
wrapped: 2076180480 (Integer)
When an overflow must be detected, use the checked methods of the JDK: Math.addExact(Integer.MAX_VALUE, 1) throws
ArithmeticException: integer overflow.
Number methods in the GDK
The Groovy Development Kit adds methods to Number, Integer, BigDecimal, Double and the others. The ones used
daily:
| Method | Meaning |
|---|---|
|
calls the closure with |
|
counts up or down, both ends inclusive |
|
steps from |
|
absolute value, in the receiver’s own type |
|
|
|
truncation toward zero |
|
|
|
numeric narrowing, truncating |
// iteration
def rounds = []
3.times { rounds << "round $it" }
assert rounds == ['round 0', 'round 1', 'round 2']
def hours = []
6.upto(9) { hours << it }
9.downto(7) { hours << it }
assert hours == [6, 7, 8, 9, 9, 8, 7]
def watering = []
0.step(10, 4) { watering << it } // 0, 4, 8
10.step(0, -5) { watering << it } // 10, 5 (the limit is exclusive)
assert watering == [0, 4, 8, 10, 5]
def doses = []
0.step(1, 0.25) { doses << it }
assert doses == [0, 0.25, 0.50, 0.75] // 1 is excluded
println doses
// sign, rounding and truncation
assert (-4.5).abs() == 4.5 && (-3).abs() == 3
assert 21.456.round() == 21 && 21.456.round(1) == 21.5g && 21.456.round(2) == 21.46
assert 2.5.round() == 3 && (-2.5).round() == -3 // BigDecimal: HALF_UP, away from zero
assert Math.round(-2.5d) == -2 // double via Math.round: toward +infinity
assert 21.99.trunc() == 21 && 21.456.trunc(1) == 21.4
assert 21.5.toInteger() == 21 && 21.5.intValue() == 21
assert Math.floor(21.5d) == 21d && Math.ceil(21.5d) == 22d // no GDK floor/ceil: use Math
[0, 0.25, 0.50, 0.75]
Note the surprises these assertions encode. step excludes its limit (unlike upto and downto, which include both
ends). BigDecimal.round() rounds half away from zero, so (-2.5).round() is -3, while Math.round(-2.5d) rounds
toward positive infinity and gives -2. There is no floor() or ceil() on Number in the GDK; call Math.floor
and Math.ceil.
Number methods are plain Java methods plus extension methods, so everything from Math, BigDecimal
(setScale, compareTo) and BigInteger remains available.
Source: GDK: Number, GDK: Integer, GDK: BigDecimal
Conversion and comparison
Conversions use as, toInteger() and friends. == compares values across number types, so 1 == 1L and
1 == 1.0 are true. The one exception to value-like behaviour is BigDecimal.equals(), which also compares the scale:
// conversions
assert '42'.toInteger() == 42 && '3.14'.toBigDecimal() == 3.14 && '12'.isInteger()
assert !'12.5'.isInteger() && '12.5'.isNumber() && !'12.5x'.isNumber()
assert 65 as char == 'A' && 'A'.toCharacter().charValue() == (char) 65
assert (21.7 as int) == 21 && (21.7 as BigDecimal).class == BigDecimal
assert 5.toString().padLeft(3, '0') == '005'
assert Integer.toHexString(255) == 'ff' && Integer.toString(255, 16) == 'ff' && Integer.parseInt('ff', 16) == 255
assert Integer.toBinaryString(10) == '1010' && Integer.toString(10, 2) == '1010'
// comparison and equality across types
assert 1 == 1L && 1 == 1.0 && 1g == 1.0g
assert 1.0 == 1.00 && !(1.0).equals(1.00) // BigDecimal.equals compares scale
assert (1.0 <=> 1.00) == 0
assert 0.1f != 0.1d // float widened to double differs
println "1.0.equals(1.00) = ${(1.0).equals(1.00)}, 1.0 == 1.00 = ${1.0 == 1.00}"
1.0.equals(1.00) = false, 1.0 == 1.00 = true
Source: Groovy documentation: Coercion operator, Groovy documentation: Behaviour of ==, GDK: CharSequence
== in Groovy calls compareTo for Comparable values, which is why it ignores the scale, while an explicit
equals (and Set or Map keys) does not. (21.7 as int) truncates (21); round first when that is not intended.
200 as byte and (byte) 200 wrap to -56.
A greenhouse example
Watering volume, temperature averages and prices are the places where the BigDecimal default pays off:
def litresPerMinute = 2.5 // BigDecimal
def minutes = 12
def total = litresPerMinute * minutes // BigDecimal * int -> BigDecimal
assert total == 30.0 && total.class == BigDecimal
def readings = [21.5, 19.0, 23.25, 20.5] // temperatures as BigDecimal
def mean = readings.sum() / readings.size()
assert mean == 21.0625 // exact: 84.25 / 4
println "mean=${mean} (${mean.class.simpleName})"
def thirds = [1, 1, 1].collect { it / 3 }.sum()
println "3 * (1/3) = ${thirds} -- ${thirds == 1 ? 'exactly 1' : 'not exactly 1'}"
def asDouble = [1, 1, 1].collect { it / 3d }.sum()
println "3 * (1/3d) = ${asDouble}"
// money-like amounts: stay in BigDecimal, round only for display
def seedPrice = 3.20
def packets = 7
assert seedPrice * packets == 22.40
println "seeds: ${(seedPrice * packets).setScale(2)}"
mean=21.0625 (BigDecimal)
3 * (1/3) = 0.9999999999 -- not exactly 1
3 * (1/3d) = 1.0
seeds: 22.40
The third line is a reminder that BigDecimal is exact only for numbers with a finite decimal expansion: one third
cannot be represented, and ten decimal digits is the rounding. The double version happens to print 1.0. Pick
the type on purpose: BigDecimal for exact decimal arithmetic, double for scientific calculations where speed and
range matter, and an explicit MathContext or setScale when the number of digits matters.
Where older references are outdated
References written for Groovy 1.x (the first edition of Groovy in Action targets Groovy 1.0) explain the BigDecimal
default and the behaviour of the / operator in spirit, but they predate several features:
-
Binary literals and underscores in numbers came with Groovy 2.0.
-
Decimal literals without a leading zero (
.5) came with Groovy 4.0. -
The result-type tables for arithmetic and
**on this page come from the current documentation. Check an older text’s claims about promotion and about the power operator against them, and against a run on your Groovy version. -
Error messages for malformed literals (an invalid octal number, a trailing underscore) are the 6.0 wording.
References
-
Groovy Language Documentation: Grammar (numeric literals, operator precedence)
-
Groovy 2.0 release notes (binary literals, underscores)
-
Groovy 4.0 release notes (decimal literals without a leading zero)
-
Groovy in Action (Manning, first edition) (publisher page; described in the section bibliography)