Classes, Fields & Properties

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). Groovy compiles to ordinary JVM classes, so the class model is Java’s with a few changes of default: a class and its members are public unless stated otherwise, semicolons and types are optional, and a member declared without any modifier is a property rather than a package-private field. The property is the main idea of this page. For the Java side of the comparison see Java Classes & Objects.

Classes and types

A class has the familiar shape, and the greenhouse examples on this site use a small model throughout: a Greenhouse holds zones, a Zone holds plants and sensors report readings.

class Plant {
    String name
    int daysToHarvest
    double waterLitres = 0.5
}

def p = new Plant(name: 'basil', daysToHarvest: 30)
println "${p.name} ${p.daysToHarvest} ${p.waterLitres}"
println Plant.declaredFields.findAll { !it.synthetic }*.name.sort()
println Plant.declaredMethods*.name.findAll { it =~ /^[gs]et(?!MetaClass)/ }.sort()
assert java.lang.reflect.Modifier.isPublic(Plant.modifiers)   // classes are public by default
int days = 30
println days.class.name                  // a primitive used as an object is boxed
println p.daysToHarvest.getClass().name
basil 30 0.5
[daysToHarvest, name, waterLitres]
[getDaysToHarvest, getName, getWaterLitres, setDaysToHarvest, setName, setWaterLitres]
java.lang.Integer
java.lang.Integer

Points about types:

  • Groovy has the eight JVM primitives (int, long, double, boolean, …​) and all reference types. A primitive field or variable keeps the primitive representation, but as soon as it is used as an object (method call, def, collection element) it is boxed, which is why days.class.name is java.lang.Integer. The effect on arithmetic and overflow is described in Numbers & Numeric Literals.

  • Types are optional. def and an omitted type both mean Object; see Variables, Typing, def, var & val.

  • A file may hold several classes and the file name need not match a class. If the file also has statements outside any class it is a script; see Program Structure, Packages, Imports & Scripts.

  • Interfaces, abstract classes, extends and implements work as in Java and are in Inheritance, Interfaces & Generics.

Inner, nested and anonymous classes

Groovy supports all the Java nesting forms. An inner class (declared without static) holds a reference to its enclosing instance and can read the outer members, including private ones. A static nested class has no enclosing instance. An anonymous inner class implements an interface or extends a class at the point of use.

class Greenhouse {
    String name
    private List<String> log = []

    // inner class: sees the enclosing instance
    class Sensor {
        String id
        void report(double value) { log << "${id}=${value}" }
    }

    // static nested class: no enclosing instance
    static class Limits {
        static final double MAX_TEMP = 35.0
    }

    Sensor newSensor(String id) { new Sensor(id: id) }
}

def gh = new Greenhouse(name: 'north')
def s = gh.newSensor('t1')
s.report(21.5)
println gh.@log
println Greenhouse.Limits.MAX_TEMP

// anonymous inner class implementing an interface
def r = new Runnable() {
    void run() { println 'watering' }
}
r.run()
println r.class.isAnonymousClass()

// anonymous subclass of a class
def named = new Object() { String toString() { 'anon' } }
println named
[t1=21.5]
35.0
watering
true
anon

Two practical notes. An inner class created inside an instance method of the outer class (as newSensor does) gets the enclosing instance implicitly. For one-method interfaces a closure is usually simpler than an anonymous class, as { -> println 'watering' } as Runnable; see Closures.

Visibility

A member without a modifier is a property (next section), and a class without a modifier is public. The usual four levels exist: public, protected, private and package-private. Package-private is not the default: it must be requested with the @PackageScope annotation (groovy.transform.PackageScope), either on a field or method or on the whole class.

Because Groovy dispatches dynamically, private and protected are not enforced at run time for access from Groovy code (the example below reads a private field and calls a private method). This is a long-standing, documented behaviour and not something to rely on: treat private as a statement of intent.

package demo

class Visibility {
    public String open = 'public field'
    protected String guarded = 'protected field'
    private String hidden = 'private field'
    String prop = 'property'
    @groovy.transform.PackageScope String pkg = 'package-private field'
    private String secret() { 'private method' }
}

def v = new Visibility()
// Groovy does not enforce private at run time
println v.hidden
println v.secret()
println v.guarded

