GDK Overview & Object Methods
|
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). The Groovy Development Kit (GDK) is the set of methods that Groovy adds to the classes of the JDK:
String.center, List.collect, File.text, Object.with and thousands more. The JDK classes are not modified. The
methods are looked up at run time (or compiled to direct calls under @CompileStatic) as if they belonged to the receiver,
which is why 'abc'.center(9, '*') reads like a normal method call. This page explains where those methods come from, how
to find them, and the methods defined on java.lang.Object, which therefore work on every value, followed by a few
utilities of the library that do not need an import: ConfigSlurper, Expando and the observable collections. Collections
have a page of their own, Working with Collections; files,
processes and dates are on Files I/O & Paths,
External Processes and
Dates & Times. Writing your own extension methods is on
Extension Modules, and the dispatch machinery behind all of this
on Runtime Metaprogramming & the MOP.
What the GDK is
An extension method is a static method whose first parameter is the receiver. The GDK declares them in a few classes of
the runtime: org.codehaus.groovy.runtime.DefaultGroovyMethods (the large general-purpose one, for Object, collections,
numbers and so on), StringGroovyMethods, IOGroovyMethods, NioGroovyMethods, ProcessGroovyMethods, and others, plus
and the extension modules of Groovy’s own modules (the date and time support for java.time is one). A module
descriptor tells the runtime which classes hold extension methods. Groovy 6.0 provides over 2000 extension methods for more
than 150 JDK classes.
// String.center() does not exist in the JDK: it is a GDK extension method
println String.declaredMethods.any { it.name == 'center' }
println 'ok'.center(8, '*')
def m = String.metaClass.getMetaMethods().find { it.name == 'center' && it.parameterTypes.size() == 2 }
println m.declaringClass.name // the receiver type that the method extends
println 'ok'.metaClass.respondsTo('ok', 'center').size() > 0
// the Object methods covered below, as the metaclass sees them
println(Object.metaClass.getMetaMethods()*.name.findAll { it in ['with', 'tap', 'inspect', 'dump', 'identity', 'is', 'sleep'] }.unique().sort())
println !5.respondsTo('times').isEmpty()
false
***ok***
java.lang.CharSequence
true
[dump, identity, inspect, is, sleep, tap, with]
true
String.declaredMethods does not list center, yet the call works: the metaclass finds the GDK method, whose receiver type
is CharSequence (the method applies to every CharSequence, String included). respondsTo sees it too, so reflection
helpers and DSL code can discover GDK methods like any other. The last line shows that with, tap, inspect, dump,
identity, is and sleep are all GDK methods on Object, which is the subject of the following sections.
Reading the GDK reference
The GDK reference at https://docs.groovy-lang.org/latest/html/groovy-jdk/ is generated from the extension classes and is
organised by the JDK class that is extended, not by the implementing class. Open the page of the receiver type (for
example java/lang/String.html, java/util/List.html, java/io/File.html) and look under its method summary. Three
points help when reading it:
-
The signature is shown from the receiver’s point of view: a method declared as
static <T> List<T> sort(Iterable<T> self)appears assort()onIterable. The same name often appears on several receivers (Iterable,Collection,List, arrays,Iterator,Map) with slightly different behaviour: look at the receiver that matches your variable. -
A method that is inherited by a type is listed on the page of its supertype.
List.collectis documented onIterable,Object.withonObject, andNumber.timesonNumber. When a method seems to be missing, check the supertypes (and interfaces) of the class. -
A JDK method with the same signature always wins over a GDK method of the same name. That is why
List.sort(Comparator)from the JDK returnsnullwhile the GDKsort(Closure)returns the list (see Closures).
The companion API reference at https://docs.groovy-lang.org/latest/html/api/ documents Groovy’s own classes
(groovy.lang, groovy.util, groovy.json and so on).
Source: Groovy GDK reference
with, tap and identity
with runs a closure with the receiver as the closure’s delegate (and as its single argument it) and returns the closure’s
result. tap does the same but returns the receiver, which makes it the tool for configuring an object in the middle of an
expression. identity(Closure) is another way to call the closure for the receiver. Names inside the block are resolved
against the receiver first, so its properties and methods can be written bare, as in a builder. The rules for delegate resolution are on
Closure Delegation: Owner, Delegate & This.
class Sensor { String id; String zone; double reading }
def s = new Sensor()
def r = s.with { // with: delegate is the receiver, result is the closure result
id = 'T-01'
zone = 'north'
reading = 21.5
"configured $id"
}
println r
def t = s.tap { reading += 1 } // tap: result is the receiver itself
println t.is(s)
println s.reading
def sb = new StringBuilder().tap { append('a'); append('b') }
println sb
println s.with { it.zone.toUpperCase() } // the receiver is also passed as it
Sensor maybe = null
println maybe?.with { id } // safe navigation skips the closure
println 5.identity { it + 1 } // identity is an alias of with
5.each { println "each on a single object: $it" } // Object.each treats a non-collection as one element
configured T-01
true
22.5
ab
NORTH
null
6
each on a single object: 5
-
withreturns whatever the block evaluates to, so it suits a computation on the receiver.tapignores the block’s result and returns the receiver, so it chains and is the idiom for initialising aStringBuilderor a configuration object. -
maybe?.with { ... }is safe on null: the block is skipped and the result isnull. -
eachon a value that is not a collection treats it as a single element, so5.each { }runs once. This is occasionally useful in code that accepts "a value or a list of values". -
Under
@CompileStaticthe receiver type is known, so property accesses inside the block are checked; give the block a typed parameter or use@DelegatesToin your own helpers (see Static Type Checking & @CompileStatic).
Source: Groovy GDK: java.lang.Object
inspect, dump, getProperties and is
Four methods answer "what is this object?":
-
inspect()returns the value as source-like text: strings are quoted, so the result can often be evaluated back. The default for a plain object is itstoString(). -
dump()lists the object’s fields and their values (using reflection), which is helpful for classes that have no usefultoString(). -
getProperties()(or.properties) returns the bean properties as aMapof name to value, without type information. It includesclass. -
is(other)compares identity, the equivalent of Java’s==on references. In Groovy==callsequals()(orcompareTo()forComparableobjects), which is why a separateisexists.
class Sensor { String id; String zone; double reading }
def s = new Sensor(id: 'T-01', zone: 'north', reading: 21.5)
println s.dump().replaceAll(/@[0-9a-f]+/, '@hash') // dump lists every field with its value
println 'a\tb'.inspect() // inspect gives source-like text
println 'a\tb'
println([1, 'two', 3.0, null, [k: 'v']].inspect())
println([1, 'two', 3.0, null, [k: 'v']].toString())
def text = [1, 'two', 3.0, null, [k: 'v']].inspect()
assert Eval.me(text) == [1, 'two', 3.0, null, [k: 'v']] // inspect output can be evaluated back
println 'round trip ok'
println s.properties.keySet().sort() // getProperties(): a Map of the bean properties
println s.properties.subMap(['id', 'zone'])
def twin = new Sensor(id: 'T-01', zone: 'north', reading: 21.5)
println s == twin // == calls equals(), which Sensor does not override
println s.is(s)
println s.is(twin) // is(): identity, never overridable
<Sensor@hash id=T-01 zone=north reading=21.5>
'a\tb'
a b
[1, 'two', 3.0, null, ['k':'v']]
[1, two, 3.0, null, [k:v]]
round trip ok
[class, id, reading, zone]
[id:T-01, zone:north]
false
true
false
The hash after @ in dump() is the identity hash and changes on every run, so the example above replaces it. Do not
parse the format of dump() or inspect() in program logic: they are diagnostic tools.
Source: Groovy GDK: java.lang.Object
sleep
sleep(ms) pauses the current thread and, unlike Thread.sleep, does not throw a checked exception, and it sleeps for
the whole time even if the thread is interrupted. The three-argument form sleep(ms, onInterrupt) takes a closure that is
called with the InterruptedException when the thread is interrupted; the closure returns true to end the sleep early, or
false to keep sleeping.
def started = System.nanoTime()
sleep(200)
def ms = (System.nanoTime() - started).intdiv(1_000_000)
println ms >= 200
def t = Thread.start {
sleep(60_000) { e -> println "interrupted: ${e.class.simpleName}"; true } // true: stop sleeping
}
sleep(100)
t.interrupt()
t.join()
println 'done'
true
interrupted: InterruptedException
done
Prefer a scheduler or a CompletableFuture to sleeping in production code; sleep is for scripts, demos and tests. The
concurrency pages cover better primitives, from Async/Await onwards.
Source: Groovy GDK: java.lang.Object
ConfigSlurper
ConfigSlurper parses a Groovy-syntax configuration script into a ConfigObject, a map with property access, nested
blocks and optional per-environment sections. Nested blocks become nested maps; a read of a key that does not exist
returns an empty ConfigObject (which is falsy) instead of null, so use isSet, containsKey or the Groovy truth to
test for presence. The constructor takes an environment name, and an environments { ... } block in the script is
resolved against it.
def config = new ConfigSlurper('production').parse('''
greenhouse {
name = 'North Hall'
sampleSeconds = 30
limits {
minTemp = 12.0
maxTemp = 30.0
}
}
environments {
development { greenhouse.sampleSeconds = 5 }
production { greenhouse.sampleSeconds = 60 }
}
''')
println config.greenhouse.name
println config.greenhouse.sampleSeconds
println config.greenhouse.limits.maxTemp
println config.flatten()['greenhouse.limits.minTemp']
println config.greenhouse.limits.containsKey('minTemp')
println config.greenhouse.missing instanceof ConfigObject
println config.isSet('nothing')
println config.greenhouse.limits.keySet().toList()
println config.toProperties().getProperty('greenhouse.name')
def dev = new ConfigSlurper('development').parse("""
greenhouse { sampleSeconds = 30 }
environments { development { greenhouse.sampleSeconds = 5 } }
""")
println dev.greenhouse.sampleSeconds
North Hall
60
30.0
12.0
true
true
false
[minTemp, maxTemp]
North Hall
5
flatten() turns the nested structure into dotted keys, and toProperties() converts it to java.util.Properties, which
suits code that already reads properties files. The result of parsing is plain data: a script that is not under your control
should be treated like any other untrusted Groovy code, see
Compilation Customizers & Secure Scripting.
Expando
An Expando is a dynamic bag of properties: assign any name and it exists. A closure assigned to a name becomes a method of
the expando and has the expando as its delegate. Reading a name that was never assigned returns null. Two caveats come
from closure resolution. Write delegate.field for the expando’s own state inside such a closure, because a bare name is
resolved against the owner (the surrounding script) first. And == on two expandos compares all their properties.
def zone = new Expando(id: 'north', targetTemp: 21.0)
zone.describe = { -> "zone ${delegate.id} target ${delegate.targetTemp}" }
zone.raise = { double d -> delegate.targetTemp += d; delegate }
println zone.describe()
zone.raise(1.5).raise(0.5) // raise returns the Expando, so calls chain
println zone.targetTemp
println zone.nothing // unknown names read as null, never an exception
println zone.properties.keySet().sort()
zone.toString = { -> "Zone(${delegate.id})" }
println zone
println "$zone"
println zone == new Expando(id: 'north')
zone north target 21.0
23.0
null
[describe, id, raise, targetTemp]
Zone(north)
Zone(north)
false
An Expando is handy for test doubles and for objects whose shape is only known at run time. When the shape is known,
a class, a record or a map is clearer, and Expando carries no type information for the static checker.
Observable lists, maps and sets
ObservableList, ObservableMap and ObservableSet (package groovy.util) wrap or extend a collection and fire a
java.beans.PropertyChangeEvent for each change. The event subclasses tell what happened: ElementAddedEvent,
ElementUpdatedEvent, ElementRemovedEvent, ElementClearedEvent and ElementMultiAddEvent for lists and sets;
PropertyAddedEvent, PropertyUpdatedEvent, PropertyRemovedEvent and PropertyClearedEvent for maps. A companion event
named size is fired as well, so the listener below ignores it.
def log = []
def alarms = new ObservableList()
alarms.addPropertyChangeListener { e ->
if (e.propertyName == 'size') return // ignore the companion size event
switch (e) {
case ObservableList.ElementAddedEvent: log << "added ${e.newValue} at ${e.index}"; break
case ObservableList.ElementUpdatedEvent: log << "updated ${e.oldValue} -> ${e.newValue}"; break
case ObservableList.ElementRemovedEvent: log << "removed ${e.oldValue}"; break
}
}
alarms << 'frost'
alarms << 'wind'
alarms[0] = 'hard frost'
alarms.remove('wind')
println log
println alarms
def readings = new ObservableMap()
readings.addPropertyChangeListener { e ->
if (e.propertyName != 'size') println "${e.class.simpleName}: ${e.propertyName} ${e.oldValue} -> ${e.newValue}"
}
readings.t1 = 20.5
readings.t1 = 21.0
readings.remove('t1')
def tags = new ObservableSet(['a'] as Set)
tags.addPropertyChangeListener { e ->
if (e.propertyName != 'size') println "${e.class.simpleName}: ${e.newValue ?: e.oldValue}"
}
tags << 'b'
tags << 'b' // already present: no event
tags.remove('a')
[added frost at 0, added wind at 1, updated frost -> hard frost, removed wind]
[hard frost]
PropertyAddedEvent: t1 null -> 20.5
PropertyUpdatedEvent: t1 20.5 -> 21.0
PropertyRemovedEvent: t1 21.0 -> null
ElementAddedEvent: b
ElementRemovedEvent: a
Listeners are called synchronously on the mutating thread, so keep them short. These classes are not thread safe on their own; see Parallel Collections & Dataflow for concurrent alternatives. Adding an element that a set already contains fires no event.
Custom display with groovyToString (6.0)
Groovy 6.0 introduces a groovyToString() protocol (GROOVY-11893). When a class defines a groovyToString() method that returns
a String, Groovy uses it instead of toString() for display in string interpolation, println, inspect and
collection formatting, while toString() remains what Java code sees. The GDK also supplies groovyToString for collections, maps,
ranges and primitive arrays, which is why a int[] prints as [1, 2, 3] in Groovy and not as [I@hash.
class Reading {
String zone
double celsius
String toString() { "Reading[zone=$zone, celsius=$celsius]" }
String groovyToString() { "$zone: $celsius C" }
}
def r = new Reading(zone: 'north', celsius: 21.5)
println r // println uses groovyToString
println "$r"
println([north: r])
println r.toString() // the real toString() is untouched
println r.inspect()
println([1, 2, 3] as int[]) // Java would print [I@hash
println(1..4)
north: 21.5 C
north: 21.5 C
[north:north: 21.5 C]
Reading[zone=north, celsius=21.5]
north: 21.5 C
[1, 2, 3]
1..4
That makes it possible to keep a technical toString() for logs and Java interop and still give scripts, groovysh and
tests a friendlier rendering. The protocol is new in 6.0, so code that must run on Groovy 5 or earlier cannot rely on it.
Selectively disabling extension methods (6.0)
A GDK method can clash with a library that adds a method of the same name to its own types: Eclipse Collections' groupBy
returns a multimap, Groovy’s returns a map. The system property groovy.extension.disable lists extension methods that
Groovy must not apply. Before 6.0 naming a method disabled every overload. In 6.0 an entry can also name a receiver type or a
full parameter signature (GROOVY-11892): groupBy disables all groupBy overloads, groupBy,countBy disables both methods,
and the release notes document forms such as groupBy(MutableList) or groupBy(Iterable,Closure) that disable a single overload.
The script below runs unchanged three times with different settings (the property must be set when the JVM starts, here
through JAVA_OPTS of the groovy command):
def attempt = { String label, Closure c ->
try { println "$label: ${c()}" } catch (MissingMethodException e) { println "$label: MissingMethodException" }
}
attempt('list groupBy') { [1, 2, 3, 4].groupBy { it % 2 } }
attempt('set groupBy') { ([1, 2, 3, 4] as Set).groupBy { it % 2 } }
attempt('map groupBy') { [a: 1, b: 2, c: 3].groupBy { it.value % 2 } }
attempt('center') { 'abc'.center(7, '*') }
$ groovy extensions.groovy
list groupBy: [1:[1, 3], 0:[2, 4]]
set groupBy: [1:[1, 3], 0:[2, 4]]
map groupBy: [1:[a:1, c:3], 0:[b:2]]
center: **abc**
$ JAVA_OPTS="-Dgroovy.extension.disable=groupBy" groovy extensions.groovy
list groupBy: MissingMethodException
set groupBy: MissingMethodException
map groupBy: MissingMethodException
center: **abc**
$ JAVA_OPTS="-Dgroovy.extension.disable=groupBy,center" groovy extensions.groovy
list groupBy: MissingMethodException
set groupBy: MissingMethodException
map groupBy: MissingMethodException
center: MissingMethodException
Disabling by method name worked as documented. The per-receiver and per-signature forms from the release notes
(groupBy(Iterable,Closure), groupBy(Map) and similar spellings) did not change the behaviour of this script in Groovy
6.0.0 when tried, so test them against your own classes before depending on them. The setting affects the whole JVM, including
libraries that use the disabled method through Groovy, so prefer the narrowest form that works.
Where the book is outdated
The Manning book Groovy in Action (publisher page in the references) presents the GDK and its Object methods well, but
check these points against the current documentation:
-
The count of GDK methods and the list of receiver classes have grown with every release; Groovy 5.0 added about 350 methods and 6.0 adds more.
-
groovyToString, selective disabling of extension methods and most of the lazy iterator methods did not exist when the book was written. -
Tooling and packaging moved: the
ObservableListfamily,ConfigSlurperandExpandolive in the main Groovy jar, but several modules the book assumed in the core are separate artifacts today (see Build Tools: Gradle, Maven & Ant). -
Baselines: Groovy 6.0 needs JDK 17 or later.