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.

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

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

java.lang.*

String, Math, Thread

java.util.*

List, Map, Date

java.io.*

File, Writer

java.net.*

URL, URI

java.time.*

LocalDateTime, ZoneOffset

groovy.lang.*

Binding, Closure, GroovyShell

groovy.util.*

ConfigObject, Expando

java.math.BigInteger, java.math.BigDecimal

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 which sin(0) and cos(0) work without Math..

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

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 SQLDate lets java.util.Date and java.sql.Date live in the same file without fully qualified names.

  • Static import aliasing: import static Calendar.getInstance as now turns Calendar.getInstance() into a call to now(), 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().

flowchart LR src["Greenhouse.groovy"] subgraph gen["Generated class Greenhouse extends Script"] direction TB run["run(): every loose statement, in order"] meth["methods: copied as they are"] main["static main(String[]): InvokerHelper.runScript"] end bnd["Binding: undeclared variables, args"] src -- "loose statements" --> run src -- "def power(n) { ... }" --> meth src -- "compiler adds" --> main run -- "undeclared name x" --> bnd meth -- "undeclared name x" --> bnd main --> 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)

Things to read from the output:

  • The class is Greenhouse, a subclass of Script, with run(), the copied method power and the generated main.

  • run() returns the value of the last statement, here the assignment threshold = 30. A script always has a return value because Script.run() is declared to return Object.

  • The undeclared name threshold was 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 main is 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 main are 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 main with no arguments or with one String[] parameter; Groovy also accepts a single untyped (Object) parameter, for example def main(args) { ... }.

  • Java expects void main; Groovy also accepts def 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 java itself 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

static main(String[] args)

2

static main(Object args)

3

static main()

4

main(String[] args) (instance)

5

main(Object args) (instance)

6

main() (instance)

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