Groovy Operator Overloading — Custom Operators for Cleaner Code
In this tutorial, you will learn about Groovy Operator Overloading. We cover key concepts, practical examples, and best practices to help you master this topic.
Groovy operator overloading maps operators like +, *, and [] to specific methods, enabling custom types with intuitive mathematical and collection syntax without boilerplate.
What You'll Learn
- Operator to method mapping
- Implementing custom operators
- Comparison and indexing operators
- Best practices for overloading
Why It Matters
Operator overloading makes domain types (vectors, matrices, money, dates) behave like built-in types. DodaZIP uses overloaded operators for combining archive paths and calculating compression ratios naturally.
Real-World Use
Mathematical libraries, vector/matrix operations, DSL design, unit conversion, and any domain where algebraic notation improves readability.
flowchart LR
A["Operators"] --> B["Arithmetic"]
A --> C["Comparison"]
A --> D["Indexing"]
A --> E["Conversion"]
B --> F["plus, minus, multiply"]
C --> G["compareTo, equals"]
D --> H["getAt, putAt"]
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
Operator-Method Mapping
| Operator | Method | Example |
|---|---|---|
a + b |
a.plus(b) |
x + y |
a - b |
a.minus(b) |
x - y |
a * b |
a.multiply(b) |
x * y |
a / b |
a.div(b) |
x / y |
a ** b |
a.power(b) |
x ** y |
a[b] |
a.getAt(b) |
list[0] |
a[b] = c |
a.putAt(b, c) |
map[key] = val |
a << b |
a.leftShift(b) |
list << item |
++a |
a.next() |
++count |
a == b |
a.equals(b) |
x == y |
a <=> b |
a.compareTo(b) |
x <=> y |
a as Type |
a.asType(Type) |
val as String |
Custom Vector Type
class Vector {
double x, y
Vector plus(Vector other) {
new Vector(x: x + other.x, y: y + other.y)
}
Vector minus(Vector other) {
new Vector(x: x - other.x, y: y - other.y)
}
Vector multiply(double scalar) {
new Vector(x: x * scalar, y: y * scalar)
}
double dot(Vector other) {
x * other.x + y * other.y
}
String toString() { "($x, $y)" }
}
def v1 = new Vector(x: 3, y: 4)
def v2 = new Vector(x: 1, y: 2)
println v1 + v2 // (4.0, 6.0)
println v1 - v2 // (2.0, 2.0)
println v1 * 2 // (6.0, 8.0)
println v1.dot(v2) // 11.0
Custom Money Type
class Money {
BigDecimal amount
String currency
Money plus(Money other) {
if (currency != other.currency) throw new IllegalArgumentException("Currency mismatch")
new Money(amount: amount + other.amount, currency: currency)
}
Money multiply(BigDecimal factor) {
new Money(amount: amount * factor, currency: currency)
}
int compareTo(Money other) {
amount <=> other.amount
}
boolean equals(Object other) {
if (!(other instanceof Money)) return false
amount == other.amount && currency == other.currency
}
String toString() { "$currency $amount" }
}
def prices = [
new Money(amount: 10.00, currency: "USD"),
new Money(amount: 5.00, currency: "USD"),
new Money(amount: 7.50, currency: "USD")
]
def total = prices[0] + prices[1] + prices[2]
println total // USD 22.50
Indexing with getAt/putAt
class Grid {
private data = [:]
int width, height
Grid(int w, int h) { width = w; height = h }
def getAt(int x, int y) {
data["$x,$y"]
}
void putAt(int x, int y, Object value) {
data["$x,$y"] = value
}
def getAt(Map coords) {
data["${coords.x},${coords.y}"]
}
}
def grid = new Grid(10, 10)
grid[3, 4] = "player"
grid[5, 2] = "enemy"
println grid[3, 4] // player
println grid[x: 5, y: 2] // enemy
println grid[0, 0] // null
leftShift Operator
class LogBuffer {
private entries = []
LogBuffer leftShift(String entry) {
entries << "[${new Date().format('HH:mm:ss')}] $entry"
this
}
String toString() { entries.join('\n') }
}
def log = new LogBuffer()
log << "Server started" << "User logged in" << "Request processed"
println log
Comparison Operators
class Priority implements Comparable<Priority> {
int level
String label
int compareTo(Priority other) {
level <=> other.level
}
boolean equals(Object other) {
if (!(other instanceof Priority)) return false
level == other.level
}
}
def low = new Priority(level: 1, label: "low")
def high = new Priority(level: 3, label: "high")
def critical = new Priority(level: 5, label: "critical")
println low < high // true
println high >= low // true
println critical > high // true
println [critical, low, high].sort()
// [low, high, critical]
asType Operator
class Temperature {
double celsius
def asType(Class target) {
if (target == Fahrenheit) {
return new Fahrenheit(fahrenheit: celsius * 9/5 + 32)
}
throw new IllegalArgumentException("Cannot convert to $target")
}
}
class Fahrenheit {
double fahrenheit
String toString() { "${fahrenheit}F" }
}
def temp = new Temperature(celsius: 100)
def f = temp as Fahrenheit
println f // 212.0F
Common Mistakes
1. Inconsistent equals and compareTo
If equals says two objects are equal, compareTo should return 0. Breaking this contract causes bugs in sorted collections.
2. Mutating objects in operators
plus should return a new instance, not modify this. Operators should be immutable by convention.
3. Forgetting to handle null
Operator methods receive null arguments. Check for null before dereferencing.
4. Overloading too many operators
Only overload operators where the meaning is obvious. matrix * matrix is clear. customer * product is not.
5. Unsupported operations
If subtract doesn't make sense for your type, don't implement minus. An error is better than a confusing result.
Practice Questions
1. What method does the << operator map to?
leftShift. Example: list << item calls list.leftShift(item).
2. How do you implement custom array indexing?
Define getAt and putAt methods. The parameters can be positional or use maps for named indices.
3. What must you implement for == to work correctly?
Override equals(Object) and hashCode(). Groovy's == always calls equals.
4. Can you overload ?: (Elvis) operator?
No. The Elvis operator cannot be overloaded in Groovy.
Challenge: Create a ComplexNumber class with full operator overloading for arithmetic and comparison.
FAQ
{{< faq question="Is operator overloading unique to Groovy?" >}} No. C++, Python, Kotlin, and Scala also support it. Java does not. {{< /faq >}}
{{< faq question="Does operator overloading work with @CompileStatic?" >}} Yes. The method calls are resolved at compile time based on the declared types. {{< /faq >}}
{{< faq question="Can I overload assignment (=) operator?" >}} No. The assignment operator cannot be overloaded in Groovy. {{< /faq >}}
{{< faq question="What is the performance impact of operator overloading?" >}} None. Operators are just method calls. The bytecode is identical to calling the method directly. {{< /faq >}}
{{< faq question="Can I change the precedence of operators?" >}} No. Operator precedence is fixed in the language and cannot be modified. {{< /faq >}}
Mini Project
Create a Duration class that supports arithmetic and comparison for time intervals.
class Duration {
long milliseconds
static Duration ofSeconds(long s) { new Duration(milliseconds: s * 1000) }
static Duration ofMinutes(long m) { new Duration(milliseconds: m * 60000) }
Duration plus(Duration other) {
new Duration(milliseconds: milliseconds + other.milliseconds)
}
Duration multiply(int times) {
new Duration(milliseconds: milliseconds * times)
}
int compareTo(Duration other) {
milliseconds <=> other.milliseconds
}
String toString() {
"${milliseconds / 1000}s"
}
}
def fiveSec = Duration.ofSeconds(5)
def twoMin = Duration.ofMinutes(2)
println fiveSec + twoMin // 125s
println twoMin * 3 // 360s
println fiveSec < twoMin // true
What's Next
Now that you understand operator overloading, proceed to Groovy scripting.
| Topic | Description | Link |
|---|---|---|
| Scripting | Scripts and bindings | {{< ref "25-scripting" >}} |
| AST transformations | Compile-time Metaprogramming | {{< ref "26-ast-transformations" >}} |
| Builders | Markup Builder patterns | {{< ref "09-builders" >}} |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro