Operator Overloading

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.0 (released 2026-09-22) running on JDK 26, and every example on it was run with that combination. Groovy has no special syntax for overloading: an operator is plain syntax for a method call, so a class takes part in a + b simply by having a public method called plus that accepts b. The built-in types work the same way. Their operators are GDK methods (plus on List, multiply on String, leftShift on collections), which is why the operators described in Operators: Arithmetic, Relational & Logical and in Object Operators can all be redefined for your own types.

The only requirements are that the method is public, has the right name and accepts the right number of arguments. The argument type decides what can appear on the right-hand side, and you can provide several overloads.

A first overload

A greenhouse tracks water in litres. A Volume class that supports + with another volume and with a plain number:

class Volume {
    final int litres
    Volume(int litres) { this.litres = litres }
    Volume plus(Volume other) { new Volume(litres + other.litres) }
    Volume plus(int extra)    { new Volume(litres + extra) }
    String toString() { "${litres}L" }
}
def a = new Volume(10)
def b = new Volume(4)
assert (a + b).litres == 14
assert (a + 5).litres == 15
println "${a + b} and ${a + 5}"
try { println(5 + a) } catch (e) { println "5 + a -> ${e.class.simpleName}" }
14L and 15L
5 + a -> MissingMethodException

Dispatch is on the left operand. a + 5 calls Volume.plus(int), but 5 + a calls Integer.plus(Volume), which does not exist. To support a number on the left you must add a method to Integer, with a category, an extension module or ExpandoMetaClass (see Metaclasses, ExpandoMetaClass & Categories and Extension Modules):

class Volume {
    final int litres
    Volume(int litres) { this.litres = litres }
    Volume plus(int extra) { new Volume(litres + extra) }
    String toString() { "${litres}L" }
}
class VolumeCategory { static Volume plus(Integer n, Volume v) { v + n } }
def v = new Volume(10)
use(VolumeCategory) { println(5 + v) }
15L

The operator to method table

All operators other than the comparison operators have a corresponding method. This is the complete list, checked against Groovy 6.0.0:

Operator Method Operator Method

a + b

a.plus(b)

a[b]

a.getAt(b)

a - b

a.minus(b)

a[b] = c

a.putAt(b, c)

a * b

a.multiply(b)

a in b

b.isCase(a)

a / b

a.div(b)

a as Type

a.asType(Type)

a % b

a.remainder(b)

a(...)

a.call(...)

a ** b

a.power(b)

a++

a.next()

a | b

a.or(b)

a--

a.previous()

a & b

a.and(b)

+a

a.positive()

a ^ b

a.xor(b)

-a

a.negative()

a << b

a.leftShift(b)

~a

a.bitwiseNegate()

a >> b

a.rightShift(b)

a == b

a.equals(b), or a.compareTo(b) == 0 if a is Comparable

a >>> b

a.rightShiftUnsigned(b)

a < b, a <= b, a > b, a >= b, a <=> b

a.compareTo(b)

Four points about the table:

  • % calls remainder. The operators chapter of the official documentation still lists mod in this table, while the Groovy 6.0 compound-assignment table (and the behaviour of 6.0.0, verified here) uses remainder. A class that defines only mod gets a MissingMethodException for %. Treat mod as out of date for the operator; the GDK method Integer.mod() still exists, and gives a non-negative result.

  • ++ and -- call next() and previous() and then rebind the variable to the returned value. Define them as methods that return a new object; mutating this and returning it would change every other reference to it.

  • !, && and || are not in the table, and neither are ?: and ?.. The Groovy truth decides what ! and && see; see "Operators that cannot be overloaded" below.

  • The comparison operators (<, <=, >, >=, <=>) all use compareTo; == uses equals, unless the class implements Comparable, in which case it uses compareTo.

Arithmetic, bitwise and unary operators

A complete value class that overloads most operators, and is Comparable and has equals/hashCode so that the comparison operators work:

