Methods, Constructors & Parameters
|
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). Methods and constructors look like their Java counterparts but behave differently in three visible ways: arguments can be passed by name, parameters can have default values, and the method that runs is chosen from the runtime types of the arguments (multi-methods). The Java side of the comparison is on Java Methods & Parameters. The class model these methods belong to is described in Classes, Fields & Properties.
Defining methods
A method has an optional modifier, a return type or def, a name, a parameter list and a body. The value of the last
expression in the body is returned when there is no return, and a void method discards it. Parentheses around the
arguments of a call can be left out when there is at least one argument and the call is a statement or a simple
expression; a call without arguments needs ().
// Methods: return type, def, implicit return, static, visibility
class Calc {
int twice(int x) { x * 2 } // last expression is the result
def describe(x) { "value: $x" } // untyped parameter and result
static String unit() { 'celsius' }
private int secret() { 42 }
void noResult() { 1 + 1 } // void discards the value
int explicit(int x) { return x + 1 }
}
def c = new Calc()
println c.twice(21)
println c.describe([1, 2])
println Calc.unit()
println c.noResult()
println c.explicit(1)
// parentheses are optional for calls with arguments
def r = c.twice 4
println r
// private is not enforced at run time for calls made from Groovy
println c.secret()
42
value: [1, 2]
celsius
null
2
8
42
A void method called from Groovy yields null, as the fourth line shows. Methods can be static, final, abstract,
synchronized and so on, with the Java meanings; private is a declaration of intent that the dynamic call path does not
enforce (see Classes, Fields & Properties). Closures
are an alternative to small methods; see Closures.
Constructors
A class with no declared constructor gets a public no-argument one. Declaring any constructor removes it. Constructors can
call another constructor of the same class with this(...), or the superclass with super(...), as in Java.
Besides new, Groovy allows two coercions that call a constructor: a List assigned to a typed variable, or as. And
when a no-argument constructor is available, new T(name: value, ...) sets properties after construction (see
Classes, Fields & Properties).
class Sprinkler {
String zone
int litres
Sprinkler() { zone = 'none'; litres = 0 }
Sprinkler(String zone) { this(zone, 10) } // constructor chaining
Sprinkler(String zone, int litres) { this.zone = zone; this.litres = litres }
String toString() { "Sprinkler($zone, $litres)" }
}
def a = new Sprinkler()
def b = new Sprinkler('north')
Sprinkler c = ['south', 20] // list coercion calls the constructor
def d = ['east', 5] as Sprinkler // explicit coercion
def e = new Sprinkler(zone: 'west', litres: 7) // no-arg constructor plus setters
println([a, b, c, d, e])
[Sprinkler(none, 0), Sprinkler(north, 10), Sprinkler(south, 20), Sprinkler(east, 5), Sprinkler(west, 7)]
Generated constructors
Writing constructors that only copy parameters into fields is routine, so Groovy has AST transformations that generate
them. @TupleConstructor creates a positional constructor with one parameter per property, in declaration order, with
default values taken from the initialisers, so trailing arguments can be omitted. @MapConstructor creates a constructor
taking a Map of property values. Both are described, with @Canonical, @Builder and the other class-design
transformations, in
AST Transformations: Code Generation & Class Design.
import groovy.transform.MapConstructor
import groovy.transform.ToString
import groovy.transform.TupleConstructor
@TupleConstructor
@ToString
class Valve {
String id
boolean open = false
}
@MapConstructor
@ToString
class Pump {
String id
int rpm
}
@TupleConstructor(includes = 'id')
@ToString
class Probe {
String id
int depth = 5
}
println new Valve() // every property has a default: zero-arg works
println new Valve('v1')
println new Valve('v2', true)
println new Pump(id: 'p1', rpm: 900)
println new Probe('pr1')
try { new Probe('pr2', 9) } catch (e) { println e.class.simpleName }
println new Pump()
Valve(null, false)
Valve(v1, false)
Valve(v2, true)
Pump(p1, 900)
Probe(pr1, 5)
GroovyRuntimeException
Pump(null, 0)
includes = 'id' restricts the generated constructor to the listed properties, which is why the two-argument call fails
with a GroovyRuntimeException (no matching constructor).
Named parameters
When a call passes name: value pairs, Groovy gathers all of them into a single Map and passes it as the first
argument, wherever they appear in the call. A method that wants named parameters therefore declares a Map as its first
parameter. If a call passes no named arguments, the Map is not supplied automatically, and the call fails unless the
method has another overload.
// Named parameters in a method: collected into a Map as the first parameter
def water(Map opts, int litres) {
"${opts.zone ?: 'all'} gets $litres l at ${opts.at ?: 'now'}"
}
println water(zone: 'north', at: '06:00', 30)
try { water(20) } catch (MissingMethodException e) { println 'no Map given: ' + e.message.readLines()[0] }
println water([:], 20)
println water(30, zone: 'south') // named arguments may appear anywhere in the call
north gets 30 l at 06:00
no Map given: No signature of method: water for class: blk4 is applicable for argument types: (Integer) values: [20]
all gets 20 l at now
south gets 30 l at now
(The class name in the message is the name of the script the example was run from.) A Map parameter does not check which
keys it receives; a misspelt key is silently ignored. The next section addresses that.
@NamedVariant, @NamedParam and @NamedDelegate
@NamedVariant (since Groovy 2.5) generates a second, Map-based overload of a method with ordinary positional
parameters, so the same method supports both call styles. If no parameter carries @NamedParam or @NamedDelegate,
every parameter becomes a named one. If some parameters are annotated, only those become named, and the others stay
positional after the Map. @NamedParam(required = true) makes a key mandatory, and @NamedDelegate makes the keys of a
class’s properties available as named arguments.
import groovy.transform.NamedDelegate
import groovy.transform.NamedParam
import groovy.transform.NamedVariant
// With no annotation on any parameter, all of them become named
@NamedVariant
String plan(String zone, int slots = 4) { "$zone:$slots" }
println plan(zone: 'north')
println plan(zone: 'north', slots: 6)
println plan('north', 2) // the original method is still there
// With @NamedParam, only the annotated parameters become named
@NamedVariant
String schedule(@NamedParam(required = true) String zone, @NamedParam int hour, int repeat) { "$zone@$hour x$repeat" }
println schedule(zone: 'a', hour: 6, 3)
try { schedule(hour: 6, 3) } catch (AssertionError e) { println e.message.readLines()[0] }
class Zone { String id; int slots }
@NamedVariant
String describe(@NamedDelegate Zone z, boolean verbose = false) { "${z.id}/${z.slots}/$verbose" }
println describe(id: 'a', slots: 3, true)
north:4
north:6
north:2
a@6 x3
Missing required named argument 'zone'. Keys found: [hour]. Expression: namedArgs.containsKey(zone)
a/3/true
The generated overload is the one that checks the keys: a missing required key fails with an AssertionError.
Default arguments and varargs
A parameter with a default value (int days = 7) may be omitted from the call, from the right. The compiler generates one
overload per omission, which is how the feature stays visible to Java callers. A trailing parameter declared as an array
type with ... or [] is a vararg: it accepts any number of arguments, including none, or an array. A vararg whose type is
Object... accepts mixed values.
// Default arguments
String report(String zone, int days = 7, String unit = 'celsius') { "$zone/$days/$unit" }
println report('a')
println report('a', 14)
println report('a', 14, 'kelvin')
println this.class.declaredMethods*.name.findAll { it == 'report' }.size()
// Varargs
int total(int... values) { values.sum(0) }
println total()
println total(1)
println total(1, 2, 3)
println total([4, 5] as int[])
String join(String sep, Object... parts) { parts.join(sep) }
println join('-', 'a', 1, 2.5)
println join('-')
a/7/celsius
a/14/celsius
a/14/kelvin
3
0
1
6
9
a-1-2.5
The three generated overloads (report(String), report(String, int) and report(String, int, String)) show up as
3 in the count. The last line prints an empty string: a vararg with no arguments is an empty array.
Method selection and multi-methods
Java picks an overload at compile time from the declared types of the arguments. Groovy picks it when the call runs,
from the runtime types, so this is called multiple dispatch or multi-methods. The selection prefers the candidate whose
parameter types are closest to the actual argument types in the class hierarchy: an exact match wins, then a superclass or
interface, and a primitive parameter is also matched by the corresponding wrapper. A null argument matches any reference
type, so it is resolved by the distance to Object. If two candidates are equally close, the call fails with an ambiguous
method overloading error.
// Method selection: the runtime types of the arguments decide
String pick(Object o) { 'Object' }
String pick(String s) { 'String' }
String pick(Integer i) { 'Integer' }
String pick(Number n) { 'Number' }
String pick(int i) { 'int' }
String pick(CharSequence cs) { 'CharSequence' }
Object o = 'text'
println pick(o) // runtime type String, not declared type Object
println pick(1)
println pick(1L)
println pick(1.5)
println pick("g${1}") // GString: no GString overload, so CharSequence
println pick(null) // resolved by distance to Object
println pick(new StringBuilder('x'))
String
Integer
Number
Number
CharSequence
Object
CharSequence
Note pick(1): both Integer and int fit, and the run picked the Integer overload, so do not rely on a primitive
overload being preferred for a literal. The second example contrasts Groovy with Java on a class hierarchy, and shows
an ambiguous call.
// Multi-methods versus Java: dispatch on runtime type
class Animal { }
class Dog extends Animal { }
String sound(Animal a) { 'generic' }
String sound(Dog d) { 'woof' }
Animal a = new Dog()
println sound(a) // Groovy: woof; Java would print generic
println sound(a as Animal)
// Ambiguity
interface A {}
interface B {}
class Both implements A, B {}
String amb(A a) { 'A' }
String amb(B b) { 'B' }
try { amb(new Both()) } catch (e) { println e.class.simpleName }
woof
generic
GroovyRuntimeException
The first call dispatches on the runtime class (Dog), where Java would print generic; an explicit cast on the argument
(a as Animal) steers the selection back to the Animal overload, as the second line shows. The ambiguity case raises a
GroovyRuntimeException whose message names the overlapping prototypes. See
Differences with Java for more on this difference, and
Static Type Checking & @CompileStatic for the mode in which
selection becomes compile-time again.
Declaring exceptions
A method may declare throws, and Groovy accepts the clause, but it never forces a caller to catch or declare a checked
exception: the check is a Java compiler rule, not a JVM rule. A checked exception thrown from Groovy code propagates like
any other, and a caller can catch it or not. Declaring throws is still useful as documentation, for Java callers that do
get the compile-time check, and for tools. Exception handling itself is in
Exceptions & Error Handling.
// Exception declaration is optional and unchecked
class Loader {
String load(String path) throws IOException {
if (!path) throw new IOException('empty path')
return "loaded $path"
}
String loadQuiet(String path) { // no throws clause needed
throw new FileNotFoundException(path)
}
}
def l = new Loader()
println l.load('a.cfg')
try { l.load('') } catch (IOException e) { println "caught ${e.message}" }
// checked exception escapes without being declared or caught
try { l.loadQuiet('missing.cfg') } catch (e) { println e.class.simpleName }
loaded a.cfg
caught empty path
FileNotFoundException
Where the book is outdated
Groovy in Action (publisher page in the references) covers method definition, default and named parameters, constructors and multi-methods, and that material still holds. Check the following against the current documentation:
-
@NamedVariant,@NamedParamand@NamedDelegate,@TupleConstructorand@MapConstructorwere added in later releases than the book describes. -
The method selection algorithm has been refined across releases, so consult the Semantics page rather than the book for edge cases (primitives,
null,GString). -
The Java baseline (JDK 17 or later for Groovy 6.0) and imports: see What’s New & Migrating from Older Versions.