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 |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Four points about the table:
-
%callsremainder. The operators chapter of the official documentation still listsmodin this table, while the Groovy 6.0 compound-assignment table (and the behaviour of 6.0.0, verified here) usesremainder. A class that defines onlymodgets aMissingMethodExceptionfor%. Treatmodas out of date for the operator; the GDK methodInteger.mod()still exists, and gives a non-negative result. -
++and--callnext()andprevious()and then rebind the variable to the returned value. Define them as methods that return a new object; mutatingthisand 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 usecompareTo;==usesequals, unless the class implementsComparable, in which case it usescompareTo.
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]callsgetAt(b)anda[b] = ccallsputAt(b, c). With several indices,a[b, c]callsgetAt(b, c); a list indexa[[b, c]]passes the list. -
x in aisa.isCase(x), the same method thatswitchuses to decide whether acaselabel matches (see Control Structures). -
a as TcallsasType(T). Delegate tosuper.asType(c)for the types you do not handle. See Type Coercion & Promotion. -
a(x)on an object callsa.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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
Source: Groovy 6.0 release notes: Compound assignment operator overloading, Groovy documentation: Compound assignment operator overloading
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, defineasBoolean(); 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 namedimplieson your class is never called by it; the GDK methodBoolean.impliesexists 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
Source: Groovy documentation: Customizing the truth with asBoolean() methods, Groovy 5.0 release notes: A new logical implication operator
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,nextand the like. Use*Assignonly for types that are deliberately mutable accumulators. -
Implement
Comparableandequals/hashCodetogether. -
Remember that dispatch is on the left operand: provide the symmetric case with an extension or category if you need it.
-
Under
@CompileStaticthe 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.