class Volume implements Comparable<Volume> {
    final int litres
    Volume(int litres) { this.litres = litres }
    Volume plus(Volume o)   { new Volume(litres + o.litres) }
    Volume plus(int n)      { new Volume(litres + n) }
    Volume minus(Volume o)  { new Volume(litres - o.litres) }
    Volume multiply(int n)  { new Volume(litres * n) }
    Volume div(int n)       { new Volume(litres.intdiv(n)) }
    Volume remainder(int n) { new Volume(litres % n) }
    Volume power(int n)     { new Volume((litres ** n) as int) }
    Volume negative()       { new Volume(-litres) }
    Volume positive()       { this }
    Volume next()           { new Volume(litres + 1) }
    Volume previous()       { new Volume(litres - 1) }
    Volume leftShift(int n) { new Volume(litres << n) }
    Volume rightShift(int n){ new Volume(litres >> n) }
    Volume or(Volume o)     { new Volume(litres | o.litres) }
    Volume and(Volume o)    { new Volume(litres & o.litres) }
    Volume xor(Volume o)    { new Volume(litres ^ o.litres) }
    Volume bitwiseNegate()  { new Volume(~litres & 0xFF) }
    int compareTo(Volume o) { litres <=> o.litres }
    boolean equals(Object o) { o instanceof Volume && litres == o.litres }
    int hashCode() { litres }
    String toString() { "${litres}L" }
}
def a = new Volume(10), b = new Volume(4)
assert a + b == new Volume(14)
assert a + 5 == new Volume(15)
assert a - b == new Volume(6)
assert a * 3 == new Volume(30)
assert a / 3 == new Volume(3)
assert a % 3 == new Volume(1)
assert a ** 2 == new Volume(100)
assert -a == new Volume(-10) && +a === a
assert a++ == new Volume(10)        // postfix: the old value, then a = a.next()
assert a == new Volume(11)
assert --a == new Volume(10)
assert a << 1 == new Volume(20) && a >> 1 == new Volume(5)
assert (a | b) == new Volume(14) && (a & b) == new Volume(0) && (a ^ b) == new Volume(14)
assert ~a == new Volume(245)
// comparison operators come from compareTo
assert a > b && b < a && a >= a && a <= a && a != b
assert (a <=> b) == 1
assert [a, b].max() == a
println "${a + b}, ${a - b}, ${-a}, ${~a}"
14L, 6L, -10L, 245L

An implementation of Comparable is what makes the whole family of relational operators, sort(), max() and ranges work at once. If a class is Comparable and also provides next() and previous(), it can be used as a range endpoint: (new Volume(1)..new Volume(4)).toList() produces [1L, 2L, 3L, 4L]. Ranges are covered in Ranges.

Subscript, in, as and call

Four operators are about using an object as a container or a function.

  • a[b] calls getAt(b) and a[b] = c calls putAt(b, c). With several indices, a[b, c] calls getAt(b, c); a list index a[[b, c]] passes the list.

  • x in a is a.isCase(x), the same method that switch uses to decide whether a case label matches (see Control Structures).

  • a as T calls asType(T). Delegate to super.asType(c) for the types you do not handle. See Type Coercion & Promotion.

  • a(x) on an object calls a.call(x); this is how closures, and any other callable object, are invoked.

class Greenhouse {
    private final Map<String, BigDecimal> temps = [:]
    BigDecimal getAt(String zone) { temps[zone] }
    void putAt(String zone, BigDecimal value) { temps[zone] = value }
    boolean isCase(Object x) { x in temps.values() }    // used by `in` and switch
    Object asType(Class c) { c == List ? temps.keySet().sort() : super.asType(c) }
    String call(String zone) { "zone $zone: ${temps[zone]}" }
}
def g = new Greenhouse()
g['north'] = 21.5
g['south'] = 24.0
assert g['north'] == 21.5
assert 24.0 in g
assert !(30.0 in g)
assert (g as List) == ['north', 'south']
assert g('south') == 'zone south: 24.0'
switch (21.5) { case g: println 'a zone is at 21.5'; break; default: println 'no match' }
println g('north')

class Grid {
    def data = [:]
    def getAt(int row, int col) { data[[row, col]] }
    void putAt(List idx, v) { data[idx] = v }
    def getAt(List idx) { data[idx] }
}
def grid = new Grid()
grid[[1, 2]] = 'basil'
println grid[1, 2]
println grid[[1, 2]]
a zone is at 21.5
zone north: 21.5
basil
basil

