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.

Binary, octal and hexadecimal

A prefix selects the base:

Base Prefix Example Value

Binary

0b or 0B

0b10101111

175

Octal

0 followed by octal digits

0755

493

Hexadecimal

0x or 0X

0xFF8800

16746496

Decimal

none

16_746_496

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

BigInteger

G or g

Long

L or l

Integer

I or i

BigDecimal

G or g

Double

D or d

Float

F or f

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, short and int with each other give int.

  • long with any of those gives long.

  • BigInteger with any other integral type gives BigInteger.

  • BigDecimal with byte, char, short, int or BigInteger gives BigDecimal.

  • float, double and BigDecimal (in any combination with each other) give double. Note that this includes BigDecimal mixed with float or double, a common source of surprise.

  • Two BigDecimal operands give BigDecimal.

The flowchart below applies the rules in order; the first matching question decides the type.

flowchart TD A["a op b, with op one of + - * %"] --> B{"either operand float or double?"} B -- yes --> D["double"] B -- no --> C{"either operand BigDecimal?"} C -- yes --> E["BigDecimal"] C -- no --> F{"either operand BigInteger?"} F -- yes --> G["BigInteger"] F -- no --> H{"either operand long?"} H -- yes --> I["long"] H -- no --> J["int (byte, char, short, int)"] K["a / b"] --> L{"either operand float or double?"} L -- yes --> D L -- no --> M["BigDecimal (operands integral or BigDecimal)"] N["a ** b"] --> O["Integer, Long, BigInteger, BigDecimal or Double (see the power rules)"]

(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: Integer if the result fits, otherwise Long if it fits, otherwise Double.

  • Negative integral exponent: Integer, Long or Double if the result fits that type (so 10 ** -1 is a Double 0.1, while 0.5 ** -2 is the Integer 4).

  • Zero or positive integral exponent:

    • a BigDecimal base gives BigDecimal;

    • a BigInteger base gives BigInteger;

    • an Integer base gives Integer if the result fits, otherwise BigInteger;

    • a Long base gives Long if the result fits, otherwise BigInteger.

// 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 Long is a BigInteger;

  • ** with a result that does not fit int or long;

  • any operation with a BigInteger operand.

// 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

n.times { }

calls the closure with 0 …​ n-1

a.upto(b) { }, a.downto(b) { }

counts up or down, both ends inclusive

a.step(to, by) { }

steps from a up to but excluding to; by may be negative or a decimal

n.abs()

absolute value, in the receiver’s own type

x.round(), x.round(digits)

BigDecimal/Double rounding half up (away from zero for BigDecimal)

x.trunc(), x.trunc(digits)

truncation toward zero

s.toInteger(), toBigDecimal(), isNumber(), isInteger()

String conversion and test (see Strings and GStrings)

n.toInteger(), intValue(), longValue()

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.

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

== 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