println Visibility.declaredFields.findAll { !it.synthetic }.collect { f ->
    def m = f.modifiers
    "${f.name}: " + (java.lang.reflect.Modifier.isPublic(m) ? 'public' :
        java.lang.reflect.Modifier.isProtected(m) ? 'protected' :
        java.lang.reflect.Modifier.isPrivate(m) ? 'private' : 'package')
}.sort()
private field
private method
protected field
[guarded: protected, hidden: private, open: public, pkg: package, prop: private]

The last line shows what the compiler produced: the property prop became a private field behind accessors, and @PackageScope was needed to get a package-private one. Java callers see exactly these members. Under @CompileStatic (see Static Type Checking & @CompileStatic) the compiler does reject illegal access to private members.

Fields versus properties

A field is a member with an explicit access modifier (private, protected, public): it is what it looks like in Java. A property is a member declared with a type or def and no access modifier: the compiler turns it into a private field, a public getter and, unless it is final, a public setter. static and final do not change this, so without an access modifier they give a static and/or read-only property. Reading obj.name calls getName() and assigning calls setName(...), so a later custom accessor never breaks callers.

class Reading {
    String sensor            // property: private field + getter + setter
    final int id = 7         // final property: private final field + getter only
    public String label = 'L' // field, public: no accessors
    private double raw = 1.5  // field
    def noType                // property of type Object
}
def r = new Reading(sensor: 's1')
println r.sensor
r.sensor = 'Re-assigned'
println r.getSensor()
r.setSensor('via setter')
println r.sensor
println Reading.methods*.name.findAll { it ==~ /[gs]et[A-Z].*/ && !(it =~ /MetaClass|Property|getClass/) }.sort()
try { r.id = 9 } catch (ReadOnlyPropertyException e) { println e.message }
println r.properties.keySet().sort()
println r.properties.sensor
// direct field access with .@
println r.@sensor
println r.@raw
s1
Re-assigned
via setter
[getId, getNoType, getSensor, setNoType, setSensor]
Cannot set read-only property: id for class: Reading
[class, id, label, noType, raw, sensor]
via setter
via setter
1.5

What the run shows:

  • Only the properties (sensor, id, noType) have accessors. The public field label and the private field raw have none, and a final property has a getter only, so assigning to it fails with ReadOnlyPropertyException.

  • obj.properties is a Map view built from the metaclass. It always contains class, and as the output shows it also lists fields, so use it for inspection and debugging rather than as a definition of what is a property. Passing it to another bean for copying is common but copies those entries too.

  • obj.@field bypasses the accessor and reads the field itself. Inside the class, this.field also reads the field directly, which is how a setter avoids calling itself.

  • A property may be static and may have an initial value. A static final member without an access modifier is still a property (a static, read-only one with a getter only). Only an explicit modifier, as in public static final, makes it a plain field:

class Config {
    static final String K = 'key' // static read-only property: getK() only
    static String s = 'shared'    // static property: getS() and setS()
    final String f = 'fixed'      // read-only property: getF() only
    public static final String C = 'const' // explicit modifier: a field, no accessors
}
println Config.methods*.name.findAll { it ==~ /[gs]et[A-Z].*/ && !(it =~ /MetaClass|Property|getClass/) }.sort()
println Config.K
Config.s = 'changed'
println Config.getS()
println new Config().getF()
[getF, getK, getS, setS]
key
changed
fixed

Custom accessors

Declaring a getter or setter by hand replaces the generated one. A property-style read of a method that has no field behind it, such as getSummary() below, is a computed (read-only) property.

class Thermostat {
    double target = 20

    // custom accessors replace the generated ones
    void setTarget(double t) {
        println "setTarget($t)"
        this.target = Math.min(t, 30)   // inside the class this.target uses the field directly
    }
    double getTarget() {
        println 'getTarget()'
        return target
    }
    // read-only computed property: getter without a field
    String getSummary() { "target=${this.@target}" }
}
def t = new Thermostat()
t.target = 45
println t.target
println t.summary
println t.@target
setTarget(45.0)
getTarget()
30.0
target=30.0
30.0

The Groovy style guide recommends writing explicit getters and setters only when they do something; otherwise leave the property alone. For boolean properties Groovy also generates an isFoo() getter alongside getFoo().

@Field in scripts

A script is compiled into a class whose run() method holds the statements. A variable declared with def or a type in a script is local to run() and invisible to methods declared in the same script. Annotating the declaration with groovy.transform.Field turns it into a field of the script class, which methods can then share.

import groovy.transform.Field

@Field List<String> events = []