Equality and comparison in detail

== is not Java’s ==. It calls equals null-safely, and falls back to compareTo when the left operand implements Comparable. This has a practical consequence for your own classes: a class that implements Comparable is compared by compareTo even if it does not override equals, and a class that overrides only equals is compared by it. The identity operators === and !== are not overloadable; they always call is().

class V implements Comparable<V> { int n; int compareTo(V o) { n <=> o.n } }
println(new V(n: 1) == new V(n: 1))      // no equals(): uses compareTo
class W { int n; boolean equals(o) { true } }
println(new W(n: 1) == new W(n: 2))      // only equals(): used as is
class OnlyMod { def mod(x) { 'mod' } }
class OnlyRem { def remainder(x) { 'remainder' } }
try { println(new OnlyMod() % 1) } catch (e) { println "OnlyMod: ${e.class.simpleName}" }
println(new OnlyRem() % 1)
true
true
OnlyMod: MissingMethodException
remainder

If you override equals, override hashCode too, as in Java, or hash-based collections will misbehave.

Compound assignment

By default a = b` is shorthand for `a = a + b`: it calls `plus`, which creates a new value, and rebinds the variable. This is right for immutable values and wasteful for mutable accumulators. Before Groovy 6.0 it was also impossible to use `= on a final variable.

Groovy 6.0 (GEP-15) lets a class define a dedicated *Assign method for each compound operator. When the receiver has a matching method, the operator calls it and no reassignment happens: the object is mutated in place and the value of the whole expression a = b` is the receiver `a`, not the return value of `plusAssign`. If no `*Assign+ method matches, the old a = a.plus(b) form still applies, so existing code keeps working.

Operator Assign method Fallback method

+=

plusAssign

plus

-=

minusAssign

minus

*=

multiplyAssign

multiply

/=

divAssign

div

%=

remainderAssign

remainder

**=

powerAssign

power

<<=

leftShiftAssign

leftShift

>>=

rightShiftAssign

rightShift

>>>=

rightShiftUnsignedAssign

rightShiftUnsigned

&=

andAssign

and

|=

orAssign

or

^=

xorAssign

xor

class WaterLog {
    List<Integer> entries = []
    int total = 0
    void plusAssign(int litres) { entries << litres; total += litres }   // mutates in place
    WaterLog plus(int litres) { new WaterLog(entries: entries + litres, total: total + litres) }
}
def log = new WaterLog()
def same = log
log += 5
log += 7
assert log.is(same)                 // no reassignment: still the same object
assert log.total == 12 && log.entries == [5, 7]
def r = (log += 1)
assert r.is(log)                    // value of the expression is the mutated receiver
println "total = ${log.total}"

// without plusAssign the old desugaring x = x.plus(y) still applies
class Plain { int n; Plain plus(int k) { new Plain(n: n + k) } }
def p = new Plain(n: 1); def p0 = p
p += 2
assert p.n == 3 && !p.is(p0)
println "plain: new object = ${!p.is(p0)}"
total = 13
plain: new object = true

Final variables and static compilation

Because a *Assign call does not reassign, the compiler can allow += on a final variable when the receiver type has a matching method and the compiler can resolve it, as it does under @CompileStatic or @TypeChecked. In purely dynamic code the choice is made at run time, so the final check cannot be relaxed. Running the examples on 6.0.0 showed these cases (the release notes describe the feature in general terms, so check the behaviour for your own shapes of code):

import groovy.transform.CompileStatic
class Tank { int litres; void plusAssign(int n) { litres += n } }

@CompileStatic
void topUp() {
    final Tank t = new Tank()
    t += 5                               // final local: accepted
    println "final local: ${t.litres}"
}
topUp()

@CompileStatic
class Station {
    final Tank tank = new Tank()
    void refill(Station other, int n) { other.tank += n }   // final property of another reference: accepted
}
def s = new Station()
s.refill(s, 3)
println "through another reference: ${s.tank.litres}"
final local: 5
through another reference: 3

