2014/1/10

Groovy in JVM

Groovy 是必須要依附在 JVM 的 script 語言,他的語法是簡化自 Java 的結果,由於她專為 Java 而生的特性,我們只需要附加幾個 groovy 的 jar 檔,就可以使用 groovy。事實上,我們只需要 groovy 當作是一種 XML,XML 有自己的語法規範,在 JVM 會有 XML Parser 協助轉換 XML 為 Java 的物件,而 groovy 就像是一種進階的 XML 文件檔案,他不單能描述資料,也能跟 Java 物件互動,這也是 groovy DSL 帶來的優勢。

以往我們以 XML 作為延伸 Java 系統的第一選擇,但在這些高階 DSL 的協助下,我們可以用 groovy 更精簡的語法,而得到比 xml 更多的功能。

Installation

安裝有兩個部份,第一個部份是標準元件,首先到 groovy 官方網站下載 套件,目前是 2.2.1 版 groovy-binary-2.2.1.zip,通常處理 Java 的套件,我不會選擇使用 windows installer,而是下載 binary package,然後再自行到環境變數設定 GROOVY_HOME 為 c:\java\groovy-2.2.1(我習慣把套件Java都放在 c:\java 裡面),然後再把 %GROOVY_HOME%\bin 放到 PATH 裡面。Linux 環境也是類似的作法,差別只是 Linux 環境的 script 不一樣,通常可以修改 /etc/profile,然後增加 export GROOVY_HOME=/usr/share/groovy-2.2.1 ,增加 export PATH=$GROOVY_HOME/bin:$PATH 。

第二個部份,是安裝 IDE 的 Plugin ,在 Groovy-Eclipse 的 網頁 裡面,選擇適合自己 Eclipse 版本的 Plugin,我是選擇 4.3(Kepler) ,在 Eclipse -> Help -> Install New Software 裡面把 groovy-eclipse 的 http://dist.springsource.org/release/GRECLIPSE/e4.3/ 路徑加進去,就可以安裝Groovy-Eclipse了。

Groovy with Eclipse - Tutorial 給了一篇 tutorial,我覺得作者每一小段都提供簡單的 sample 的方法還不錯,以下我們用類似的方式測試,但我修改了一些 topic 的先後順序。

source code

如果需要以下內容的範例程式,請點擊 連結 下載檔案。

HelloWorld

這是一個列印HelloWorld的範例,前面是用 groovy 做的,檔名為 TestGroovy.groovy,後面是 TestJava.java,兩個範例的功能一樣,但精簡的 groovy 帶來了有效率的開發。

package test.groovy

class TestGroovy {

    static main(args) {
        def mylist=[
            "字串  Hello",
            " World",
            "...",
            ", 數字:",
            1
        ]
        mylist.each{ print it }
        println ""
    }
}
package test.groovy;

import java.util.ArrayList;
import java.util.List;

public class TestJava {
    public static void main(String[] args) {
        List<String> mylist =new ArrayList<String>();
        mylist.add("字串  Hello");
        mylist.add(" World");
        mylist.add("...");
        mylist.add(", 數字:");
        mylist.add(""+1);

        for(String a: mylist) {
            System.out.print(a);
        }
        System.out.println("");
    }
}

跟Java的差別

Difference with Java 裡面提到了跟 Java 程式碼的差異

  1. Floating point number literals 預設的型別為 BigDecimals
  2. 預設就會 import java.io., java.lang., java.math.BigDecimal, java.math.BigInteger, java.net., java.util., groovy.lang., groovy.util.
  3. == 就等同於 Java 裡的 equals,如果要判斷是不是同一個instance,就要寫成 foo.is(bar)
  4. in 是 keyword
  5. 宣告 array 要寫成 int[] a = [1,2,3] ,而不是 int[] a = {1,2,3};
  6. loop迴圈要寫成 for (i in 0..len-1) {...} 或是 for (i in 0..<len) {...} 或是 len.times {...} ,而不是 for (int i=0; i < len; i++) {...}
  7. Semicolons 句末的分號; are optional
  8. return keyword is optional
  9. static method 裡面可使用 this
  10. Methods and classes 預設為 public
  11. method 拼錯了,只會在 runtime 產生 MissingMethodException,而不會在 compile time 發生,這要參考 Runtime vs Compile time, Static vs Dynamic 這篇文章。在 groovy 2.0.0 之後,支援了 @groovy.transform.TypeChecked 這個 annotation,可在 compile time 提供錯誤檢查。