def record(String e) { events << e }
record 'opened'
record 'closed'
println events
println this.class.superclass.name
println this.class.declaredFields*.name.findAll { !it.startsWith('$') && !it.startsWith('_') }
[opened, closed]
groovy.lang.Script
[events]

Without @Field, the method record would fail to find events at all. A variable assigned without def goes to the script binding instead, which is a different mechanism covered in Program Structure, Packages, Imports & Scripts.

GroovyBeans

A class whose state is exposed as properties is a GroovyBean, the Groovy form of a JavaBean. Besides the dotted syntax, a bean offers dynamic access by name, which is the basis of many builders, binders and templates.

class Plant {
    String name
    int daysToHarvest
}
class Zone {
    String id
    List<Plant> plants = []
}
def z = new Zone(id: 'A', plants: [new Plant(name: 'basil', daysToHarvest: 30), new Plant(name: 'mint', daysToHarvest: 45)])
// dynamic property names and subscript access
def prop = 'id'
println z."$prop"
println z['id']
println z.getProperty('id')
z.setProperty('id', 'B')
println z.id
// spread over the list of beans
println z.plants*.name
println z.plants.sum { it.daysToHarvest }
// a property is visible through the metaclass
println z.metaClass.properties*.name.sort()
println z.hasProperty('plants') != null
println z.hasProperty('nope')
A
A
A
B
[basil, mint]
75
[class, id, plants]
true
null

hasProperty(name) returns a MetaProperty or null, so the last line prints null. The spread operator and its null-safe companions are in Object Operators.

Named-argument construction

Every class without an explicit constructor gets the implicit no-argument constructor, and Groovy lets a call pass name: value pairs that are applied through the property setters right after construction. The names must be settable properties: an unknown name fails with MissingPropertyException. This is why many Groovy classes need no hand-written constructor at all; constructors that do take arguments are in Methods, Constructors & Parameters.

class Zone {
    String id
    int slots
    List<String> plants = []
    String toString() { "Zone($id, $slots, $plants)" }
}
// named-argument constructor: no-arg constructor then property setters
def z = new Zone(id: 'A', slots: 4, plants: ['basil'])
println z
// 'with' returns the closure result, 'tap' returns the receiver
def r1 = z.with { slots = 6; plants << 'mint'; 'ignored' }
println r1
def r2 = z.tap { slots = 8 }
println r2.is(z)
println z
// a plain Map can become an object with 'as' too
def z2 = [id: 'B', slots: 2] as Zone
println z2
// unknown name fails
try { new Zone(idd: 'x') } catch (MissingPropertyException e) { println e.message }
Zone(A, 4, [basil])
ignored
true
Zone(A, 8, [basil, mint])
Zone(B, 2, [])
No such property: idd for class: Zone
Possible solutions: id

with and tap both run a closure with the receiver as its delegate, so properties can be set by bare name. They differ only in what they return: with gives the closure’s last value (useful for computing something), and tap gives back the receiver (useful for a fluent configuration chain). Delegation rules are in Closure Delegation: Owner, Delegate & This. The @Canonical, @TupleConstructor and @Builder AST transformations generate richer constructors; see AST Transformations: Code Generation & Class Design.

The greenhouse model

The running scenario of this section uses these classes. Properties are drawn as attributes, and the two different relationships (a greenhouse owns zones; a zone only references the plants it holds) are shown with composition and aggregation.

classDiagram class Greenhouse { String name List zones Sensor newSensor(String id) } class Zone { String id int slots List plants } class Plant { String name int daysToHarvest double waterLitres } class Sensor { String id void report(double value) } Greenhouse "1" *-- "many" Zone : owns Greenhouse "1" *-- "many" Sensor : inner class Zone "1" o-- "many" Plant : holds

Where the book is outdated

The Manning book Groovy in Action (publisher page in the references) introduces fields, properties and GroovyBeans, and that explanation still holds. Check these points against the current documentation:

  • Methods and annotations added to the language and GDK after the book was written, such as with, tap, @Field and @PackageScope; use them as described above.

  • Inner and anonymous classes: check the current documentation rather than the book for what is supported, since the class model has been aligned with Java over the 3.x to 6.x releases.

  • Records (incubating) and sealed types are a newer alternative to hand-written value classes; see Records & Sealed Types.

  • Anything written about the default imports or the Java baseline (JDK 17 or later for Groovy 6.0): see What’s New & Migrating from Older Versions.

For the same ideas in other languages on this site compare Kotlin Classes & Objects, where properties are the default as well.