Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

The Twisted Sisters of Overloadable Groovy Operators

DZone's Guide to

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.

· Java Zone
Free Resource

Download Microservices for Java Developers: A hands-on introduction to frameworks and containers. Brought to you in partnership with Red Hat.

Groovy 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.

Download Modern Java EE Design Patterns: Building Scalable Architecture for Sustainable Enterprise Development.  Brought to you in partnership with Red Hat

Topics:
groovy ,operator overloading ,mutability ,java ,tutorial

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}