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 |
|---|---|
|
|
any of them with |
|
any integral type with |
|
|
|
|
|
|
|
division |
|
power |
|
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:
-
The coercion forms added since: closure to SAM type without
as(Groovy 2.2 and later), map and list coercion in plain assignments, and, in 6.0, intersection-type casts for lambdas and closures. -
The number-literal and division behaviour, which has remained stable but is described on Numbers & Numeric Literals.
-
Static checking: with
@CompileStaticseveral of the lenient dynamic conversions shown on this page are compile errors; see Static Type Checking & @CompileStatic and What’s New & Migrating from Older Versions.
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.