Overriding operators

1+1 is just a convenient way of writing 1.plus(1). This is achieved by class

Integer having an implementation of the plus method. This convenient feature is also available for other operators.

You can easily use any of these operators with your own classes. Just implement the respective method. Unlike in Java, there is no need to implement a specific interface.

Method-based operators

Operator Name Method Works with
a + b Plus a.plus(b) Number, string, collection
a-b Minus a.minus(b) Number, string, collection
a*b Star a.multiply(b) Number, string, collection
a/b Divide a.div(b) Number
a%b Module a.mod(b) Integral number
a++ Post increment a.next() Number, string, range
++a Pre increment
a– Post decrement a.previous() Number, string, range
–a Pre decrement
a**b Power a.power(b) Number
a | b Numerical or a.or(b) Integral number
a & b Numerical and a.and(b) Integral number
a ^ b Numerical xor a.xor(b) Integral number
~ a Bitwise compliment a.negate() Integral number, string (the

latter returning a regular

expression pattern)

 

a [b] Subscript a.getAt(b) Object, list, map,

String

,

Array

 

a [b]=c Subscript assignment a.putAt(b, c) Object, list, map,

StringBuffer

,

Array

 

a << b Left shift a.leftShift(b) Integral number, also used like

“append” to

StringBuffers,

Writers, Files,

Sockets, Lists

a >>b Right shift a.rightShift(b) Integral number
a >>> b Right shift unsigned a.rightShiftUnsigned(b) Integral number
switch(a){

case b:

}

 

Classification b.isCase(a) Object, range, list,

collection, pattern, closure;

also used with collection c in c.grep(b), which returns all items of c where b.isCase(item)

 

a==b Equals a.equals(b) Object; consider

hashCode()

a

 

a !=b Not Equal ! a.equals(b) Object
a <=> b Spaceship a.compareTo(b) java.lang.Comparable
a > b Greater than a.compareTo(b) > 0
a >= b Greater than or equal to a.compareTo(b) >= 0
a < b Less than a.compareTo(b) < 0
a <= b Less than or equal to a.compareTo(b) <= 0
a as type Enforced correction a.asType(typeClass) Any type

This is all good in theory, but let’s see how they work in practice.

Overridden operators in action:

We implement equals such that it copes with null comparison. This is Groovy style. The default implementation of the equals operator doesn’t throw any NullPointerExceptions either. Remember that == (or equals) denotes object equality (equal values), not identity (same object instances).

Operator override

Bellow code demonstrates an implementation of the equals == and plus + operators for a Money class. We allow money of the same form of currency to be added up but do not support multicurrency addition.

We implement equals such that it copes with null comparison. This is Groovy style. The default implementation of the equals operator doesn’t throw any NullPointerExceptions either. Remember that == (or equals) denotes object equality (equal values), not identity (same object instances).

Code:

class Money {

            private int amount

            private String currency

            Money (amountValue, currencyValue) {

                        amount = amountValue

                        currency = currencyValue

            }

            boolean equals (Object other) {                                              (override == operator)

                        if (null == other) return false

                        if (! (other instanceof Money)) return false

                        if (currency != other.currency) return false

                        if (amount != other.amount) return false

                        return true

            }

            int hashCode() {

                        amount.hashCode() + currency.hashCode()

            }

            Money plus (Money other) {                                                    (implement + operator)

                        if (null == other) return null

                        if (other.currency != currency) {

                                    throw new IllegalArgumentException(

                                    "cannot add $other.currency to $currency")

                                    }

                        return new Money(amount + other.amount, currency)

                        }

            }

            def buck = new Money(1, 'USD')

            assert buck

            assert buck == new Money(1, 'USD')                                     (use overhidden ==)

            assert buck + buck == new Money(2, 'USD')                        (use overhidden +)

Overriding  equals is straightforward, as we show at override == operator. We also provide a hashCode method to make sure equal Money objects have the same hashcode. This is required by Java’s contract for java.lang.Object. The use of this operator is shown at use overhidden== where one dollar becomes equal to any other dollar.

At  Implement + operator, the plus operator is not overridden in the strict sense of the word, because there is no such operator in Money’s superclass (Object). In this case, operator implementing is the best wording. This is used at Use overhidden+, where we add two Money objects.

To explain the difference between overriding and overloading, here is a possible overload for Money’s plus operator. In case, we would like to add Money as

            assert buck + 1 == new Money(2, 'USD')

We can provide the additional method

  Money plus (Integer more) {

                        return new Money(amount + more, currency)

            }

that overloads the plus method with a second implementation that takes an Integer parameter. The Groovy method dispatch finds the right implementation at runtime.

This example leads to the general issue of how to deal with different parameter types when implementing an operator method.

Making coercion work for you:

Implementing operators is straightforward when both operands are of the same

type. Things get more complex with a mixture of types, say

            1 + 1.0

This adds an Integer and a BigDecimal. One of the two arguments needs to be promoted to the more general type. This is called coercion.

When implementing operators, there are three main issues to consider as part of coercion.

Supported argument types

You need to decide which argument types and values will be allowed. If an operator must take a potentially inappropriate type, throw an IllegalArgumentException where necessary. For instance, in our Money example, even though it makes sense to use Money as the parameter for the plus operator, we don’t allow different currencies to be added together.

Promoting more specific arguments

If the argument type is a more specific one than your own type, promote it to your type and return an object of  your type. To see what this means, consider how you might implement the plus operator if you were designing the BigDecimal class, and what you’d do for an Integer argument. Integer is more specific than BigDecimal: Every Integer value can be expressed as a BigDecimal, but the reverse isn’t true. So for the BigDecimal. plus(Integer)operator, we would consider promoting the Integer to BigDecimal,performing the addition, and then returning another BigDecimal even if the result could accurately be expressed as an Integer.

Handling more general arguments with double dispatch

f the argument type is more general, call its operator method with yourself (“this,” the current object) as an argument. This is also called double dispatch,and it helps to avoid duplicated, asymmetric, possibly inconsistent code. Let’s reverse our previous example and consider Integer.plus (BigDecimal operand). We would consider returning the result of the expression operand.plus(this), delegating the work to BigDecimal’s plus(Integer) method. The result would be a BigDecimal, which is reasonable—it would be odd for 1+1.5 to return an Integer but 1.5+1 to return a BigDecimal. Of course, this is only applicable for commutative operators.

Groovy’s conventional behavior

Groovy’s general strategy of coercion is to return the most general type. Other languages such as Ruby try to be smarter and return the least general type that can be used without losing information from range or precision. The Ruby way saves memory at the expense of processing time. It also requires that the language promote a type to a more general one when the operation would generate an overflow of that type’s range. Otherwise, intermediary results in a complex calculation could truncate the result.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.