Type Coercion & Promotion

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, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26 (checked on 2026-10-14). Groovy converts between types far more readily than Java: a String becomes an enum constant, a closure becomes a Runnable, a map becomes an interface implementation, and 1 + 1.5 gets a result type from fixed promotion rules. Two different mechanisms are involved. Promotion is what arithmetic does to numeric operands. Coercion is a conversion that Groovy applies on assignment or on request with as. Knowing which one is at work explains most "why did this compile?" and "why did this not throw?" surprises.

Cast versus the as operator

A Java-style cast (T) value checks that the value already is a T (or converts it in the few cases where the language defines the conversion, such as numbers and char). The coercion operator value as T calls value.asType(T), which can convert much more. The first fails for a string that merely looks like a number; the second parses it.

try {
    println((Integer) '42')
} catch (e) {
    println "${e.class.simpleName}: ${e.message}"
}
println('42' as Integer)              // asType parses the string
println((String) 42)                  // a cast to String calls toString()
println((double) 3)                   // numeric casts convert
println('ab' as List)                 // a CharSequence is coerced to a list of characters
try {
    println((List) 'ab')
} catch (e) {
    println e.class.simpleName
}
println(null as String)               // null stays null
println(null as boolean)              // ... except for boolean, where it is false
GroovyCastException: Cannot cast object '42' with class 'java.lang.String' to class 'java.lang.Integer'
42
42
3.0
[a, b]
GroovyCastException
null
false

A single-character string is a special case worth knowing: (int) '5' yields the character code 53, while '5' as int parses the text and yields 5 ('55' as int gives 55, and (int) '55' is a GroovyCastException). Prefer as or toInteger() when the intent is parsing; see Strings & GStrings.

println('5' as int)
println((int) '5')
println('55' as int)
println((int) 'a')
println('5'.toInteger())
5
53
55
97
5

Number promotion

For arithmetic, the type of the result is decided by the operand types. The rules, in the order Groovy applies them:

Operands Result type

byte, char, short, int with each other

int

any of them with long

long

any integral type with BigInteger

BigInteger

float or double with any integral type or each other

double

BigDecimal with byte, short, int, long or BigInteger

BigDecimal

BigDecimal with float or double

double

division / of two integral operands

BigDecimal (an exact quotient, or intdiv for an integer)

power ** with an integer base and a non-negative integer exponent

Integer, widening to Long then BigInteger as needed

def show = { println "${it.class.simpleName}: ${it}" }
show(1 + 1L)
show(1 + 1.5f)
show(1 + 1.5)               // 1.5 is a BigDecimal literal
show(1.5f + 1.5)            // float + BigDecimal -> double
show(1.5d + 1.5G)
show(1 / 2)
show(4 / 2)
show(10.intdiv(3))
show(Integer.MAX_VALUE + 1) // int + int stays int: it wraps
show(Integer.MAX_VALUE + 1L)
show(2 ** 3)
show(2 ** -1)
show(2 ** 0.5)
show((byte) 1 + (byte) 2)
show(new BigInteger('9' * 20) + 1)
show(1L + 1G)
Long: 2
Double: 2.5
BigDecimal: 2.5
Double: 3.0
Double: 3.0
BigDecimal: 0.5
BigDecimal: 2
Integer: 3
Integer: -2147483648
Long: 2147483648
Integer: 8
Double: 0.5
Double: 1.4142135623730951
Integer: 3
BigInteger: 100000000000000000000
BigInteger: 2

Two lines deserve attention. Integer.MAX_VALUE + 1 wraps silently, exactly as in Java, because both operands are int: promotion widens to the larger operand type, it does not detect overflow. And 4 / 2 is a BigDecimal 2, not an Integer; use intdiv when an integer quotient is wanted. The full rules, the literal suffixes (G, L, I, D, F) and the overflow behaviour of ** are on Numbers & Numeric Literals.

Implicit coercion in assignments

Assigning to a variable, field, parameter or return type declared with a type coerces the value to that type when Groovy knows a conversion, whether or not the value is exactly the right class. Dynamic Groovy applies the conversion at run time and applies the same narrowing rules as an explicit cast, which has two consequences: the conversion often works where Java would not compile, and numeric narrowing happens without any warning.

int i = 3.9                       // truncates toward zero
long l = 5                        // widening
double d = 1
float f = 2
BigDecimal bd = 5
String s = 42                     // toString()
char c = 65                       // int to char
println([i, l, d, f, bd, s, c]*.class*.simpleName)
println([i, l, d, f, bd, s, c])

