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 as sort() on Iterable. 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.collect is documented on Iterable, Object.with on Object, and Number.times on Number. 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 returns null while the GDK sort(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).

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
  • with returns whatever the block evaluates to, so it suits a computation on the receiver. tap ignores the block’s result and returns the receiver, so it chains and is the idiom for initialising a StringBuilder or a configuration object.

  • maybe?.with { ... } is safe on null: the block is skipped and the result is null.

  • each on a value that is not a collection treats it as a single element, so 5.each { } runs once. This is occasionally useful in code that accepts "a value or a list of values".

  • Under @CompileStatic the receiver type is known, so property accesses inside the block are checked; give the block a typed parameter or use @DelegatesTo in your own helpers (see Static Type Checking & @CompileStatic).

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 its toString().

  • dump() lists the object’s fields and their values (using reflection), which is helpful for classes that have no useful toString().

  • getProperties() (or .properties) returns the bean properties as a Map of name to value, without type information. It includes class.

  • is(other) compares identity, the equivalent of Java’s == on references. In Groovy == calls equals() (or compareTo() for Comparable objects), which is why a separate is exists.

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.

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.

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 ObservableList family, ConfigSlurper and Expando live 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.