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 whydays.class.nameisjava.lang.Integer. The effect on arithmetic and overflow is described in Numbers & Numeric Literals. -
Types are optional.
defand an omitted type both meanObject; 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,
extendsandimplementswork 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. Thepublicfieldlabeland theprivatefieldrawhave none, and afinalproperty has a getter only, so assigning to it fails withReadOnlyPropertyException. -
obj.propertiesis aMapview built from the metaclass. It always containsclass, 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.@fieldbypasses the accessor and reads the field itself. Inside the class,this.fieldalso reads the field directly, which is how a setter avoids calling itself. -
A property may be
staticand may have an initial value. Astatic finalmember without an access modifier is still a property (a static, read-only one with a getter only). Only an explicit modifier, as inpublic 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.
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,@Fieldand@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.