The Twisted Sisters of Overloadable Groovy Operators
Groovy operator overloading is fairly straightforward, but there are some oddities with the ++ and -- operators to be aware of. Also, never return "this". Ever.
Join the DZone community and get the full member experience.
Join For FreeGroovy supports operator overloading for a limited set of operators. Each supported operator corresponds to a particular method signature. If a type implements that method, then that operator is effectively overloaded for instances of that type.
The +
operator, for example, corresponds to the plus
method, enabling a.plus(b)
to be substituted with a + b
. And thanks to the wonders of polymorphism, a type can define multiple plus
methods to allow +
to behave differently depending on the type on the right hand side of the operator (e.g. [1] + 2
inserts 2
at the end of the list while [1] + [2]
joins the two lists together, resulting in [1, 2]
for both).
This approach to overloading seemed fairly straightforward until I recently discovered two sister operators that had some interesting twists: ++
and --
, which correspond to the next()
and previous()
methods, respectively.
Twist 1: Implicit Reassignment
The first twist is that when the ++
operator is used on a reference, it also reassigns that reference to the value returned by next()
. (The same applies to --
and previous()
.) This means that, unlike the other overridable Groovy operators, you cannot interchange a standalone statement of a++
with a.next()
, as shown by this code.
int i = 0
i++
assert i == 1
int j = 0
j.next() // 1 is returned by next(), but not assigned to j, keeping its value unchanged
assert j == 0
Twist 2: In Tandem Pre and Post Forms
Like Java, the placement of ++
or --
before or after a reference determines if the reassignment occurs before or after the operation is executed.
int i = 1
int j = ++i // i is incremented, then its value is assigned to j
int k = i++ // k is assigned the current value of i, then i is incremented
assert [i, j, k] == [3, 2, 2]
This holds true for other data types overriding ++
and --
, like java.util.Date
in the Groovy JDK.
def date = { int dayOfMonth -> new Date(2017 - 1900, Calendar.MAY, dayOfMonth).clearTime() }
Date d = date(1)
Date e = ++d
Date f = d++
assert [d, e, f].date == [3, 2, 2]
Unlike Java, however, Groovy lets you use the operator on both sides of the reference simultaneously (++a++
); the corresponding method is called twice, but not in chained succession. Confused? Perhaps the behavior best explained by this code.
class Foo {
int value
def next() {
println "next() called when value is $value"
new Foo(value + 1)
}
}
def f = new Foo(1)
++f++ // our wacky tandem usage of ++
println "The final value is $f.value"
When executed, it prints:
next() called when value is 1
next() called when value is 1
The final value is 2
(I'm using Groovy 2.4.7 on Java 1.8.0_111)
Twist 3: A Twist to the Twists
But before we file these caveats under the "good to know" category and move on with our lives, it's worth noting that we can override next()
and previous()
in such a way that it invalidates the aforementioned behaviors. Specifically, we can return this
from our method implementations.
@groovy.transform.TupleConstructor
class Bar {
int value
Bar next() {
++value
this // return this instance instead of a new Bar
}
}
As a consequence of returning a self-reference from our method, we cause the statements next()
and ++
to be perfectly interchangeable.
def b = new Bar(1)
b++
assert b.value == 2
b.next()
assert b.value == 3
Furthermore, we can use the in-tandem operators and get the expected result (insomuch as you can expect in-tandem operators to effect a result at all).
def b = new Bar(1)
++b++
assert b.value == 3
Is This
the Answer?
So are we to conclude that it's better to return this
than a new instance from our next()
and previous()
methods? Certainly not!
For starters, assuming that ++
or --
is supposed to change something about the object it's called on, returning this
must mean that the object's type is mutable. Who wants to write mutable data types these days?
Secondly, if we return this
from our method, we break the ++a
vs. a++
reassignment behavior, making the placement of the operator moot. Consider how our Bar
type compares to int
and java.util.Date
when applying ++
in its pre and post forms.
def b = new Bar(1)
def c = ++b
def d = b++
assert [b, c ,d].value == [3, 3, 3] // not [3, 2, 2] as with int and java.util.Date
All three Bar
references have the same value because they're all pointing to the same instance of Bar
.
Summary
Groovy operator overloading is fairly straightforward, but there are some oddities with the ++
and --
operators to be aware of. Given that 99% of all custom data types outside of numbers and iterators will have no reasonable semantics for the next()
and previous()
methods, and given that there is a 0% need to ever write a statement like ++a++
, you will likely never encounter these oddities. The important takeaway is that, on the rare occassions when you do decide to implement next()
and previous()
in order to override ++
and --
, never return this
.
Opinions expressed by DZone contributors are their own.
Comments