precompile or direct mode

執行 TestGroovy.groovy 有兩種方式,Groovy簡介、安裝SDK及基本開發環境 有一張說明圖寫得很清楚。



在 Eclipse 的 Run As 裡面執行 TestGroovy.groovy 也會發現有兩種選擇,一種是 Groovy Console,一種是 Groovy Script,選第一種時,會出現一個獨立的 Groovy Console IDE 視窗,可直接在視窗裡面修改 groovy code,然後直接執行,這也就是 direct mode,而選擇 Groovy Script 則是在 Eclipse Console 裡面直接得到執行結果,查看一下會發現,TestGroovy.groovy 會先被編譯成 TestGroovy.class 與 TestGroovy$_main_closure1.class,並放在 bin 目錄裡面,Eclipse console 是使用 JVM 執行這些 Java class 的結果。

在一開始獨立安裝的 groovy-binary-2.2.1.zip 裡面,我們可以在 bin 目錄找到幾個 batch 檔,groovyConsole.bat 就是剛剛 Groovy Console IDE 視窗,文件在 Groovy Console 。 groovyc.bat 可以把 TestGroovy.groovy 編譯成 java class 檔,groovy.bat 則
可以執行 groovy script,但這個並不是執行 groovyc 編譯的 class 檔,在Using Groovy from the command line 得到相關的文件說明。

>groovy -c UTF-8 test\groovy\TestGroovy
字串  Hellolo World..., 數字:r:1

Groovy Classes, Objects and Methods

Groovy class 預設為 public,所有的 groovy source file 都是以 .groovy 作為副檔名。

// 檔案目錄 test\groovy\SumIt.groovy

package test.groovy

class SumIt {

    static sum(a,b) {
        a+b
    }
    static main(args) {
        println sum(1,5)
        println sum(2,4)
    }
}

groovy 的欄位預設為 private,且會自動產生 getter與 setter methods,所以可以直接使用 setFirstName,也可以用 p.lastName 的方式,存取欄位資料。

當我們用 new Person(firstName: "名字", lastName:"姓") 這個方式產生 Person 時,groovy將會自動產生含有這兩個欄位作為參數的 constructor。

使用 == 就等同於在 Java 的 equals,在 Person 類別必須要覆寫 hashCode 與 equals 才有作用。

// test\groovy\Person.groovy

package test.groovy

public class Person{
    String firstName
    String lastName
    int age
    def address

    static void main(def args) {
        Person p = new Person()
        // use the generated access methods
        p.setFirstName("Name")
        // This will still use the generated access method, it is not a direct access!
        p.lastName = "LastName"
        p.age=10;
        p.address = ("Homestreet 3");
        println(p.firstName + " " + p.lastName);

        // use the generated constructor
        Person p2 = new Person(firstName: "名字", lastName:"姓");
        println(p2.firstName + " " + p2.lastName);

        println ""
        println "p==p2 : "+(p==p2);
        println "p.is(p2) : "+p.is(p2);
        p2=p;
        println ""
        println("after p2=p")
        println "p==p2 : "+(p==p2);
        println "p.is(p2) : "+p.is(p2);

        Person p3 = new Person();
        p3.setFirstName("Name");
        p3.age=10;
        p3.lastName = "LastName";
        p3.address = ("Homestreet 3");

        println ""
        println "p==p3 : "+(p==p3);
        println "p.is(p3) : "+(p.is(p3));
    }

