Optionality: Parentheses, Semicolons, return & public
|
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). Much of what Java makes mandatory is optional in Groovy: the parentheses around call arguments, the
semicolon that ends a statement, the return keyword and the public modifier. Leaving them out is what gives Groovy code
its light look and makes domain-specific languages possible. It also has rules, and the failures when the rules are broken
are runtime errors that can look unrelated to the cause. This page lists the rules, with the failing cases.
Optional parentheses
A method call used as a statement can omit its parentheses when it has at least one argument. A call with several arguments separates them by commas; a closure as the last argument can follow the call directly (the call can have parentheses before it or not). Used like this, the call is a command expression.
def add(a, b) { a + b }
def label(String s) { "<${s}>" }
def twice(Closure c) { c(); c() }
println 'optional parentheses' // one argument
println add(1, 2) // parentheses kept for a nested call
def total = add 3, 4 // two arguments, no parentheses
println total
twice { print 'tick;' } // closure as the only argument
println()
println label('a').toUpperCase() // chained calls need parentheses
println "${label 'b'}" // a command expression inside a placeholder
def water(String plant) { [every: { Integer h -> "water ${plant} every ${h}h" }] }
def plan = water 'tomatoes' every 2 // command chain: water('tomatoes').every(2)
println plan
optional parentheses
3
7
tick;tick;
<A>
<b>
water tomatoes every 2h
Several command expressions in a row form a command chain: a b c d means a(b).c(d), and water 'tomatoes' every 2
above reads as water('tomatoes').every(2). The chain alternates method names and arguments, and a bare word at an odd
position is a property access. This is the grammar behind water 'tomatoes' every 2.hours at 6.am in the greenhouse
watering DSL; see
Domain-Specific Languages for how such a chain is
built.
When parentheses are required
The parentheses are needed whenever Groovy could not otherwise tell a call from a property access or a variable, and
whenever the command-expression grammar does not apply. The cases below were each run on Groovy 6.0.0; each failure is
shown as it occurs, using GroovyShell.evaluate so that all of them can be shown from one script.
def attempts = [
'nested command expression': 'println add 1, 2',
'no-argument call': 'def f() { 1 }; def g = f',
'literal receiver': 'println [1, 2].collect { it * 2 }',
'method name as argument': 'def l = [3, 1]; l.each println',
'condition in assert': 'assert add 1, 2 == 3',
'property is not a call': "def s = 'a'; s.toUpperCase",
'parentheses used': 'println(add 1, 2)',
]
attempts.each { name, code ->
try {
new GroovyShell(new Binding()).evaluate("def add(a, b) { a + b }\n" + code)
println "${name}: ok"
} catch (Throwable e) {
println "${name}: ${e.class.simpleName}: ${e.message.readLines()[0]}"
}
}
nested command expression: MissingPropertyException: No such property: println for class: Script1
no-argument call: MissingPropertyException: No such property: f for class: Script1
literal receiver: MissingPropertyException: No such property: println for class: Script1
method name as argument: MissingPropertyException: No such property: println for class: Script1
condition in assert: MultipleCompilationErrorsException: startup failed:
property is not a call: MissingPropertyException: No such property: toUpperCase for class: java.lang.String
3
parentheses used: ok
(The 3 is the output of the last case, whose println ran.) The rules behind the list:
-
A call without arguments always needs
().fis a property or variable reference, not a call, and fails withMissingPropertyException. The same applies tos.toUpperCaseversuss.toUpperCase(). -
A command expression cannot be an argument of another command expression.
println add 1, 2is read as a chain, not asprintln(add(1, 2)); writeprintln add(1, 2)orprintln(add 1, 2). Likewise the argument of a command expression cannot start with a method call you want to chain:println label('a').toUpperCase()is fine because of the parentheses,one 'a'.toUpperCase()would not callonewith the upper-cased value. -
A literal list or map after a method name is an index.
println [1, 2].collect { it * 2 }isprintln[1, 2], a subscript on a (missing) property; useprintln([1, 2].collect { it * 2 }). -
A method name is not a value.
l.each printlndoes not pass the method; use a closurel.each { println it }or a method pointerl.each(this.&println). -
Keywords such as
asserttake an expression, not a command expression.assert add 1, 2 == 3is a syntax error; writeassert add(1, 2) == 3. -
Constructor calls and calls on
this/superneed parentheses as in Java:new Plant(name: 'basil').
A command expression may also use named arguments without braces or brackets (cfg a: 1, b: 2 is the call
cfg([a: 1, b: 2])), which is how builders and DSLs read; see
Builders.
Optional semicolons
A newline ends a statement, so the semicolon is only needed to put two statements on one line. The trap is the reverse:
Groovy ends a statement at the newline when the line is already a complete statement. An operator at the end of a line
tells the parser the statement continues; an operator at the start of the next line starts a new statement. Inside
parentheses or brackets, newlines are ignored; a line that starts with . continues the previous expression.
def a = 1; def b = 2 // two statements on one line need the semicolon
println a + b
def sum = a +
b // operator at the end of the line: statement continues
println sum
def broken = a
+ b // operator at the start of the next line: a new statement
println broken
def wrapped = (a
+ b) // inside parentheses newlines are ignored
println wrapped
def chain = [3, 1, 2]
.sort() // a leading dot continues the expression
.reverse()
println chain
3
3
1
3
[3, 2, 1]
broken is 1, not 3: the second line + b is a separate (unary plus) statement whose value is discarded. Put the
operator at the end of the line, or wrap the expression in parentheses. A semicolon is still required between the clauses
of a classic for loop and after a statement that shares its line with another. See
Lexical Structure, Comments, Keywords & Identifiers
for the newline rules (the "grouping stack").
The optional return keyword
A method returns the value of its last evaluated expression when it has no return. The last statement may be an
expression, an assignment, an if/else (the value of the branch taken), a try (the value of the try block when it
completes, ignoring finally) or a switch (the value of the branch). A loop, a missing else branch and a declared void
method produce null. return is still needed to leave a method early and when the return value is not the last
expression.
def implicit() { 42 }
int explicit() { return 42 }
println implicit() + explicit()
def lastIf(boolean b) { if (b) 'yes' else 'no' }
println lastIf(true) + lastIf(false)
def ifNoElse(boolean b) { if (b) 'yes' }
println ifNoElse(false) // no else branch: null
def lastTry() { try { 'ok' } finally { 'ignored' } }
println lastTry()
def lastSwitch(x) { switch (x) { case 1: 'one'; break; default: 'other' } }
println lastSwitch(1) + lastSwitch(2)
def assignLast() { def x = 5 }
println assignLast() // an assignment is an expression
def loopLast() { for (i in 1..3) { i } }
println loopLast() // a loop has no value
def voidLike() { println 'side effect' }
println voidLike() // println returns null
void v() { 'x' }
println v()
def closureLast = { a, b -> a * b }
println closureLast(3, 4)
def firstEven(list) {
for (n in list) {
if (n % 2 == 0) return n // early return
}
null
}
println firstEven([1, 3, 4, 6])
84
yesno
null
ok
oneother
5
null
side effect
null
null
12
4
Two consequences follow. A method whose last line is a side-effecting call, such as list << x or map.put(k, v), returns
that call’s result to the caller, which can leak a mutable collection or confuse the code that calls it. And a method that
is meant to return nothing should be declared void so the value is discarded explicitly. A return in a closure returns
from the closure only, not from the enclosing method; see Closures and
Control Structures.
The optional public keyword
Classes, methods and constructors are public by default, so public is redundant on them. A field declared without a
modifier is not a public field: it is a property, which Groovy compiles to a private field with a public getter and
(unless final) setter. Writing public on a field turns it into a real public field with no accessors. Groovy does not
enforce private: reflection-style access from other code still works, which the example shows for a method and a field.
import java.lang.reflect.Modifier
class Zone {
String name // property: private field plus public getter and setter
public int exposed = 2 // public field: no accessors generated
private int secret = 1
String hello() { 'hi' } // public by default
private String hidden() { 'private but callable' }
}
def z = new Zone(name: 'bench')
println z.hello()
println z.hidden() // Groovy does not enforce private
println z.secret
println Modifier.isPublic(Zone.getDeclaredMethod('hello').modifiers)
println Modifier.isPrivate(Zone.getDeclaredField('name').modifiers)
println Modifier.isPublic(Zone.getDeclaredField('exposed').modifiers)
println Zone.declaredMethods*.name.findAll { it.endsWith('Name') }.sort()
hi
private but callable
1
true
true
true
[getName, setName]
The access to hidden() and secret works because the dynamic runtime resolves members at run time; it is a known
limitation, and @CompileStatic rejects it. Packages, visibility modifiers, @PackageScope and properties are covered on
Classes, Fields & Properties. A script’s main method
and the way a script file is turned into a class are on
Program Structure, Packages, Imports & Scripts.
Putting it together
A greenhouse snippet that uses all four relaxations at once, followed by the same code in the explicit, Java-like style. Both do the same thing; the relaxed one is shorter, and the explicit one shows what the compiler adds.
// relaxed style
class Bench {
String name
List<Integer> temps = []
def record(int t) { temps << t; this }
def average() { temps ? temps.sum() / temps.size() : 0 }
}
def bench = new Bench(name: 'north')
bench.record 20
bench.record 24
println bench.average()
// explicit style
public class Bench2 {
private String name;
private List<Integer> temps = new ArrayList<Integer>();
public Bench2 record(int t) { temps.add(t); return this; }
public Object average() { return temps.size() > 0 ? (Object) (temps.sum() / temps.size()) : 0; }
}
Bench2 other = new Bench2();
other.record(20);
other.record(24);
System.out.println(other.average());
22
22
Which style to use is a team decision: the relaxed form is the one used throughout the Groovy documentation, and Style Guide & Idioms collects the official recommendations. Keep parentheses where they remove ambiguity, and always where a call has no arguments.
Where the book is outdated
The Manning book Groovy in Action (publisher page in the references) explains optional parentheses, semicolons and
return in its chapter on the language basics, and the rules are unchanged. What to check against the current
documentation instead of the book:
-
Command chains (added in Groovy 1.8, after the first edition’s Groovy 1.0), which are the base of the DSL support described in Domain-Specific Languages.
-
The parser: Groovy 4.0 replaced the original parser with Parrot, which is stricter about some forms that used to compile. Code ported from the book may need parentheses added; see What’s New & Migrating from Older Versions.
-
The default imports and Java baseline (JDK 17 or later for Groovy 6.0).
Other languages in this site treat the same points differently: Kotlin Lexical Structure & Style has optional semicolons and implicit lambda returns, but always requires parentheses on calls, and Python Lexical Structure & Style has no semicolons or braces at all.