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, @NamedParam and @NamedDelegate, @TupleConstructor and @MapConstructor were 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.