Search This Blog

Loading...

Wednesday, April 28, 2010

Groovy features, part 3 - method gotchas

Table of contents




Foreword



This is a regular article produced on Groovy exploration way. We'll talk about Groovy method calls processing here.

Previous part may be found here - Groovy features, part 2 - java beans



Operator-like methods


Groovy offers convenient call syntax for predefined set of methods. It looks like operator appliance at source code level but is compiled to a method call. Example:


Integer i = 1;
Integer j = 2;
println i.class
println j.class
println i + j


Output:


class java.lang.Integer
class java.lang.Integer
3


We can see that it looks like '+' operator is applied to two objects of type 'java.lang.Integer', howver, it's transformed to call 'i.plus(j)'. I.e. Groovy automatically converts '+' defined at source level to invocation of 'plus()' method at runtime.

That means that it's possible to declare such a method at custom class and use operator-like syntax for it:


class FirstNumberWrapper {
Number i

def plus(j) {
println "FirstNumberWrapper.plus($j) is called"
new FirstNumberWrapper(i: i + j)
}

String toString() {"'wrapper for $i'"}
}

class SecondNumberWrapper {
Number i

String toString() {"'wrapper for $i'"}
}

first = new FirstNumberWrapper(i: 1)
second = new SecondNumberWrapper(i: 2)

println "Adding 3 to first wrapper ($first)..."
first += 3
println "done. Result: $first"

println "Adding 3 to second wrapper ($second)..."
second += 3


Output:

Adding 3 to first wrapper ('wrapper for 1')...
FirstNumberWrapper.plus(3) is called
done. Result: 'wrapper for 4'
Adding 3 to second wrapper ('wrapper for 2')...
Caught: groovy.lang.MissingMethodException: No signature of method: SecondNumberWrapper.plus() is applicable for argument types: (java.lang.Integer) values: [3]
at GroovyTestScript.run(GroovyTestScript.groovy:26)


Here is a complete list of method name - operator sign mappings:


















































































































OperatorMethod
a + ba.plus(b)
a - ba.minus(b)
a * ba.multiply(b)
a / ba.div(b)
a % ba.mod(b)
a++; ++aa.next()
a--; --aa.previous()
a ** ba.power(b)
a | ba.or(b)
a & ba.and(b)
a ^ ba.xor(b)
~aa.negate()
a[b]a.getAt(b)
a[b] = ca.putAt(b, c)
a << ba.leftShift(b)
a >> ba.rightShift(b)
a >>> ba.rightShiftUnsigned(b)
switch (a) { case b: }b.isCase(a)
a == ba.equals(b)
a != b!a.equals(b)
a <=> ba.compareTo(b)
a > ba.compareTo(b) > 0
a >= ba.compareTo(b) >= 0
a < ba.compareTo(b) < 0
a <= ba.compareTo(b) <= 0
a as typea.asType(typeClass)


Groovy Equals


I want to pay special attention to ' == ' operator processing - it is automatically mapped to 'equals()' call. You should use is() method to compare objects by identity:


s1 = "xxx"
s2 = new String("xxx")
s3 = s2

println s1 == s2 // true
println s1.is(s2) // false

