Object Operators
|
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) and every example was run on Groovy 6.0.0 on
JDK 26, checked on 2026-10-14. It covers the operators that act on objects rather than on numbers or booleans: the ones
that navigate, reference, spread, index, compare identities and convert. Arithmetic, relational, logical, bitwise and
conditional operators (including the Elvis operator) are in
Operators: Arithmetic, Relational &
Logical, the regex operators ~, =~ and ==~ in
Regular Expressions, and how a class supplies its own
behaviour for them in Operator Overloading.
The examples use a small greenhouse model: sensors that may or may not be assigned to a zone.
class Zone { String name }
class Sensor { String id; Zone zone; Double reading; List<String> tags = [] }
Safe navigation
Reading a property of null normally fails with a NullPointerException. The safe navigation operator ?. returns
null for the whole expression when the receiver is null, and the rest of the chain is not evaluated, including the
arguments of a skipped method call.
def s1 = new Sensor(id: 's1', reading: 21.5d, zone: new Zone(name: 'north'))
def s2 = new Sensor(id: 's2') // no zone assigned
assert s1.zone.name == 'north'
assert s2.zone?.name == null // zone is null: the chain stops
try {
s2.zone.name // plain dot on null
assert false
} catch (NullPointerException e) {
println e.class.simpleName
}
assert s2.zone?.name?.toUpperCase() == null
assert s1.zone?.name?.toUpperCase() == 'NORTH'
def calls = 0
def next = { calls++; 'x' }
s2.zone?.name?.concat(next()) // the argument is never evaluated
assert calls == 0
s2.zone?.name = 'x' // an assignment through ?. is skipped too
assert s2.zone == null
NullPointerException
The operator guards only the link it is written on. In a.b?.c.d a null in b is safe, but a null returned by
c still fails on .d, so put ?. on every link that can be null. It is a shortcut for the usual
null-check, not a replacement for modelling absence; Optionality
compares it with Optional and the Elvis operator.
Direct field access
By default obj.id goes through the property accessor: a getter such as getId() if one exists. The direct field access
operator .@ bypasses accessors and reads or writes the field itself, even a private one.
class Valve {
String id = 'v1'
private boolean open = false
String getId() { 'ID-' + id } // inside the class, id is the field
}
def v = new Valve()
assert v.id == 'ID-v1' // goes through getId()
assert v.@id == 'v1' // direct field access, no getter
v.@id = 'raw'
assert v.@id == 'raw'
assert v.@open == false // reads a private field
try { v.@nothing } catch (MissingFieldException e) { println e.class.simpleName + ': ' + e.message }
MissingFieldException: No such field: nothing for class: Valve
Use .@ sparingly: it breaks the encapsulation the accessor provides and is mainly useful inside a getter or setter
that wants the stored value, and in tests or framework code. Properties and fields are explained in
Classes, Fields & Properties.
Method pointer
The method pointer operator .& takes a method of a particular object and turns it into a Closure (precisely a
MethodClosure). The method is looked up when the closure is called, not when the pointer is created, so overloads are
selected on the runtime argument types.
def text = 'Greenhouse'
def sub = text.&substring
assert sub(0, 5) == 'Green' // overloads chosen at call time
assert sub(5) == 'house'
assert sub instanceof Closure
assert sub.method == 'substring'
assert sub.owner.is(text)
def doubleIt(int n) { n * 2 }
assert [1, 2, 3].collect(this.&doubleIt) == [2, 4, 6] // a method as a closure argument
def describe(Integer i) { "int $i" }
def describe(String s) { "string $s" }
def d = this.&describe
assert d(1) == 'int 1'
assert d('a') == 'string a'
assert [4, 9].collect(Math.&sqrt) == [2.0d, 3.0d] // static methods work as well
def mk = String.&new // so does the constructor
assert mk('abc') == 'abc'
def name = 'toLowerCase'
assert 'ABC'.&"$name"() == 'abc' // the name may be a GString
def late = text.&noSuchMethod // no error yet
try { late() } catch (e) { println e.class.simpleName }
MissingMethodException
A method pointer is the way to hand an existing method to collect, each, sort or any API that takes a closure,
without wrapping it in { x -> obj.method(x) }. Because it is a normal closure it also supports currying and
composition, covered in Closures. When a pointer is created on a name
added at runtime through the metaclass, it finds that method too.
Method reference
The method reference operator :: arrived with the Parrot parser in Groovy 3.0. It names a method or constructor
where a Java functional interface is expected, so it is what you use with java.util.stream and the java.util.function
types. In dynamic Groovy it is an alias of the method pointer operator; under @CompileStatic it compiles to bytecode
similar to what Java produces. The four method forms and the constructor forms from Java are supported.
import java.util.function.Function
import java.util.stream.Stream
assert Stream.of('a', 'bb', 'ccc').map(String::length).toList() == [1, 2, 3] // class + instance method
assert Stream.of(1, 2, 3).map(String::valueOf).toList() == ['1', '2', '3'] // class + static method
assert Stream.of('1', '2').map(Integer::new).toList() == [1, 2] // constructor reference
def prefix = 'gh-'
Function<String, String> f = prefix::concat // bound to one object
assert f.apply('1') == 'gh-1'
def arr = Stream.of('a', 'b').toArray(String[]::new) // array constructor
assert arr.class == String[]
def m = 'abc'::length // no functional interface expected: a Closure, like .&
assert m instanceof Closure
assert m() == 3
Prefer .& in everyday Groovy code, where closures are first-class, and :: when the target type is a Java
functional interface or when the same code should read like its Java counterpart. The Java side of the comparison is in
Lambdas & Method References.
Spread operator
The spread-dot operator *. calls a method or reads a property on every element of an iterable and collects the
results in a list. It is shorthand for collect { it?.property }: a null element yields null instead of failing.
class Reading { String sensor; double celsius }
def readings = [new Reading(sensor: 'a', celsius: 20.5d), new Reading(sensor: 'b', celsius: 22.0d)]
assert readings*.sensor == ['a', 'b']
assert readings*.celsius*.intValue() == [20, 22] // chain several spreads
assert readings.sensor == ['a', 'b'] // a property on a list spreads implicitly
assert [readings[0], null]*.sensor == ['a', null] // null elements give null
def nothing = null
assert nothing*.sensor == null // a null receiver gives null
assert 'x'*.length() == [1] // a non-iterable counts as one element
assert [a: 1, b: 2]*.key == ['a', 'b'] // on a map it spreads over the entries
assert [a: 1, b: 2]*.value == [1, 2]
The result of *. is always a list in the order of the receiver, so readings*.celsius.sum() first spreads and
then sums. More on the collection methods behind it is in
Working with Collections; the implicit spread is the
basis of GPath, described in XML Parsing, GPath &
Security.
Spreading method arguments
In an argument list * in front of a list passes its elements as separate arguments. It may be mixed with ordinary
arguments and placed anywhere in the list.
def avg(double a, double b, double c) { (a + b + c) / 3 }
def args = [20.0d, 21.0d, 22.0d]
assert avg(*args) == 21.0d
def pair = [20.0d, 22.0d]
assert avg(*pair, 21.0d) == 21.0d
assert avg(21.0d, *pair) == 21.0d
def sum(int... xs) { xs.sum() }
assert sum(*[1, 2, 3]) == 6 // also fills a varargs parameter
Spreading list elements
Inside a list literal * inlines the elements of another collection or range.
def a = [1, 2]
assert [0, *a, 3] == [0, 1, 2, 3]
assert [*a, *a] == [1, 2, 1, 2]
assert [*1..3] == [1, 2, 3]
Spreading map elements
Inside a map literal *: inlines the entries of another map. Order matters: a key written after the spread
overrides the spread value, one written before is overridden by it.
def base = [unit: 'C', min: 0]
assert [max: 40, *:base] == [max: 40, unit: 'C', min: 0]
assert [*:base, unit: 'F'].unit == 'F' // later key wins
assert [unit: 'F', *:base].unit == 'C' // the spread comes later and wins
Range operator
.. creates an inclusive range, ..< one that excludes the upper bound. Ranges are List objects, they work with
any Comparable type that implements next() and previous(), and they are the argument of the subscript operator
for slicing. Integer ranges are groovy.lang.IntRange. Ranges have their own page,
Ranges.
def r = 1..5
assert r instanceof IntRange
println r.class.name
assert (1..<5).toList() == [1, 2, 3, 4]
assert (5..1).toList() == [5, 4, 3, 2, 1] // descending
assert ('a'..'e').toList() == ['a', 'b', 'c', 'd', 'e']
assert 3 in (1..5) // membership uses contains
println((1..10).step(3).toList())
groovy.lang.IntRange
[1, 4, 7, 10]
Spaceship operator
<=> delegates to compareTo and returns a negative number, zero or a positive number. It is mainly used to build comparators and multi-key sorts:
the Elvis operator ?: chains keys because 0 is false in the Groovy truth. null sorts before everything; null <=> null is 0.
assert (1 <=> 2) == -1
assert (2 <=> 2) == 0
assert ('b' <=> 'a') == 1
assert (null <=> 1) == -1
assert (1 <=> null) == 1
assert (null <=> null) == 0
def gh = [[n: 'b', t: 2], [n: 'a', t: 2], [n: 'c', t: 1]]
assert gh.sort { x, y -> x.t <=> y.t ?: x.n <=> y.n }*.n == ['c', 'a', 'b'] // by t, then by n
class Version implements Comparable<Version> {
int major, minor
int compareTo(Version o) { major <=> o.major ?: minor <=> o.minor }
String toString() { "$major.$minor" }
}
assert new Version(major: 1, minor: 2) <=> new Version(major: 1, minor: 10) == -1
assert [new Version(major: 2, minor: 0), new Version(major: 1, minor: 5)].sort()*.toString() == ['1.5', '2.0']
assert new Version(major: 1, minor: 2) < new Version(major: 2, minor: 0) // < and > use the same method
try { [1, 2] <=> [1, 3] } catch (e) { println e.class.simpleName }
IllegalArgumentException
A List is not Comparable, so two lists cannot be compared with <=>; compare the elements, or use a key
extraction as above. The relational operators <, <=, > and >= use the same compareTo, and so does the
sort() method without arguments.
Subscript and safe index
The subscript operator [] is shorthand for getAt on the right-hand side and putAt on the left. The GDK defines
them for lists, arrays, maps, strings, ranges and more. A negative index counts from the end, several indexes and
ranges can be combined, and writing past the end of a list grows it (padding with null).
def list = [10, 20, 30, 40]
assert list[1] == 20
assert list[-1] == 40
assert list[1..2] == [20, 30]
assert list[0, 2] == [10, 30]
assert list[0, 2..3] == [10, 30, 40]
list[1] = 21
list[4] = 50 // grows the list
list[7] = 1 // pads with nulls
assert list == [10, 21, 30, 40, 50, null, null, 1]
assert list[100] == null // reading past the end gives null
def m = [a: 1]
m['b'] = 2
assert m == [a: 1, b: 2]
assert 'greenhouse'[0..4] == 'green'
assert 'greenhouse'[-5..-1] == 'house'
try { 'abc'[10] } catch (e) { println e.class.simpleName }
StringIndexOutOfBoundsException
A class opts in by declaring getAt and putAt, which also makes the object usable on the right of a multiple
assignment (destructuring, see
Variables, Typing, def, var & val).
class Cell {
private final Map data = [:]
def getAt(String k) { data[k] }
void putAt(String k, v) { data[k] = v }
}
def c = new Cell()
c['t'] = 21
assert c['t'] == 21
class Point { int x, y; def getAt(int i) { [x, y][i] } }
def (px, py) = new Point(x: 3, y: 4)
assert px == 3 && py == 4
The safe index operator ?[], added in Groovy 3.0, is ?. for subscripts: on a null receiver a read returns
null and a write is silently ignored. ?[ is a single token, so no space may separate the ? and the [; and it
cannot start an expression.
List<Integer> maybe = null
assert maybe?[0] == null
maybe?[0] = 5 // quietly ignored
assert maybe == null
Map cfg = null
assert cfg?['unit'] == null
String[] array = ['a', 'b']
assert array?[1] == 'b'
Membership
in is isCase written as an operator, and !in is its negation. For a collection isCase means contains, but
the same method gives in a meaning for many other types: ranges, regex patterns, classes (is an instance of),
closures (the predicate holds) and so on. It is also what switch uses for its case labels, see
Control Structures.
def zones = ['north', 'south']
assert 'north' in zones
assert 'east' !in zones
assert 5 in (1..10)
assert 'a' in ~/a|b/ // Pattern
assert 'abc' in String // Class: instance of
assert !(null in String)
assert 3 in { it > 2 } // Closure: predicate
assert null in [null, 1]
def m = [a: 1]
println(('a' in m)) // a map tests its keys
println((1 in m))
true
false
Identity
In Groovy == calls equals (or compareTo for Comparable objects), unlike Java where it compares references. To
compare references use is, or, since Groovy 3.0, the identity operator === and its negation !==, equivalent to
Java’s == and != on objects.
def l1 = [1, 2]; def l2 = [1, 2]
assert l1 == l2 // equal content
assert l1 !== l2 // different objects
assert l1 === l1
assert l1.is(l1)
assert !l1.is(l2)
String s1 = 'abc'
def s3 = new String('abc')
println(s1 === s3)
assert 1 == 1L // == converts numbers
assert new BigDecimal('1.0') == new BigDecimal('1.00') // compareTo, scale is ignored
assert !new BigDecimal('1.0').equals(new BigDecimal('1.00'))
false
Which instances are shared (small boxed integers, interned strings) is a JVM detail, so do not use identity on
values. Equality, including equals versus compareTo, is explained in
Operators: Arithmetic, Relational &
Logical.
Coercion with as
Casting (Integer) '42' fails when the types are not assignable. The coercion operator as converts instead, using
the conversion rules of Groovy; unless the target is the same type it returns a new object, and it fails when no
rule fits. All the rules are described on
Type Coercion & Promotion.
assert ('42' as Integer) == 42
assert (42 as String) == '42'
assert (3.7 as int) == 3
assert ([1, 2, 2] as Set) == [1, 2] as Set
assert ([1, 2] as int[]).class == int[]
assert ('abc' as List) == ['a', 'b', 'c']
enum Mode { AUTO, MANUAL }
assert ('AUTO' as Mode) == Mode.AUTO
def r = { 'ran' } as Runnable // closure to an interface
def cmp = [compare: { a, b -> b <=> a }] as Comparator // map of closures to an interface
assert [1, 3, 2].sort(false, cmp) == [3, 2, 1]
try { 'abc' as Integer } catch (e) { println e.class.simpleName }
try { new Date() as Mode } catch (e) { println e.class.simpleName }
try { (Integer) '42' } catch (e) { println e.class.simpleName }
NumberFormatException
GroovyCastException
GroovyCastException
A class defines its own conversion by implementing asType(Class); the operator calls it.
class Identifiable { String name }
class User {
Long id
String name
def asType(Class target) {
if (target == Identifiable) {
return new Identifiable(name: name)
}
throw new ClassCastException("User cannot be coerced into $target")
}
}
def p = new User(name: 'Xavier') as Identifiable
assert p instanceof Identifiable
assert !(p instanceof User)
Diamond
The diamond <> exists for compatibility with Java 7 code: the generic arguments are inferred from the declaration.
In dynamic Groovy it has no effect; in statically checked Groovy it is optional because the checker infers the types
anyway. The documentation notes that, per GROOVY-12319, it may also be used when creating an anonymous inner class, as
in Java 9 (JEP 213).
List<String> a = new LinkedList<>()
Map<String, List<Integer>> m = new HashMap<>()
m.get('k', []) << 1
println a.class.name + ' ' + m
interface Processor<T> { T process(T val) }
Processor<String> p = new Processor<>() {
String process(String val) { val.toUpperCase() }
}
assert p.process('ok') == 'OK'
java.util.LinkedList [k:[1]]
Call operator
The call operator () on an expression invokes its call method, so any object that defines call can be used like
a function. Closures work this way, and so does a class that does not implement Callable. Overloads of call are
chosen as for any method, and under @CompileStatic the call is checked statically.
class Threshold {
double limit
boolean call(double reading) { reading > limit }
}
def t = new Threshold(limit: 30)
assert t(31)
assert !t(20)
assert t.call(31) // the explicit form
def c = { x -> x * 2 }
assert c(2) == 4 // closures define call, too
def handlers = [alarm: { 'ring' }]
assert handlers.alarm() == 'ring'
class Multi { def call(int x) { 'int' }; def call(String s) { 'str' } }
def mm = new Multi()
assert mm(1) == 'int' && mm('a') == 'str'
This is also how a DSL makes an object appear as a command, covered in Domain Specific Languages.
Quick reference
| Operator | Meaning | Equivalent |
|---|---|---|
|
null-safe property or method |
|
|
read or write field |
reflection on the field, no accessor |
|
method pointer, a |
|
|
method reference, since 3.0 |
alias of |
|
property or call on every element |
|
|
spread arguments, list elements, map entries |
|
|
inclusive or exclusive range |
|
|
three-way compare |
|
|
subscript, safe index since 3.0 |
|
|
membership |
|
|
reference identity, since 3.0 |
|
|
coercion |
|
|
diamond |
no effect at run time |
|
call operator |
|
Where the book is outdated
Books written for Groovy 2.x and earlier, such as the first edition of Groovy in Action (written for Groovy 1.0)
consulted for this section, predate several operators on this page: the safe index ?[], the method reference :: and the identity operators ===/!== all arrived with Groovy 3.0
(2020), so older text sends you to is() and to closures wrapping a method. The older operators behave as those books describe; the additions only give shorter spellings.
The Groovy 4.0 and later releases did not remove any operator discussed here. The publisher page of the book is linked
in the references below.