    boolean equals(o) {
        if (this.is(o)) return true;

        if (!o || getClass() != o.class) return false;

        Person that = (Person) o;

        if (firstName? !firstName.equals(that.firstName) : that.firstName!= null) return false;
        if (lastName? !lastName.equals(that.lastName) : that.lastName!= null) return false;
        if (age? !age.equals(that.age) : that.age!= null) return false;
        if (address? !address.equals(that.address) : that.address!= null) return false;

        return true;
    }

    int hashCode() {
        int result = (firstName ? firstName.hashCode() : 0);
        result = 31 * result + (lastName ? lastName.hashCode() : 0);
        result = 31 * result + (age ? age.hashCode() : 0);
        return result = 31 * result + (address ? address.hashCode() : 0);
    }
}

執行的結果為

Name LastName
名字 姓

p==p2 : false
p.is(p2) : false

after p2=p
p==p2 : true
p.is(p2) : true

p==p3 : true
p.is(p3) : false

Groovy 允許使用不固定個數的參數,但該optional 參數,必須要指定預設值 c=10。

// test\groovy\SumIt.groovy
package test.groovy

class SumIt {

    static sum(a,b, c=10) {
        a+b+c
    }
    static main(args) {
        println sum(1,5)
        println sum(2,4)

        println sum(2,4,5)
    }
}

Loops

迴圈可使用 list.each 的方式,搭配指定變數 firstName-> println firstName 或是使用內建的 it 變數 println it。

針對 number 變數,有 upto(), downto(), times(),也可以使用 (1..6) 這樣的 range data type,來處理迴圈。

package test.groovy

class PrintLoop {

    public static void main(def args){
        def list = [
            "John",
            "Mary",
            "James"
        ]

        // using a variable assignment
        list.each{firstName-> println firstName }
        // using the it variable
        list.each{println it}

        println ""
        5.times {println "Times + $it "}
        1.upto(3) {println "Up + $it "}
        4.downto(1) {print "Down + $it "}

        def sum = 0
        1.upto(100) {sum += 1}
        print sum

        (1..6).each {print "Range $it"}
    }
}

執行結果為

John
Mary
James
John
Mary
James

Times + 0 
Times + 1 
Times + 2 
Times + 3 
Times + 4 
Up + 1 
Up + 2 
Up + 3 
Down + 4 Down + 3 Down + 2 Down + 1 100Range 1Range 2Range 3Range 4Range 5Range 6

Groovy Data Types

Reference variables

Groovy 的所有變數都是 Reference variables,沒有 primitive variables,即使寫成 int 10,背後還是轉換為物件。

Groovy 允許使用 static and dynamic typed variables,動態資料型別要以 def 作為 data type。

以下的 TypeTest可用來查看 def 型別轉換的狀況。

package test.groovy

class TypesTest {

    static main(args) {
        int i = 1 // Short form for Integer i = new Integer(1)
        int j = i +3
        int k = i.plus(3); // Same as above
        // Make sure this worked
        assert(k==4);
        println i.getClass().getName()
        println j.getClass().getName()
        println k.getClass().getName()

        // Automatic type assignement
        def value = 1.0F
        println value.getClass().getName()
        def value2 = 1;
        println value2.getClass().getName()
        value2 = value2 / 2;
        println value2
        println value2.getClass().getName()
    }
}

執行的結果

java.lang.Integer
java.lang.Integer
java.lang.Integer
java.lang.Float
java.lang.Integer
0.5
java.math.BigDecimal

strings

Groovy 的 string 有兩種,第一種是 " " 包圍的 string,型別為 org.codehaus.groovy.runtime.GStringImpl,這種字串稱為 Groovy String,簡稱 GString,可以直接用 $name 進行替換的運算,第二種是 ' ' 包圍的 string,這就是 Java 的字串,型別為 java.lang.String。

package test.groovy