println s2 == s3 // true
println s2.is(s3) // true


  • Multimethods;

  • Java selects target overloaded method at compile-time, hence, it looks at static reference type. Consider the following example:


    class GeneralItem {}
    class SubItem extends GeneralItem {}

    class BaseService {
    public void test(Object data) {
    System.out.println("BaseService.test()");
    }
    }

    class SubService extends BaseService {
    public void test(String data) {
    System.out.println("SubService.test()");
    }
    }

    public class JavaTest {
    public static void main(String[] args) {
    BaseService subService = new SubService();
    subService.test(""); // prints 'BaseService.test()'
    }
    }



    Here actual object type is 'SubService' and argument type is 'String'. However, BaseService.test() is called because static type of 'SubService' reference is 'BaseService', hence, algorithm that resolves overloaded method to use selects 'BaseService.test()'.

    We can perform the same test in Groovy and see that 'SubService.test()' is printed. The reason is that Groovy resolves all method calls in runtime, hence, it checks both actual argument types and target reference type. This is really cool!

  • Overloaded operators;

  • Multimethods allow to do the following nice trick - it's possible to define overloaded versions of base operator methods. Necessary version is automatically chose in runtime:


    class NumberWrapper {
    def number;

    def multiply(Number number) {
    println 'muliply(Number)'
    new NumberWrapper(number: this.number * number)
    }

    def multiply(NumberWrapper wrapper) {
    println 'muliply(NumberWrapper)'
    new NumberWrapper(number: number * wrapper.number)
    }

    def String toString() {
    "wrapper for $number"
    }
    }

    def wrapper1 = new NumberWrapper(number: 1)
    def wrapper2 = new NumberWrapper(number: 2)

    println(wrapper1 * wrapper2)
    println(wrapper1 * 3)


    Output:


    muliply(NumberWrapper)
    wrapper for 2
    muliply(Number)
    wrapper for 3


    Optional parameters



    It's possible to define default parameters values for Groovy method arguments. Any number of such parameters can be introduced. The only restriction is that they should be trailing within the method definition:


    def sum(first, second, third = 3, forth = 4) {
    println "sum($first, $second, $third, $forth)"
    }

    sum(10, 20)
    sum(10, 20, 30)
    sum(10, 20, 30, 40)


    Output:

    sum(10, 20, 3, 4)
    sum(10, 20, 30, 4)
    sum(10, 20, 30, 40)


    Vararg methods



    Groovy supports java syntax sugar for var-arg parameter delivered as array at runtime. Moreover, any trailing array parameter is treated as vararg parameter with 0..* cardinality:


    def test1(first, ... trailing) {
    println "test1($first, $trailing)"
    }

    def test2(first, Object[] trailing) {
    println "test2($first, $trailing)"
    }

    test1(1, 2, 3, 4)
    test1(1)

    test2(10, 20, 30, 40)
    test2(10)


    Output:


    test1(1, [2, 3, 4])
    test1(1, [])
    test2(10, [20, 30, 40])
    test2(10, [])


    Named parameters



    All method calls above used so-called 'positional parameters', i.e. parameter meaning is defined by its position at method call expression. However, it's possible to use rather convenient alternative to that - 'named parameters'. Method caller supplies every parameter with it's name and it's position doesn't matter at all. Moreover, it's even possible to provide support for optional parameters within method implementation:


    def test(Map args) {
    args.get('quantity', 1) // Provide default value for 'quantity' argument
    println "Total price: ${args.quantity * args.price}"
    }

    test(quantity: 3, price: 2)
    test(price: 4, quantity: 2)
    test(price: 10)


    Output:


    Total price: 6
    Total price: 8
    Total price: 10


    Constructors with named parameters



    Groovy allows to create instances of classes with 'named parameters syntax' for their properties. The only restriction for such usage is that class which object is constructed should have no-args constructor:


    class TestClass {
    String property1, property2

    def String toString() {
    return "property1: $property1; property2: $property2";
    }
    }

    println new TestClass(property1: 'xxx')
    println new TestClass(property2: 'zzz', property1: 'xxx')


    Output:


    property1: xxx; property2: null
    property1: xxx; property2: zzz


    Constructors with positional parameters



    There is convenient syntax for calling constructors with positional parameters as well.

    The main idea is that every time Groovy sees the need to convert list to some other type it tries to call appropriate type's constructor with all list items as arguments (at same order). Such a conversion may be performed either explicitly (via 'as' operator) or implicitly:


    class TestClass {
    String property1, property2

    def TestClass(property1, property2) {
    this.property1 = property1
    this.property2 = property2
    }

    def String toString() {
    return "property1: $property1; property2: $property2";
    }
    }

    def obj1 = [1, 2] as TestClass
    TestClass obj2 = ['xxx', new Date()] // Note that we use explicit variable typing here

    println obj1
    println obj2


    Output:


    property1: 1; property2: 2
    property1: xxx; property2: Mon May 10 23:14:43 MSD 2010

    2 comments: