Program Structure, Packages, Imports & Scripts
|
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, released 2026-09-22; JDK 17 or newer). Every example was run with
Groovy 6.0.0 on JDK 26 and the output shown is the real output of that run. Features newer than Groovy 4.0 name the
release that introduced them. It covers how a file is organised: the package statement, the many forms of import,
what the compiler imports for you, and the difference between a script and a class, including the Groovy 5 support for
JEP 445/512 style source files.
Package names
Packages play the same role as in Java: they separate code bases and avoid name clashes. A class must declare its package before the class definition, otherwise it lives in the default package.
package greenhouse.climate
class Sensor { }
To refer to Sensor from elsewhere you use the fully qualified name greenhouse.climate.Sensor or an import. A script
may also start with a package statement; the generated class then lives in that package, as the first example below
shows. Unlike Java, Groovy does not require the directory structure of source files to match the package, although
following the Java convention keeps tools and humans happy.
Source: Groovy documentation: Package names.
Imports
An import lets you refer to a class by its simple name. The kinds are shown in one script that was run as i1.groovy.
package greenhouse.climate
import groovy.xml.MarkupBuilder // simple import
import groovy.xml.* // star import
import static java.lang.Math.* // static star import
import static Calendar.getInstance as now // static alias
import java.sql.Date as SQLDate // type alias
import java.util.Date
// default imports: no import statement needed
Date d = new Date(0L)
BigDecimal bd = 1.5
long t = LocalDateTime.of(2026, 1, 1, 0, 0).toEpochSecond(ZoneOffset.UTC)
assert t == 1767225600L
assert new StringWriter() instanceof Writer // java.io
assert new URI('http://localhost:8080/') instanceof URI // java.net
assert Binding.name == 'groovy.lang.Binding' // groovy.lang
assert ConfigObject.name == 'groovy.util.ConfigObject' // groovy.util
assert new StreamingMarkupBuilder() != null && new MarkupBuilder() != null
assert sin(0) == 0.0 && cos(0) == 1.0
assert now().class == Calendar.getInstance().class
assert new SQLDate(1000L) instanceof java.sql.Date && d instanceof java.util.Date
assert this.class.name == 'greenhouse.climate.i1'
println this.class.name
greenhouse.climate.i1
Source: Groovy documentation: Imports.
Default imports
Groovy adds these imports to every file, so that the most common classes need no import statement. Java adds only
java.lang.*.
| Import | Examples of what it gives you |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The two arbitrary-precision numbers, imported by name rather than by star. |
The example above uses LocalDateTime and ZoneOffset without importing them, and it ran. Note that java.sql.Date is not imported by default, so Date means
java.util.Date unless you alias something else.
Simple, star and static imports
-
A simple import names one class:
import groovy.xml.MarkupBuilder. -
A star import (on-demand import) brings in every class of a package:
import groovy.xml.*. It can clutter the local namespace, which aliasing (below) can repair. -
A static import makes a static member usable without its class prefix:
import static java.lang.Boolean.FALSE. -
A static star import brings in all static members of a class:
import static java.lang.Math.*, after whichsin(0)andcos(0)work withoutMath..
Groovy’s static imports are more dynamic than Java’s. You may declare a method with the same name as a statically imported one, as long as the parameter types differ; Java reports a compile error there, Groovy does not. If the signatures are identical, the imported method takes precedence.
import static java.lang.String.format
class SomeClass {
String format(Integer i) { i.toString() } // same name, different parameter type
static void main(String[] args) {
assert format('String') == 'String' // the statically imported String.format
assert new SomeClass().format(Integer.valueOf(1)) == '1'
println 'static import overload ok'
}
}
static import overload ok
Source: Groovy documentation: Static import.
Aliasing
The as keyword gives an imported name a different local name, for a type or for a static member.
-
Import aliasing:
import java.sql.Date as SQLDateletsjava.util.Dateandjava.sql.Datelive in the same file without fully qualified names. -
Static import aliasing:
import static Calendar.getInstance as nowturnsCalendar.getInstance()into a call tonow(), which reads better when the call is separated from the class name.
Both forms appear in the script above and its asserts passed.
Module imports (6.0)
Groovy 6.0 supports Java’s module import declarations (GROOVY-11896, JEP 476 as finalized for Java 25; the 6.0 release
notes refer to JEP 511 for the same Java feature). A single statement imports all public types from all the packages that
a named module exports, which is the same as writing a star import for each package. Packages reached through
requires transitive are included, and the feature works with JDK system modules, with JPMS modules on the classpath and
with automatic modules, even when running on JDK 17.
import module java.sql
assert Connection.name == 'java.sql.Connection'
assert Source.name == 'javax.xml.transform.Source' // via requires transitive (java.xml)
assert Logger.name == 'java.util.logging.Logger' // via requires transitive (java.logging)
println 'module import ok'
module import ok
An explicit single-type import beats a module-expanded one, which resolves ambiguities such as the two List classes in
java.desktop. The word module is contextual: it can still name a variable, method or class. The wildcard and alias
forms are not supported, and the compiler says so.
import module java.desktop
import java.util.List
assert List.name == 'java.util.List' // the explicit import wins over java.awt.List
def module = 'still a legal name'
assert module == 'still a legal name'
The unsupported forms are rejected by the parser. Evaluating them through GroovyShell:
import org.codehaus.groovy.control.MultipleCompilationErrorsException
def check = { String src ->
try { new GroovyShell().evaluate(src); println 'ok' }
catch (MultipleCompilationErrorsException e) { println e.message.readLines()[1] }
}
check 'import module java.base as jb'
check 'import module java.base.*'
Script1.groovy: 1: Unexpected input: 'as' @ line 1, column 25.
Script1.groovy: 1: Unexpected input: '.' @ line 1, column 24.
Namespace conflicts
As in Java, two imports with the same simple name but different types are an error, and so is an import together with a top-level class of the same name. An inner class may shadow an import.
import org.codehaus.groovy.control.MultipleCompilationErrorsException
def check = { String src ->
try { new GroovyShell().evaluate(src); println 'ok' }
catch (MultipleCompilationErrorsException e) { println e.message.readLines()[1] }
}
check 'import java.awt.List\nimport java.util.List'
check 'import java.util.List\nclass List { }'
check 'import java.util.List\nclass Main { class List { } }\nnew Main()'
Script1.groovy: 2: The name List is already declared @ line 2, column 8.
Script1.groovy: 2: The name List is already declared @ line 2, column 1.
ok
Scripts versus classes
Groovy supports both. A class holds declarations; a script is source code with statements outside any class, and the compiler wraps it in a class for you. Since Groovy 5 there is a second way of writing runnable files, the JEP 445 compatible script, described below.
The Java version needs a class and a main:
class Main {
static void main(String... args) {
println 'Groovy world!'
}
}
The Groovy equivalent is the single line println 'Groovy world!': a script can be considered a class without declaring
it. How the file is run is covered in
Running Scripts & the groovy Command.
The Script class
A script is always compiled into a class that extends groovy.lang.Script. The compiler copies the body of the script
into a run() method, adds a static main(String[]) that calls InvokerHelper.runScript(Main, args), and names the
class after the file (Main.groovy gives class Main). Methods declared in the script become methods of that class;
the other ("loose") statements are assembled sequentially into run().
A script compiled in memory shows exactly that structure. The example parses a small script, lists the methods of the generated class and then runs it with a binding.
import java.lang.reflect.Modifier
def source = '''
println 'Hello'
int power(int n) { 2**n }
println "2^6==${power(6)}"
threshold = 30
'''
def shell = new GroovyShell()
def script = shell.parse(source, 'Greenhouse.groovy')
def cls = script.class
assert cls.name == 'Greenhouse'
assert cls.superclass == Script
println cls.declaredMethods.findAll { !it.synthetic }
.collect { (Modifier.toString(it.modifiers) + ' ' + it.returnType.simpleName + ' ' + it.name).trim() }.sort()
script.binding = new Binding(threshold: 25)
def result = script.run()
println "run() returned ${result.inspect()}"
println "binding now: ${script.binding.variables}"
// stack traces keep script line numbers
def bad = shell.parse('\n\nthrow new IllegalStateException("boom")', 'Bad.groovy')
try { bad.run() } catch (e) { println e.stackTrace.find { it.className == 'Bad' } }
[public Object run, public int power, public static transient void main]
Hello
2^6==64
run() returned 30
binding now: [threshold:30]
Bad.run(Bad.groovy:3)
Source: Groovy documentation: Script class.
Things to read from the output:
-
The class is
Greenhouse, a subclass ofScript, withrun(), the copied methodpowerand the generatedmain. -
run()returns the value of the last statement, here the assignmentthreshold = 30. A script always has a return value becauseScript.run()is declared to returnObject. -
The undeclared name
thresholdwas written to the binding that was handed in. That is how a host application passes data in and out of a script (see Integrating Groovy into Java Applications). -
Even though the code is wrapped, line numbers of the original file are preserved in stack traces (
Bad.groovy:3).
Methods in scripts
Methods can be defined anywhere in the script body and mixed with statements. The generated class carries all of them, so a script method can call another one, and a loose statement can call a method that is declared further down:
int fib(int n) {
n < 2 ? 1 : fib(n-1) + fib(n-2)
}
assert fib(10) == 89
Variables: local, binding or @Field
Whether a variable is declared decides where it lives. A declared variable (int x = 1, or def x) is a local variable
of the generated run() method and is not visible to other methods of the script. An undeclared variable goes into the
Binding, which is visible from methods and from the host application. @Field turns a declaration into a field of the
generated class: visible to all methods, but not part of the binding. The runnable demonstration, with the real output,
is in Variables, Typing, def, var & val.
If a local variable or @Field shares its name with a binding variable, rename one of them; otherwise reach the binding
one with binding.varName.
Convenience variations
Normally run() and static main(String[]) are added for you, so declaring your own is a "duplicate method" error. The
exceptions are scripts that contain only a compatible main and no loose statements, or only a no-arg run() instance
method (the latter from Groovy 5). The method you supply is used instead of the generated one. This is useful for
putting an annotation on the otherwise invisible method.
To count as a convenience variation the main parameter must be untyped (Object), String[], or absent (the no-arg
form is from Groovy 5). The run() variation still produces a Script subclass and therefore keeps the binding; the
documentation recommends it when you need the script context. The no-arg main variations produce a JEP 445 compatible
class that does not extend Script.
@groovy.transform.CompileStatic
static main(args) {
println "statically compiled main, ${args.getClass().simpleName}"
}
// only a no-arg instance run(): still a Script subclass (Groovy 5+)
def run() {
println "script subclass: ${this instanceof Script}, binding: ${binding.variables}"
}
Run as groovy c1.groovy a b and groovy c2.groovy a b:
statically compiled main, String[]
script subclass: true, binding: [args:[a, b]]
JEP 445 and 512 compatible scripts (5.0)
From Groovy 5, a source file whose only top-level code is a main method (plus fields and methods) is a JEP 445
compatible class. It differs from an ordinary script in four ways:
-
No
public static void mainis added. -
It does not extend
Script, so it has no script context and no binding variables. -
Additional class-level fields and methods are allowed next to
main, and fields do not need@Field. -
No loose statements outside
mainare allowed (field definitions excepted).
Multiple assignment at the top level is supported and results in a separate field per component.
// JEP 445/512 style: instance main, extra field and methods, no loose statements
def main() {
assert upper(zone) + lower(crop) == 'NORTHbasil'
println "is Script? ${this instanceof Script}"
}
def upper(s) { s.toUpperCase() }
def lower = String::toLowerCase
def (zone, crop) = ['north', 'BASIL']
void main(args) {
println "instance main, args=${args.toList()}"
}
println 'loose statement'
void main() { println 'main' }
Run as groovy c3.groovy a b, groovy c4.groovy a b and groovy c6.groovy a b:
is Script? false
instance main, args=[a, b]
loose statement
The first file runs main as an instance method of a class that is not a Script. The third file has a loose statement,
so it is treated as a normal script and its main is just a script method that nothing calls.
Differences with Java
-
Java accepts a
mainwith no arguments or with oneString[]parameter; Groovy also accepts a single untyped (Object) parameter, for exampledef main(args) { ... }. -
Java expects
void main; Groovy also acceptsdef main(...). -
Both additions are understood by the Groovy runner but not by the Java launch protocol of a JDK with JEP 445/512.
-
For static main variants Groovy promotes the no-arg or untyped variants to the standard
public static void main(String[] args), for compatibility with versions before Groovy 5. Such classes therefore still work with the Java launcher of older JDKs. -
Groovy’s runner understands these classes on JDK 11 and above, without preview mode. Running a JEP 512 style class from
javaitself needs JDK 25 or newer.
Main method selection priority
When a class has several valid main signatures, the Groovy runner uses a priority aligned with JEP 512: static before
instance, and with-parameters before no-arg.
| Priority | Method signature |
|---|---|
1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
Only one method is selected; the others are unreachable from the Groovy runner. The documentation says a compile-time
warning is issued for any valid main shadowed by a higher-priority one, and recommends having a single main.
Mixed static and instance overloads do follow the table:
// p_a.groovy
static void main(String[] args) { println "static String[]" }
void main() { println "instance no-arg" }
// p_d.groovy
static void main() { println "static no-arg" }
void main(String[] args) { println "instance String[]" }
static String[]
static no-arg
In my runs of Groovy 6.0.0, a file with two static variants, or two instance variants, did not compile at all: the
compiler failed in the conversion phase with Repetitive main method found. Only a mix of one static and one instance
variant worked, as above. The practical conclusion matches the documentation’s advice: write one main.
Where the book is outdated
The book consulted for this reference is the first edition of Groovy in Action (2007), which describes Groovy 1.0 on
JDK 1.4 and 5. Its examples use no annotations, enums, generics, static imports or varargs, and of course none of the Groovy 5
and 6 additions on this page (instance main, no-arg run(), JEP 445/512 compatible scripts, module imports). Its
discussion of the Groovy lifecycle (how a script becomes a class and runs) is still conceptually right. The default import
list has also grown: check the table above rather than the book. The classpath discussion in the book predates Grape
(@Grab), which is covered in
Grape Dependency Management.
References
-
Groovy documentation: Program structure — package names, imports, scripts versus classes, JEP 445 compatible scripts
-
Groovy 6.0 release notes — module import declarations (GROOVY-11896)
-
Groovy 5.0 release notes — additional scripting variations (JEP 445/512)
-
Groovy in Action (Manning) — publisher page of the book consulted for the program lifecycle and packages
-
Variables, Typing, def, var & val — script variables in more detail
-
Compiling with groovyc & Joint Compilation — compiling scripts and classes ahead of time
-
Java: Packages and Modules — the Java side of packages, imports and JPMS modules