int wrapped = 3000000000L         // long to int: bits are cut
byte small = 300                  // int to byte: bits are cut
println "${wrapped} ${small}"

List<String> strings = [1, 2]     // generics are erased: no conversion of the elements
println strings*.class*.simpleName

try {
    int bad = 'abc'
} catch (e) {
    println "${e.class.simpleName}: ${e.message.readLines()[0]}"
}
[Integer, Long, Double, Float, BigDecimal, String, Character]
[3, 5, 1.0, 2.0, 5, 42, A]
-1294967296 44
[Integer, Integer]
GroovyCastException: Cannot cast object 'abc' with class 'java.lang.String' to class 'int'

Truncation to int and byte and the unchecked List<String> are all accepted by the dynamic compiler. With @CompileStatic or @TypeChecked the narrowing from long to int becomes a compile-time error ("Possible loss of precision from long to int"); see Static Type Checking & @CompileStatic.

Closure to SAM and abstract class coercion

A closure is coerced to an interface with a single abstract method (a SAM type) when it is assigned to a variable of that type or passed to a parameter of that type, with no as needed. The same applies to an abstract class that has one abstract method; the closure then implements it, and the class’s concrete methods work as usual. With as, a closure can also stand for an interface with several methods: every method of the interface calls the same closure.

Runnable r = { println 'run!' }
r.run()

java.util.function.Function<String, Integer> length = { it.size() }
println length.apply('basil')

Comparator<String> byLength = { a, b -> a.size() <=> b.size() }
println(['ccc', 'a', 'bb'].sort(false, byLength))

abstract class Handler {
    abstract String handle(String s)
    String twice(String s) { handle(handle(s)) }
}
Handler h = { it + '!' }
println h.twice('hi')

interface Events { void onStart(); void onStop() }
def calls = []
Events ev = { calls << 'called' } as Events    // one closure for both methods
ev.onStart()
ev.onStop()
println calls
run!
5
[a, bb, ccc]
hi!!
[called, called]

For closures, see Closures.

Map and list coercion

A map of closures keyed by method name, followed by as, implements an interface or an abstract class: each key supplies the body of the method with that name. This is the lightest way to stub a collaborator in a test. Methods with no entry throw UnsupportedOperationException when called. A map of property names to values, assigned to (or coerced to) a class with a no-argument constructor, builds an instance by setting those properties; a list builds an instance through the constructor that matches its elements.

interface Sensor { String id(); double read() }
abstract class Shape {
    abstract double area()
    String describe() { "area=${area()}" }
}

def sensor = [id: { 's1' }, read: { 21.5d }] as Sensor
println "${sensor.id()} ${sensor.read()}"

def partial = [id: { 's2' }] as Sensor
println partial.id()
try {
    partial.read()
} catch (e) {
    println "${e.class.simpleName}: ${e.message}"
}

def shape = [area: { 2.5d }] as Shape
println shape.describe()

class Plant { String name; int zone }
Plant p = [name: 'basil', zone: 3]            // map to bean
println "${p.name} ${p.zone}"
println(([name: 'mint', zone: 1] as Plant).class.simpleName)

class Point {
    int x
    int y
    Point(int x, int y) { this.x = x; this.y = y }
    String toString() { "(${x},${y})" }
}
Point pt = [3, 4]                             // list to constructor arguments
println pt
println([5, 6] as Point)

println([1, 2, 3] as Set)
println(([1, 2, 3] as int[]).class.simpleName)
println(([3, 1, 2] as SortedSet).class.simpleName)
String[] array = ['a', 'b']
println array.class.simpleName
println(['a', 'b'] as LinkedList)
s1 21.5
s2
UnsupportedOperationException: null
area=2.5
basil 3
Plant
(3,4)
(5,6)
[1, 2, 3]
int[]
TreeSet
String[]
[a, b]

The map form is used extensively for stubbing in tests; see Testing with JUnit & GroovyTest. Maps themselves are covered on Maps, and the constructors involved on Methods, Constructors & Parameters.

String to enum coercion

A string is coerced to an enum of the target type when the text equals a constant name exactly. The conversion applies to assignments and to as, and a GString works too (it is converted to a String first). A wrong name, including a wrong case, is an IllegalArgumentException. The reverse direction is a plain toString().

enum Season { SPRING, SUMMER, AUTUMN, WINTER }

