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

a?.b

null-safe property or method

a == null ? null : a.b

a.@f

read or write field f directly

reflection on the field, no accessor

a.&m

method pointer, a Closure

{ args -> a.m(*args) }

T::m

method reference, since 3.0

alias of .& in dynamic code

a*.b

property or call on every element

a.collect { it?.b }

f(*xs), [*xs], [*:m]

spread arguments, list elements, map entries

x..y, x..<y

inclusive or exclusive range

new IntRange(true, x, y) for ints

a <=> b

three-way compare

a.compareTo(b)

a[i], a[i] = v, a?[i]

subscript, safe index since 3.0

getAt, putAt

x in c, x !in c

membership

c.isCase(x)

a === b, a !== b

reference identity, since 3.0

a.is(b)

x as T

coercion

x.asType(T)

new C<>()

diamond

no effect at run time

f(x) on an object

call operator

f.call(x)

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.