class StringTest {
    static main(args) {
        def name = "John"
        def s1 = "Hello $name" // $name will be replaced
        def s2 = 'Hello $name' // $name will not be replaced
        println s1
        println s2
        println s1.getClass().getName();
        println s2.getClass().getName();
    }
}

執行結果

Hello John
Hello $name
org.codehaus.groovy.runtime.GStringImpl
java.lang.String

Lists and maps

以 List persons = list[] 定義新的 list,persons.get(i) or persons[i] 可取得第 i 個元素。Map是以 Map[key1:value1, key2:value2,...]的方式定義,可以直接指定某一個位置的內容 map[key]=value。

package test.groovy

class ListMap {
    static main(args) {
        List<Integer> list = [1, 2, 3, 4]
        println list[0]
        println list[1]
        println list[2]

        List<Person> persons = list[]
        Person p = new Person(firstName: "John", lastName:"LastName")
        persons[0] = p
        println persons.size()
        println persons[0].firstName
        println persons.get(0).firstName

        Map map = [:]
        def map2 = ["John":"LastName", "James":"LastName2"]
        println map2["James"]
        map2["Test"] = "Tester"
        println map2["Test"]
    }
}

Ranges

只要Java物件有實作 previous() and next() 這兩個method,且有 implements java.lang.Comparable,都可以用在 Ranges。 0..4 就是 0,1,2,3,4 而 0..<4 就是 0,1,2,3。

package test.groovy

class RangesTest {
    public static void main(args){
        for (i in 0..4) {
            println ("Hello $i")
        }

        for (i in 0..<4) {
            println ("2nd Hello $i")
        }
    }
}

Regular Expression

運算子 說明
=~ Find: 如果pattern在字串中有出現
==~ Match: Pattern必須完整符合整個字串
~String 將String轉換為 regular expression
package test.groovy

class RegularExpressionTest {
    public static void main(String[] args) {
        // Defines a string with special signs
        def text = "James happily ever after"

        // Every word must be followed by a nonword character
        // Match
        if (text==~/(\w*\W+)*/){
            println "Match was successful"
        } else {
            println "Match was not successful"
        }
        // Every word must be followed by a nonword character
        // Find
        if (text=~/(\w*\W+)*/){
            println "Find was successful"
        } else {
            println "Find was not successful"
        }

        if (text==~/^J.*/){
            println "There was a match"
        } else {
            println "No match found"
        }
        def newText = text.replaceAll(/\w+/, "hubba")
        println newText
    }
}

Closures

closure 是可使用在 class 或 method 外面的一段程式碼,語法為{para1, para2 -> code of the closure},para1與para2為後面程式碼的參數。list.each裡面可以使用closure。

package test.groovy

class ClosureTest {
    public static void main(args){
        List<Integer> list = [5, 6, 7, 8]
        list.each({line -> println line})
        list.each({println it})
    }
}

Meta Object Protocol

meta object protocol 可動態在runtime增加 methods與 properties。如果程式中,呼叫了沒有定義的 method/property,就會自動呼叫下列這些 method。

  1. def methodMissing (String name, args) - Called for missing method
  2. void setProperty (String property, Object o) - called for non existing setter of a property
  3. Object getProperty (String property) - called for non existing getter of a property
package test.groovy

class MetaObject {
    def map

    Object getProperty (String property){
        println "Setting this propery"
        return 5;
    }

    void setProperty (String property, Object o){
        println "Hallo"
    }

    def methodMissing (String name, args){
        def s = name.toUpperCase();
        if (s.startsWith("HELLO")) {
            println "This method stats with Hello. Full name $name"
        } else {
            println "This method is missing"
        }
    }

    public static void main (args){
        def test = new MetaObject();
        test.hall();
        test.helloMethod();
        test.Hallo();
        test.test= 5;
        println test.test;
    }
}

Operator overloading

groovy支援在class裡面覆寫一些標準的 operations,例如 a+b 可以透過實作 plus method 調整 a+b 的作用。