Season s = 'SUMMER'
println s
def w = 'WINTER' as Season
println w
def gs = "${Season.AUTUMN}"
Season fromGString = gs
println fromGString
String name = Season.SPRING
println name.class.simpleName
try {
    Season bad = 'summer'
} catch (e) {
    println "${e.class.simpleName}: ${e.message}"
}
SUMMER
WINTER
AUTUMN
String
IllegalArgumentException: No enum constant Season.summer

This is what makes switch and map lookups on enums pleasant. See Enums & Annotations.

Custom coercion with asType

a as T is syntax for a.asType(T), and any class can define Object asType(Class target) to say what it can become. Handle the types you support and delegate everything else to super.asType(target), which keeps the standard coercions and raises the usual GroovyCastException for the rest. The result must be an instance of the target: returning a GString for as String fails with a ClassCastException, so call toString() on it.

class Celsius {
    BigDecimal value

    Object asType(Class target) {
        if (target == Double) return value.toDouble()
        if (target == String) return "${value} C".toString()   // toString(): a GString is not a String
        if (target == Map) return [unit: 'C', value: value]
        super.asType(target)
    }
}

def c = new Celsius(value: 21.5)
println(c as Double)
println(c as String)
println(c as Map)
try {
    c as Integer
} catch (e) {
    println "${e.class.simpleName}: ${e.message.replaceAll(/@\w+/, '@...')}"
}
21.5
21.5 C
[unit:C, value:21.5]
GroovyCastException: Cannot cast object 'Celsius@...' with class 'Celsius' to class 'java.lang.Integer'

For classes you cannot edit, asType can be added through a metaclass or an extension module; see Metaclasses, ExpandoMetaClass & Categories and Extension Modules. The operator-to-method table is on Operator Overloading.

Intersection-type casts (Groovy 6.0)

Java allows a cast to several types at once, (Runnable & Serializable), mainly to make a lambda serializable. Groovy 6.0 accepts the same syntax on lambdas, method references and closures, and the as operator accepts the intersection form in parentheses. The lambda then implements all the bounds (the extra ones as marker interfaces), and a lambda that is cast to Serializable can be written to an ObjectOutputStream and read back. Without the cast, the lambda in the example is not serializable.

import groovy.transform.CompileStatic
import java.util.function.Supplier

@CompileStatic
class Lambdas {
    static Supplier<String> plain() { () -> 'basil' }
    static Supplier<String> both() { (Supplier<String> & Serializable) () -> 'basil' }
}

println(Lambdas.plain() instanceof Serializable)
println(Lambdas.both() instanceof Serializable)

def bytes = new ByteArrayOutputStream()
new ObjectOutputStream(bytes).writeObject(Lambdas.both())
Supplier<String> back = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray())).readObject()
println back.get()

try {
    new ObjectOutputStream(new ByteArrayOutputStream()).writeObject(Lambdas.plain())
} catch (NotSerializableException e) {
    println 'plain lambda: NotSerializableException'
}

def viaAs = { println 'closure' } as (Runnable & Serializable)
println(viaAs instanceof Runnable)
viaAs.run()
false
true
basil
plain lambda: NotSerializableException
true
closure

Use it where a framework needs a serializable function object, for example distributed-computing libraries or serialized query criteria. A closure is Serializable already when it only captures serializable values; the cast matters most for real lambdas. Lambdas and method references are covered on Closures and Differences with Java.

Class literals versus variables with as

The right-hand side of as is a type, resolved at compile time: it must be a class name, not a variable that holds a class. When the target type is only known at run time, call asType on the value instead.

def type = Integer
println('42' as Integer)                        // a class literal
println('42'.asType(type))                      // a variable: call asType
println('42'.asType(Long).class.simpleName)
println('1.5'.asType(Class.forName('java.lang.Double')))

try {
    new GroovyShell().evaluate("def type = Integer; '42' as type")
} catch (e) {
    println e.class.simpleName
    println e.message.readLines()[1]
}
42
42
Long
1.5
MultipleCompilationErrorsException
Script1.groovy: 1: unable to resolve class type

This is why generic conversion helpers take a Class parameter and call value.asType(clazz) (or clazz.cast(value) when no conversion is wanted, only a checked cast).

Where the book is outdated

The Manning book Groovy in Action (publisher page in the references) devotes a section to the coercion rules, and the mechanisms (as, asType, number promotion) are unchanged. What to check against the current documentation instead of the book:

Other languages on this site make conversions explicit: Kotlin Basic Types & Variables has no implicit widening between numeric types, and Java Primitive Types & Variables describes the widening and narrowing rules that Groovy adds to.