In the same run, writing tank += n or this.tank += n inside the class that declares the final field was still rejected at compile time with cannot modify final field 'tank' outside of constructor. A final local without @CompileStatic fails with The variable [u] is declared final but is reassigned. Two further behaviours were observed: with a property as the left-hand side (holder.tank += 4), the setter is not called under @CompileStatic, as the release notes say, but in dynamic code the setter was still called (the receiver was mutated in place either way). Keep these differences in mind if a setter has side effects.

Renaming operator methods with @OperatorRename

Libraries often call the operation something other than Groovy does: add instead of plus, sub instead of minus, mult instead of multiply. Groovy 5.0 added the @OperatorRename AST transformation (GROOVY-10980) to bridge that without writing extension methods. It applies to methods, constructors and types, and maps an operator name to the method name the library really uses. The old method names keep working inside the annotated code. Since 6.0 it can also remap the *Assign names, for example @OperatorRename(plusAssign='addInPlace').

import groovy.transform.OperatorRename

// pretend this class comes from a library that does not follow Groovy's names
class Fraction {
    final int num, den
    Fraction(int n, int d) { num = n; den = d }
    Fraction add(Fraction o) { new Fraction(num * o.den + o.num * den, den * o.den) }
    Fraction sub(Fraction o) { new Fraction(num * o.den - o.num * den, den * o.den) }
    boolean equals(Object o) { o instanceof Fraction && num * o.den == o.num * den }
    int hashCode() { 1 }
    String toString() { "$num/$den" }
}

@OperatorRename(plus = 'add', minus = 'sub')
def mix() {
    def half = new Fraction(1, 2)
    def third = new Fraction(1, 3)
    assert half + third == new Fraction(5, 6)
    assert half - third == new Fraction(1, 6)
    assert half.add(third) == half + third      // the old name still works
    "${half + third}"
}
println mix()

try {
    evaluate '''
        def h = new Fraction(1, 2)
        h + h
    '''
} catch (e) { println "outside the annotation: ${e.class.simpleName}" }
5/6
outside the annotation: MissingMethodException

The effect is local to the annotated code, so + is unaffected anywhere else. This is the approach to prefer for matrix or numeric libraries whose names almost, but not quite, match Groovy’s.

Operators that cannot be overloaded

Some operators are not method calls:

  • &&, || and ! work on the Groovy truth of their operands. To make your object truthy or falsy, define asBoolean(); see Booleans & the Groovy Truth.

  • The Elvis operator ?: and its assignment ?=, the ternary operator and assignment = are control flow.

  • Identity === and !== always compare references.

  • Logical implication ==> (Groovy 5.0) is expanded by the compiler to !a || b, so a method named implies on your class is never called by it; the GDK method Boolean.implies exists for explicit calls only.

  • Member access operators such as .@ (field access) and ?. (safe navigation) are resolved by the compiler. Intercepting property access is a job for the metaobject protocol, not for operator overloading.

class Sensor { boolean ok; boolean asBoolean() { ok } }
assert !new Sensor(ok: false)
assert new Sensor(ok: true)
assert true.implies(false) == false
assert false.implies(true)
class Cond { def implies(x) { 'custom' } }
def c = new Cond()
println(c.implies(1))
println(c ==> true)
custom
true

Calling c.implies(1) runs the custom method, but c ==> true was evaluated as !c || true, which never touches it.

Guidelines

  • Overload an operator only when its meaning is obvious for the type: quantities, vectors, money, paths. A surprising + is worse than a named method.

  • Keep value types immutable and return new objects from plus, next and the like. Use *Assign only for types that are deliberately mutable accumulators.

  • Implement Comparable and equals/hashCode together.

  • Remember that dispatch is on the left operand: provide the symmetric case with an extension or category if you need it.

  • Under @CompileStatic the operator is resolved at compile time against the declared types, so declare parameter types precisely; see Static Type Checking & @CompileStatic.

  • Operator overloading is the basis of many DSLs; see Domain Specific Languages.

Where the book is outdated

Books on Groovy written before 5.0 cover the operator to method table and nothing beyond it. Since then, @OperatorRename (5.0) and the *Assign methods (6.0) have been added, the Groovy 6.0 behaviour of % is remainder, and logical implication is deliberately not overloadable.