Operator 名稱 Method
a+b plus a.plus(b)
a-b minus a.minus(b)
a*b star a.multiply(b)
a/b divide a.div(b)
a%b modulo a.mod(b)
a--, --a decrement a.previous()
a++, ++a increment a.next()
a**b power a.power(b)
a-b minus a.minus(b)
a-b minus a.minus(b)

File IO

將 userlist.txt 檔案內容,加上行號,填寫到 userlist2.txt 裡面

package test.groovy

class FileTest {
    static main(def args){
        File file = new File("d:/temp/userlist.txt")
        File file2 = new File("d:/temp/userlist2.txt")
        if( !file2.exists() ) {
            file2.createNewFile() //if it doesn't already exist
        }
        assert file2.exists()
        def file2writer = file2.newWriter()
        file2writer<<"";

        def lineNumber = 0;

        file.eachLine{ line ->
            lineNumber++
            file2 << "$lineNumber: $line\n"
        }
    }
}

XML

使用 XmlParser 處理 XML 文件

package test.groovy

class XMLTest {
    static void main(args){
        def xmldocument = '''
    <persons> 
      <person><firstname age="33">John</firstname><lastname>Chen</lastname></person>
      <person><firstname age="34">James</firstname><lastname>Lee</lastname></person>
    </persons>
    '''
        def persons = new XmlParser().parseText(xmldocument);
        def allRecords = persons.person.size()
        println("Number of person is: $allRecords")
        def person = persons.person[0]
        // name is the name of the XML tag
        println("Name of the person tag is:" + person.name())
        // text gets the text of the node firstname
        println(person.firstname.text())

        // Lets print out all important information
        for (p in persons.person){
            println "${p.firstname.text()} ${p.lastname.text()}"
        }
    }
}

Thread and Concurrency

groovy 跟 java 一樣,可以使用 thread 跟 synchronzied block。

package test.groovy
import java.util.concurrent.atomic.AtomicInteger

class SynchronizedTest {
    private final myLock = new Object()

    static synchronized void greet() {
        println "world"
    }

    synchronized int answerToEverything() {
        return 42
    }

    void foo() {
        synchronized ("myLock") {
            println "bar"
        }
    }

    private def counter = new AtomicInteger()
    synchronized out(message) {
        println(message)
    }

    void processthread() {
        def th = Thread.start {
            for( i in 1..8 ) {
                sleep 30
                out "thread loop $i"
                counter.incrementAndGet()
            }
        }

        for( j in 1..4 ) {
            sleep 50
            out "main loop $j"
            counter.incrementAndGet()
        }
        th.join()

        assert counter.get() == 12
        out "result:"+counter.get();
    }

    static main(args) {
        SynchronizedTest test=new SynchronizedTest();
        test.processthread();
    }
}

另外還有 Groovy GPars 支援 Actors, Map/Reduce, Dataflow, Fork/Join 這些平行處理的功能。

NullPointerException

groovy 可以用 ?: 或是 ?. 來處理 java 最常遇到的 NullPointerException。

package test.groovy

class AvoidNullPointer {
    static void main(args){
        Person user = null;
        def firstName = user?.firstName;
        println "firstName:"+firstName

        user = new Person();
        def firstName2 = user?.firstName;
        println "firstName2:"+firstName2

        def user2
        user2 ?: new Person();
        def firstName3 = user?.firstName;
        println "firstName3:"+firstName3
    }
}

Java class 跟 groovy script 的互動

我們在 Person.groovy 定義的Person物件,可以直接在TestJava.java 裡面直接使用 Person。

// test\groovy\Person.groovy
// test\groovy\TestJava.java

package test.groovy;

public class TestJava {
    public static void main(String[] args) {

        Person p = new Person();
        p.setFirstName("Firstname");
        System.out.println("p="+p.getFirstName());
    }
}

結語

groovy 就等同於一個簡化 Java 語法的內建動態語言,她必須跟 JVM 共生,嵌入 JVM 雖代表她天生就受限於 JVM,但也表示我們可以很容易地以Java方式整合 groovy。