以下の文章は、Object Computing, Inc(OCI) による「The Java News Brief」(2004年2月) に掲載された、Mark Volkmann による「Groovy - Scripting for Java」を、かくたにが翻訳したものです。原著者の許可を得て翻訳・公開しています。翻訳にあたっては、ma2さん、takaiさんから草稿に対してコメントをいただきました。ありがとうございます。

Groovy は日々発展中の新しい言語です。本記事の原文は2004年2月頃のものであり、当然その内容は Groovy の最新状況に追随しているわけではありません。翻訳時点での状況(1.0-beta4)をなるべく反映するようにはしましたが、正式版リリースまでにはさらに幾つもの変更が加えられることが予想されます。記事の記述と Groovy の現状とが異なる場合は、現状を優先してください。その一方で、そういった事項のこの記事へのフィードバックを歓迎します。連絡先はかくたにまでよろしくお願いします。フィードバックは記事の内容のみならず、誤訳誤記についても歓迎です。

翻訳版の主な改訂履歴:



Groovy - Java用スクリプト言語

by
マーク・ボルクマン, パートナー
Object Computing, Inc. (OCI)

目次

はじめに

Groovy はオープンソースのスクリプト言語である。Java で実装され、Java と緊密に統合されている。使うにあたっては J2SDK 1.4 が必要だ。Groovy は Ruby と Python といったスクリプト言語の機能のいくつかを Java に追加している。Groovy の機能には、動的型付け、クロージャ、容易なオブジェクト・ナビゲーション、そして、List や Map を取り扱うための簡潔な文法が含まれている。こうした機能(とさらに多くの機能)の詳細を本記事では取り上げる。

Groovy のウェブサイトから引用しよう。 "Groovy はあなたが Java プラットフォームで手早く、簡潔に、そして楽しく作業できるように設計されました―― Groovy は Python と Ruby の力強さを Java プラットフォームにもたらします。"

Groovy スクリプトではあらゆる Java クラスを利用できる。Groovy スクリプトは Java のバイトコード(.class ファイル)へとコンパイルできるので、普通の Java クラスから呼び出すこともできる。Groovy コンパイラである groovyc は、Groovy スクリプトと Java ソースファイルの両方をコンパイルすることができるが、現状の groovyc ではいくつかの Java 文法(たとえばネストしたクラス)が未サポートである。

理論上は、アプリケーション全体を Groovy で書くことができ、そのパフォーマンス特性は Java アプリケーションとほとんど等しくなるだろう。ここが、Groovy が他のスクリプト言語(Ruby、Python、Perl、そして BeanShell)とは異なるところだ。しかし現在、Groovy の性能は Java よりも劣る。 その原因のひとつは、生成された Groovy のバイトコードがコンストラクタと private/protected メソッドを呼び出すのにリフレクションを利用しているためだ。これは将来のリリースで解決されるだろう。

Groovy の開発は James Strachan と Bob McWhirter によって始められた。James は他にもたくさんのオープンソース・プロダクトの開発に携わっている(Jelly、dom4j、Jaxen、Betwixt、そして Maven)。Bob は Jaxen と Drools(オープンソースでオブジェクト指向の Java ルールエンジン)の創始者でもある。

本記事では Groovy の機能のすべてではないが、その多くを取り上げる。読者は Java の文法と Groovy の文法との比較ができる程度には Java の文法に親しんでいるものと想定する。

我われが、プログラミング言語の文法を善きものとするのは何かについて全面的に合意できれば、プログラミング言語の数はさほど多くは必要としないだろう。現実にはプログラミング言語が数多く存在していることを考えると、我われが合意に達していないことは明らかだ。本記事の読了後に、Java の文法でまったく充分で、Groovy の文法はシンタックス・シュガーが甘すぎて口に合わないな、と思うかもしれない。もし、あなたがそう判断したら、私としては Pat Niemeyer の BeanShell を http://www.beanshell.org から落としてきて調べてみることをお奨めする。そうではなく、Groovy の簡潔な文法が気に入ったなら、いやもうほんとグルーヴィだよね!!

Groovy のダウンロードとインストール

Groovy をダウンロードするには、以下の手順に従う。

  1. http://groovy.codehaus.org/ へアクセス。
  2. ページ上部のナビゲーションバーの、 "Download" のリンクをクリックする。
  3. "this site" のリンクをクリックする。
  4. ダウンロードしたいバージョンのリリースをクリックする。

最新版は CVS にあり、これもダウンロード可能だ。説明についてはこちらを参照してほしい。

Groovy をインストールするには、以下の手順に従う。

  1. ダウンロードした ZIP ファイルを展開する。
  2. 展開したディレクトリを環境変数 GROOVY_HOME に設定する。
  3. 環境変数 PATH に $GROOVY_HOME/bin (UNIXの場合) または %GROOVY_HOME%\bin (Windowsの場合)を設定する。

Groovy の実行

Groovy のスクリプトを実行するには4種類の方法がある。 どの方法であっても、スクリプトはパースされて Java ソースに変換されたうえで、 Java バイトコードへとコンパイルされる。

対話的シェル

groovysh コマンドは、Groovy のステートメントを入力できる 対話的シェルを起動する。 ステートメントは Enter キーを押すことで複数行入力できる。 ステートメントは、execute コマンド[*1]が入力されるまで評価されない。

対話的 Swing コンソール

groovyConsole コマンドは Swing のウィンドウを起動する。ウィンドウの下部ペインに Groovy ステートメントを入力する。Actions メニューから Run を選ぶとステートメントが実行される。出力は上部ペインに表示される。File メニューを使用して、スクリプトをファイルから開いたり、ファイルに保存することができる。

GroovyConsole screen shot

スクリプトファイルによる実行

Groovy のスクリプトファイル (一般的には拡張子 .groovy のファイル)を次のようなコマンドから実行することができる。groovy script-name.groovy

コンパイル済スクリプトによる実行

Groovy スクリプトは groovyc script-name.groovy コマンドを使用して Java の .class ファイルへとコンパイルできる。スクリプトにはルーズ・ステートメント[*2]があったとしても、生成された .class ファイルには main メソッドが含まれているので、java script-name コマンドを使って Java アプリケーションとして実行できる。main メソッドの内容については後述する。実行にあたってはクラスパスに Groovy の lib ディレクトリにある groovy*.jarasm*.jar を追加する必要がある。

なんと、この作業を行うためのカスタム Ant タスクも用意されている! org.codehaus.groovy.ant.Groovyc クラスがそうだ。

[*1]訳注: "go"コマンドでも実行可能。
[*2]訳注: 明示的にはどのクラスにも属さないステートメント。

文法の詳細(の一部)

まずは Java と Groovy の文法上の主な違いを取り上げる。細かいものについては後述する。

[*3]訳注: 1.0beta4だとすでに評価値が返るようになっているようだ。
[*4]訳注: これは原文の誤り。Javaのメソッドスコープのデフォルトは package-private。

動的型付け

変数、プロパティ、メソッド/クロージャの引数、そしてメソッドの戻り値において、型の宣言は省略できる。それらはどんな型でもとることができる。また、後から異なる型を割り当てることもできる。もちろん、プリミティブ型も利用可能だ(auto-boxing される)。型変換のの多くは必要に応じて自動的に行われる。たとえば、String やプリミティブ型(int とか)と型ラッパークラス(Integer とか)との間での型変換がそうだ。このおかげで、プリミティブ型をコレクションに追加することができる。

追加されたメソッド

Groovy は java.lang.Object や java.lang.String といった標準 Java クラスに数多くのメソッドを追加している。詳細は http://groovy.codehaus.org/groovy-jdk.html を参照のこと。ここでは Object クラスに追加されたものを示す。その他のものについては後述する。

dump

<class-name@hashcode property-name=property-value ...> の形式で文字列を返す。 たとえば、 <Car@ef5502 make=Toyota model=Camry> といった具合だ。

print と println

これらの static な print メソッドはオブジェクトの toString 値を出力する。 たとえば、 print car あるいは println car と書く。

invokeMethod

リフレクションを利用して動的なメソッド呼び出しを行う。 文法は、 object.invokeMethod(method-name, argument-array) だ。 次のサンプルは、4を出力する。

    s = 'abcabc' // Java の文字列
    method = 'indexOf'
    args = ['b', 2]
    println s.invokeMethod(method, args) // 4
	

Groovy な String

リテラル文字列は、シングルクォートまたはダブルクォートで囲む。ダブルクォートを使った場合は、埋め込み値を利用できる。埋め込み値の文法は、 ${expression} だ。Rubyでの # の代わりに $ を使用する。ダブルクォートで囲まれた文字列にひとつ以上の埋め込み値が含まれる場合、その文字列は groovy.lang.GString オブジェクトで表現される。そうでない文字列は java.lang.String だ。GString は必要に応じて java.lang.String へと強制的に変換される。

groovy.lang.GString といった Groovy クラスの Javadoc は、 http://groovy.codehaus.org/apidocs/ で読むことができる。

埋め込み値は toString メソッドを実装する際にとても便利だ。たとえば、

    String toString() { "${name} is ${age} years old." }

複数行文字列を生成する方法は3種類ある。以下のサンプルはすべて同じ結果になる。最後のサンプルはヒアドキュメント("here-doc")と呼ばれるものだ。3字以内の文字をデリミタ文字列として指定すると、文字列の値には最初と2番目のデリミタ間に現れる文字が値として設定される。"EOS" ("End Of String" の略)はよく使われるデリミタ値だが、もちろん他の文字も使用できる。

    s = "  This string
      spans three \"lines\"
      and contains two newlines."

    s = """  This string
      spans three "lines"
      and contains two newlines."""

    s = <<<EOS
      This string
      spans three "lines"
      and contains two newlines.
    EOS

上記のコードで,最後の改行が残らないことに注意。

以下に java.lang.String へ追加されているメソッドを示す。

contains

引数の部分文字列を含んでいるかを調べる。 'Groovy'.contains('oo')true を返す。

count

 引数の部分文字列の出現回数をカウントする。 'Groovy Tool'.count('oo')2 を返す。

tokenize

引数で与えられたデリミタを使用して文字列をトークン化し、そのトークンのリストを返す。デリミタ引数の指定は省略できる。デフォルトでは空白文字が使用される。 'apple^banana^grape'.tokenize('^')['apple', 'banana', 'grape'] を返す。

minus

対象文字列から、引数として与えられた部分文字列のうち最初に出現するものを削除する。 'Groovy Tool' - 'oo''Grvy Tool' を返す。

multiply

引数で与えられた回数だけ対象文字列を繰り返す。 'Groovy' * 3 は 'GroovyGroovyGroovy' を返す。

正規表現 (regex)

まずは、J2SE 1.4 での正規表現をおさらいしておこう。次に、Groovy がどのようにこれを拡張しているかを議論する。

J2SE 1.4 の正規表現は java.util.regex パッケージのクラスでサポートされる。Pattern オブジェクトは、コンパイルされた正規表現を表し、Pattern.compile("pattern") メソッドで生成される。正規表現の文法は Pattern クラスの Javadoc で解説されている。Matcher オブジェクトはマッチ対象文字列に対するマッチングの結果を保持する。Matcher オブジェクトは pattern.matcher("text") メソッドで生成される。単に文字列がパターンにマッチするのかを知りたいのであれば、matcher.matches() メソッドを使う。パターン内でバックスラッシュを使うには、さらに追加のバックスラッシュが必要だ。

Groovy は正規表現を3種類の演算子でサポートする。
~"pattern" は Pattern オブジェクトを生成する。これは、Pattern.compile("pattern") と同じ意味である。
"text" =~ "pattern" は Matcher オブジェクトを生成する。これは Pattern.compile("pattern").matcher("text") と同じ意味である。
"text" ==~ "pattern" は boolean 値を返す。これは Pattern.compile("pattern").matcher("text").matches() と同じ意味である。他の追加されたメソッドについては Pattern クラスと Matcher クラスの Javadoc を参照のこと。

サンプルを示そう。

    pattern = "\\d{5}" // 郵便番号にマッチ(5桁)
    text = "63304" // 郵便番号

    println text ==~ pattern // "true" が出力される

    m = text =~ pattern
    println m.matches() // "true" が出力される

    // 次の例では、パターンはリテラル文字列でなければならない。
    // 変数は使えない。
    p = ~"\\d{5}"
    m = p.matcher(text)
    println m.matches() // "true" が出力される

Groovy なスクリプト

Groovy で書かれたスクリプトのソースは、普通は拡張子 ".groovy" のファイルだ。スクリプトには(自由な順序で)ルーズ・ステートメント、クラスに属さないメソッド定義、そしてクラス定義が含まれている。

たとえば、

    // ルーズ・ステートメント
    println 'loose statement'
    myMethod 'Mark', 19
    println new MyClass(a1:'Running', a2:26.2)
    
    // クラスに属さないメソッドの定義
    def myMethod(p1, p2) {
      println "myMethod: p1=${p1}, p2=${p2}"
    }
    
    // 2つのプロパティと1つのメソッドを持つクラスの定義
    class MyClass {
      a1; a2
      String toString() { "MyClass: a1=${a1}, a2=${a2}" }
    }

メソッドとクラスの定義は、使用されるよりも前になくてもよい。ルーズ・メソッド[*5]は該当するスクリプトファイルに対応するクラスの static メソッドとしてコンパイルされる。たとえば、foo という名前のルーズ・メソッドが、Bar.groovy という名前のスクリプトファイルにあれば、Bar クラスの static メソッド foo としてコンパイルされる。ルーズ・ステートメントは、自動生成される main メソッドから呼び出される run メソッドにまとめられる。

groovyc を使用してスクリプトをコンパイルした場合、コンパイル後のクラスには、すべてのルーズ・ステートメントがまとめられた run メソッドと、この run メソッドを呼び出す main メソッドが自動で追加される。

いまのところ、スクリプトから他のスクリプトを実行するためには、実行したいスクリプトをコンパイルし、インポートしなければならない。この不具合は近いうちに修正されるはずだ。

[*5]訳注: 明示的にはどのクラスにも属さないメソッド。

演算子のオーバーロード

Groovy は特定の演算子のオーバーロードをサポートしている。オーバーロード可能な演算子はそれぞれ、特定のメソッドに対応づけられる。対応づけられたメソッドを実装することで、そのクラスのオブジェクトはメソッドに対応する演算子をオーバーロードできる。オーバーロード対応メソッドを、様ざまなパラメータの型に向けてオーバーロードすることもできる。

比較演算子

a == ba.equals(b) に対応する。
a != b!a.equals(b) に対応する。
a === b は Java の a == 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 に対応する。

比較演算子は null をハンドルするので、NullPointerException は絶対に発生しない。null はどんなオブジェクトよりも小さいものとして扱われる。

Groovy の == 演算子は、両辺のオブジェクトが等価であるかをテストし、=== 演算子は両辺がメモリ上で同一のオブジェクトであるかをテストすることに注意。

compareTo メソッドは int を返す。返す数値は、a < b の場合は負の値、a > b の場合は正の値、同値の場合は0である。

その他の演算子

a + ba.plus(b) に対応する。
a - ba.minus(b) に対応する。
a * ba.multiply(b) に対応する。
a / ba.divide(b) に対応する。
a++++aa.increment(b) に対応する。
a----aa.decrement(b) に対応する。
a[b]a.get(b) に対応する。
a[b] = ca.put(b, c) に対応する。

Groovy なクロージャ

クロージャとは、パラメータを受け取ることも可能なコード片のことである。個々のクロージャは、groovy.lang.Closure を継承した新しいクラスとしてコンパイルされる。クロージャの継承クラスは call メソッドを持つ。このメソッドはクロージャの実行と、パラメータを渡すために利用される。クロージャは、自身が生成されたスコープにある変数を読み書きできる。クロージャ内部で生成された変数は,クロージャが起動されたスコープで有効になる。クロージャは変数に代入しておくことができ、それをメソッドに引数として渡すこともできる。この特徴は List や Map、そして String のメソッドで威力を発揮する。詳しくは後述する。

クロージャを定義する文法は、

    { comma-separated-parameter-list | statements }

となっている。たとえば、

    closure = { bill, tipPercentage | bill * tipPercentage / 100 }
    tip = closure.call(25.19, 15)
    tip = closure(25.19, 15) // 上の行と同じ意味

間違った数のパラメータを渡すと、 IncorrectClosureArgumentException が発生する。

パラメータが1つのクロージャでは、it キーワードを使うことができる。このとき、パラメータリストを省略して、代わりに it を使ってステートメント中で参照できる。たとえば、以下のクロージャはいずれも同じ意味である。

    { x | println x }
    { println it }

以下は List とクロージャとを引数に取るメソッドのサンプルである。このサンプルではルーズ・メソッドとして書いたが、もちろんクラスのメソッドにもできる。このメソッドは list の各要素を引数にクロージャを実行させながら list をイテレートする。各クロージャを実行した結果として true を返してきた要素を newList に追加する。呼び出し元には newList を返す。Groovy は findfindAll メソッドを提供しているので、普通は以下のサンプルのようには書かないことに注意。

    def List myFind(List list, Closure closure) {
      List newList = []
      for (team in list) {
        if (closure.call team) newList.add team
      }
      newList
    }

myFind メソッドを使用するコードも示しておこう。

    class Team { name; wins; losses }
    
    teams = []
    teams.add new Team(name:'Rams', wins:12 , losses:4)
    teams.add new Team(name:'Raiders', wins:4 , losses:12)
    teams.add new Team(name:'Packers', wins:10 , losses:6)
    teams.add new Team(name:'49ers', wins:7 , losses:9)

    winningTeams = myFind(teams) { it.wins > it.losses }
    winningTeams.each { println it.name }

実際には myFind のようなメソッドは必要ない。というのも、List クラスにはあらかじめ findAll メソッドが用意されているからだ。使い方はこうだ。

    winningTeams = teams.findAll { it.wins > it.losses }

Groovy Bean

以下は Groovy Bean のシンプルなサンプルだ。

    class Car {
      String make
      String model
    }

このクラスはプロパティを2つ宣言しているが、メソッドは定義していない。しかし、その舞台裏では様ざまなことが行われている。クラスやプロパティ、そしてメソッドのスコープはデフォルトでは public である。public と protected のプロパティは、結果としては public/protected な getter/setter メソッドが自動生成された private フィールドになる。これらのメソッドをオーバーライドすることで独自の振る舞いを持たせられる。プロパティが明示的に private と宣言された場合には、getter/setter は自動生成されない。

上記の Groovy コードは、以下の Java コードと同じ意味である。

    public class Car {
        private String make;
        private String model;
      
        public String getMake() {
            return make;
        }
      
        public String getModel() {
            return model;
        }
      
        public void setMake(String make) {
            this.make = make;
        }
      
        public void setModel(String model) {
            this.model = model;
        }
    }

Groovy Bean として生成されるクラスは java.lang.Object クラスを継承し、かつ groovy.lang.GroovyObject インタフェースを実装する。このクラスに追加されるメソッドは、getPropertysetPropertygetMetaClasssetMetaClass そして invokeMethod である。groovy.lang.MetaClass はクラスに対してメソッドを実行時に追加することを可能にする。

Groovy Bean では名前付きパラメータを利用してインスタンスを生成することができる。たとえば次のコードでは、まず Car の引数無しコンストラクタが呼ばれ、次にそれぞれのプロパティの setter メソッドが呼ばれる。

    myCar = new Car(make:'Toyota', model:'Camry')

Groovy List

Groovy List は java.util.ArrayList のインスタンスだ。リストはカンマ区切りで並べた値を角括弧で囲むことで生成できる。たとえば、

    cars = [new Car(make:'Honda', model:'Odyssey'),
            new Car(make:'Toyota', model:'Camry')]

    println cars[1] // Camry を参照する

    for (car in cars) { println car } // Car の toString メソッドを呼び出す

    class Car {
      make; model
      String toString() { "Car: make=${make}, model=${model}" }
    }

リストの要素をリストの最後からアクセスするには、負の値をインデックスに使用する。

空のリストは [] で生成できる。例としては、

    cars = []

リストへ要素を追加する方法は2種類ある。

    cars.add car
    cars << car

リストは配列から array.toList() メソッドで生成できる。配列はリストから list.toArray() メソッドで生成できる。

Groovy はいくつかのメソッドを java.util.List に追加している。

count

引数に与えられたオブジェクトと等価な要素がリスト内にいくつあるかを数える。
[1, 2, 3, 1].count(1)2 を返す。

immutable

コレクションの immutable(変更不能)なコピーを生成する。生成にあたっては java.util.Collections の static な unmodifiableList メソッドを利用している。たとえば、

      list = [1, 2, 3].immutable()
      list.add 4 // java.lang.UnsupportedOperationException がスローされる

intersect

2つのリストに共通の要素を含んだ新しいリストを生成する。
[1, 2, 3, 4].intersect([2, 4, 6])[2, 4] を返す。

join

リストの要素それぞれに対する toString の値の間に、引数の文字列を挿入して連結する。たとえば、キャレットを区切り文字としてリストに含まれるすべての文字列を連結したければ ['one', 'two', 'three'].join('^') と書く。これは "one^two^three" を返す。

sort

リストの要素を並び替えた新しいリストを作成する。java.util.Comparator かクロージャを渡すことでソート順を変更することができる。

    fruits = ['kiwi', 'strawberry', 'grape', 'banana']
    // 次の行は [banana, grape, kiwi, strawberry] を返す。
    sortedFruits = fruits.sort()
    
    // 次の行は [kiwi, grape, banana, strawberry] を返す。
    sortedFruits =
      fruits.sort {l, r | return l.length() <=> r.length()}

上記の最後にある sort メソッド呼び出しは、クロージャをパラメータとして渡すサンプルでもある。Groovy にはクロージャをパラメータとして渡せるメソッドが数多く用意されている。

Groovy Bean では複数のプロパティを使用したソートが簡単にできる。たとえば、Player という Bean に nameagescore というプロパティがあるとしよう。この Bean のリストが players に格納されていて、それを agescore の値を基に並び替えるには、次のように書く。

    players.sort { [it.age, it.score] }

min / max

リストの要素または文字列にある文字[*6]から、最小値あるいは最大値を検索する。java.util.Comparator かクロージャを渡すことで、判定方法を変更できる。たとえば、以下のコードはリストから最小値と最大値を検索する。
[5, 9, 1, 6].min()1 を返す。
[5, 9, 1, 6].max()9 を返す。

reverse

リストの要素または文字列[*7]にある文字を、逆順に並び替える。
[1, 2, 3].reverse()[3, 2, 1] を返す。

Groovy は java.util.List オブジェクトで使用できるように、加算と減算の演算子をオーバーロードしている。

加算

2つのリストを結合した新しいリストを生成する。重複する要素があっても削除されない。
[1, 2, 3] + [2, 3, 4][1, 2, 3, 2, 3, 4] を返す。

減算

最初のリストから二番目のリストにある要素すべてを取り除いたリストを生成する。
[1, 2, 3, 4] - [2, 4, 6][1, 3] を返す。 リストの要素にプリミティブ型が含まれていなければ、要素の比較には equals が使われる。

[*6]訳注: 1.0-beta4 では文字列に対してmin/maxは実行できない。
[*7]訳注: 1.0-beta4 では文字列に対してreverseは実行できない。「"abc".reverse()」のようにカッコを付ければ実行可能でした。

Groovy Map

Groovy Map は、java.util.HashMap のインスタンスだ。マップはカンマ区切りで並べたキー/値のペアを角括弧で囲むことで生成できる。キーと値とはコロンで区切る。たとえば、

    players = ['baseball':'Albert Pujols',
               'golf':'Tiger Woods']

    println players['golf'] // Tiger Woods を出力する
    println players.golf // Tiger Woods を出力する

    for (player in players) {
      println "${player.value} plays ${player.key}"
    }

    // これは上のループと同じ意味
    players.each {player |
      println "${player.value} plays ${player.key}"
    }

空のマップは [:] で生成できる。以下はその例だ。

    players = [:]

Groovy な switch 文

Groovy の switch 文では、クラス、List、Range、Pattern など、あらゆるオブジェクトを使える。case 文は isCase メソッドを使って値を比較する。数多くのオーバーライドされた isCase メソッドが提供されている。特定の型に向けておいてオーバーロードされない限り、isCaseequals メソッドを使用する。 case がクラス名である場合には、isCase メソッドは instanceof を使用する。 isCase メソッドは、独自に作成したクラスでオーバーライドすることができる。

様ざまな異なる型の値に対応する switch 文のサンプルを示す。

    switch (x) {
      case 'Mark':
        println "got my name"
        break
      case 3..7:
        println 'got a number in the range 3 to 7 inclusive'
        break
      case ['Moe', 'Larry', 'Curly']:
        println 'got a Stooge name'
        break
      case java.util.Date:
        println 'got a Date object'
        break
      case ~"\\d{5}":
        println 'got a zip code'
        break
      default:
        println "got unexpected value ${x}"
    }

Groovy Range

Range(範囲オブジェクト)は ".." と "..." 演算子を使用して生成する。サンプルを示そう。

3..7 は「3 から 7」を表す Range を生成する。
3...7 は「3 から 6」を表す Range を生成する。
"A".."D" は「"A" から "D"」を表す Range を生成する。
"A"..."D" は「"A" から "C"」を表す Range を生成する。

範囲オブジェクト は java.util.AbstractList を継承した groovy.lang.Range インターフェースを実装するクラスで表現される。Range オブジェクトは immutable なリストだ。Range インタフェースは getFromgetTo メソッドを追加しているので、下限と上限を取得できる。2種類の Range 実装が提供されている。 groovy.lang.IntRange は整数値に限った範囲を扱う。IntRange には contains メソッドが追加されており、値が範囲内にあるかをテストできる。 groovy.lang.ObjectRange ではその他のいかなる型でも範囲に含めることができる。ここでも contains メソッドが追加されているが、使えるのは範囲に含まれるオブジェクトが java.lang.Comparable を実装している場合に限られる。

範囲オブジェクトはループ構造で役に立つ。利用例は次セクションで取り上げる。

Groovy なループ

値の範囲を通じた繰り返し処理を行うには6つの方法がある。

for

    for (i in 1..1000) { println i }

while

    i = 1
    while (i <= 1000) { println i; i++ }

each

    (1..1000).each { println it }

times

    1000.times { println it }
    // 0 から 999 まで。

upto

    1.upto(1000) { println it }

step

    1.step(1001, 1) { println it }
    // 1から1000まで。
    // パラメータの値のひとつ手前で終了する。

クロージャを受け付ける List/Map/String のメソッド

List、Map、そしてString のメソッドのなかにはクロージャをパラメータとして渡せるメソッドがある。

each

コレクションの要素または文字列内の文字に対して繰り返し処理を行う。java.util.Iterator の代わりに利用することで、より簡潔なコードを記述できる。たとえば、リストの要素の数値をそれぞれ表示させようと思えば、
[5, 9, 1, 6].each {x | println x}
あるいは
[5, 9, 1, 6].each {println it}
と書ける。

collect

コレクションまたは文字列を変換して、新しいコレクション(または文字列)を作成する。たとえば、リストの各要素の値を2倍した新しい要素を持つリストを作成するには、
doubles = [5, 9, 1, 6].collect {x | x * 2}
とする。これは doubles[10, 18, 2, 12] をセットする。

find

与えられた条件に合致するものをコレクションの要素また文字列内の文字から検索し、最初に出現した要素を返す。たとえば、リストの中から5よりも大きい最初の要素を検索したければ、
[5, 9, 1, 6].find {x | x > 5} と書くと 9 が返ってくる。

findAll

与えられた条件に合致するものをコレクションの要素または文字列内の文字から検索し、出現した要素すべての配列を返す。たとえば、リストの中から5よりも大きい要素をすべて検索したければ、
[5, 9, 1, 6].findAll {x | x > 5} と書くと [9, 6] が返ってくる。

every

コレクションの要素または文字列内の文字すべてが、与えられた条件に合致するかを調べる。たとえば、リストに含まれる数字がすべて7より小さいかを調べたいのであれば、
[5, 9, 1, 6].every {x | x < 7} と書くと false が返ってくる。

any

コレクションの要素または文字列内の文字に、与えられた条件に合致するものがひとつでも存在するかを調べる。たとえば、リストに含まれる数字に7より小さいものが含まれているかを調べたいのであれば、
[5, 9, 1, 6].any {x | x < 7} と書くと true が返ってくる。

inject

繰り返しの初回には、引数に渡された値が使われ、それぞれの繰り返しでの評価結果が次回以降に渡されていく。たとえば、5の階乗を求めるには、次のように書ける(あまり見かけないやり方ではあるが)。

    factorial = [2, 3, 4, 5].inject(1) {
      prevResult, x | prevResult * x
    }

このクロージャは、4回実行される。
1回目: 1 * 2
2回目: 2 * 3
3回目: 6 * 4
4回目: 24 * 5
結果は、120 になる。

ファイル I/O

ファイルから行を読み込む(2種類の方法)

サンプルコードの(...) はコードの省略を示す。

    file = new File('myFile.txt')
    file.eachLine { println it }
    lineList = file.readLines()

ファイルからバイト列を読み込む(2種類の方法)

    file = new File('myFile.txt')
    file.eachByte { println it }
    byteList = file.readBytes()

ディレクトリにあるファイルを読む

    dir = new File('directory-path')
    dir.eachFile { file | . . . }

読み込み完了時にリソースを閉じる

Reader または InputStream から読み込むこれらのメソッドは、例外が起きてもリソースを閉じることが保証されている。

    file.withReader { reader | . . . }
    reader.withReader { reader | . . . }
    inputStream.withStream { is | . . . }

書き込み完了時にリソースを閉じる

処理中に例外が投げられたとしても、Writer または OutputStream への書き込みを閉じることを保証する。

    file.withWriter { writer | . . . }
    file.withPrintWriter { pw | . . . }
    file.withOutputStream { os | . . . }
    writer.withWriter { writer | . . . }
    outputStream.withStream { os | . . . }

オーバーロードされた左シフト演算子

String に追加するには、

    s = 'foo'
    s = s << 'bar'

StringBuffer に追加するには、

    sb = new StringBuffer('foo')
    sb << 'bar'

List に追加するには、

    colors = ['red', 'green']
    colors << 'blue'

出力ストリームの末尾に追加するには、

    w = new File('myFile.txt').newWriter()
    w << 'foo' << 'bar'
    w.close()

オブジェクト・ナビゲーション

オブジェクト構造(Object Graphs)を XPath 風の文法で辿るには、ドット(".")演算子を使用する。NullPointerException の危険を回避するには、"->" を "." の代わりに使用する。たとえば、

    class Team {
      String name
      Person coach
      players = []
    }
    
    class Person {
      String name
    }

    p = new Person(name:'Mike Martz')
    t = new Team(name:'Rams', coach:p)

    // 次の行は、team.getCoach().getName と同じ結果を出力する
    println "coach = ${t.coach.name}"

    t = new Team(name:'Blues')

    // 次の行は null を返すのだが、
    // NullPointerException は発生しない
    println "coach = ${t->coach->name}"

    // 次の行は NullPointerException が発生する
    println "coach = ${t.coach.name}"

Groovy なリフレクション

あるオブジェクトの Class オブジェクトを取得したい場合に、Java では someObject.getClass() と書く。Groovy で同じことを行うには、someObject.class と書く。

クラス名から Class オブジェクトを取得したい場合は、Java でも Groovy でも SomeClass.class または Class.forName("pkg.SomeClass") と書く。

Groovy の GString クラスのメソッド名を一覧表示したければ、次のように書く。

    GString.class.methods.each { println it.name }

Java の java.util.List インタフェースのメソッド名を一覧表示したければ、次のように書く。

    java.util.List.class.methods.each { println it.name }

未実装メソッドをキャッチする

クラスには、未実装のメソッドに対する呼び出しをキャッチするコードを書くことができる。たとえば、

    o = new CatchCall()

    // 次の行は "unknown method Mark called with [19]" と表示する
    println o.foo("Mark", 19)

    class CatchCall {
      invokeMethod(String name, Object args) {
        try {
          return metaClass.invokeMethod(this, name, args)
        } catch (MissingMethodException e) {
          // ここに確実に呼び出せるメソッド名と引数での呼び出しを
          // 行うロジックを書いてもよい
          return "unknown method ${name} called with ${args}"
        }
      }
    }

Groovy Markup

Groovy Markup は 上述の invokeMethod メソッドを使用して存在しないメソッドの呼び出しをキャッチし、それを「ノード」に変換する。メソッドの引数はノードの属性として扱われる。メソッドに渡されたクロージャはノードのコンテンツとして扱われる。Groovy Markup では様ざまな利用法が用意されている:

これに加えて、独自のビルダを作成するには groovy.util.BuilderSupport クラスを継承すればよい。

HTML の生成

MarkupBuilder を使用して HTML を生成するサンプルを示す。

    import groovy.xml.MarkupBuilder

    mb = new MarkupBuilder()
    mb.html() {
      head() {
        title("This is my title.")
      }
      body() {
        p("This is my paragraph.")
      }
    }
    println mb[*8]

これが生成する HTML は次のようになる。

    <html>
      <head>
        <title>This is my title.</title>
      </head>
      <body>
        <p>This is my paragraph.</p>
      </body>
    </html>

XMLの生成

MarkupBuilder を使用して XML を生成するサンプルを示す。

    import groovy.xml.MarkupBuilder;

    mb = new MarkupBuilder()        
    mb.autos() {
      auto(year:2001, color:'blue') {
        make('Toyota')
        model('Camry')
      }
    }
    println mb[*9]

これが生成する XML は次のようになる。

    <autos>
      <auto year='2001' color='blue'>
        <make>Toyota</make>
        <model>Camry</model>
      </auto>
    </autos>

[*8]訳注: mb.html() メソッドの実行時点で標準出力にHTMLが出力されるので、サンプルの目的を考えるとこのステートメントは冗長。MarkupBuilderの実行結果を文字列としてmbに設定したい場合は懸田さんによるサンプルコードを参照。
[*9]訳注: mb.autos() メソッドの実行時点で標準出力にXMLが出力されるので、サンプルの目的を考えるとこのステートメントは冗長。MarkupBuilderの実行結果を文字列としてmbに設定したい場合は懸田さんによるサンプルコードを参照。

Groovy SQL

Groovy は JDBC を簡単にする。groovy.sql.Sql クラスはクエリの実行と ResultSet 行のイテレートを簡単に行う方法を提供する。次のサンプルでは、MusicCollection はデータベースの名前(この場合は、登録された ODBC データソース)で、 Artists がデータベースのテーブル名、Name がテーブルのカラム名だ。

    import groovy.sql.Sql

    dbURL = 'jdbc:odbc:MusicCollection'
    jdbcDriver = 'sun.jdbc.odbc.JdbcOdbcDriver'
    sql = Sql.newInstance(dbURL, jdbcDriver)
    sql.eachRow('select * from Artists') {
      println it.Name
    }

Groovlet

Groovlet は Groovy による、サーブレットと JSP の代替案である。Groovlet は以下の暗黙変数を提供する。

Groovlet のサンプルを示す。このコードは SimpleGroovlet.groovy みたいな名前で保存する。また、コードは HTML の出力にヒアドキュメントを使用している。

    out.println <<<EOS
    <html>
      <head>
        <title>My Simple Groovlet</title>
      </head>
      <body>
        <h1>My Simple Groovlet</h1>
        <p>Today is ${new java.util.Date()}.</p>
      </body>
    </html>
    EOS

GroovyServlet は Groovlet をコンパイルし、ファイルが変更されるまでキャッシュする。再コンパイルは変更時に自動的に行われる。 GroovyServlet は web.xml に登録しなければならない。

以下は web.xml ファイルのサンプルだが、GroovyServlet の登録部分だけしか示していない。

    <?xml version="1.0"?>
    <!DOCTYPE web-app
      PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" 
      "http://java.sun.com/dtd/web-app_2_3.dtd">
    <web-app>
      <servlet>
        <servlet-name>Groovy</servlet-name>
        <servlet-class>groovy.servlet.GroovyServlet</servlet-class>
      </servlet>
      <servlet-mapping>
        <servlet-name>Groovy</servlet-name>
        <url-pattern>*.groovy</url-pattern>
      </servlet-mapping>
    </web-app>

Groovlet のデプロイには Ant を使うのが良いだろう。基本的な手順は:

  1. 次の内容を含む WAR ファイルを作成する。
    • Groovlet ソースファイル (*.groovy) をトップレベルの階層に配置
    • WEB-INF ディレクトリに web.xml を配置
    • groovy*.jarasm*.jar ファイルを WEB-INF/lib ディレクトリに配置
  2. WAR ファイルをサーブレットエンジン(Tomcat 等)にデプロイする。

これを行うための Ant ビルドファイルはこうだ。

build.properties

    build.dir=build
    src.dir=src

    # Groovlet の置かれているディレクトリ
    groovy.dir=${src.dir}/groovy

    # web.xml の置かれているディレクトリ
    web.dir=${src.dir}/web

    # WAR ファイルの生成先パス
    war.file=${build.dir}/${ant.project.name}.war

    # WAR ファイルのデプロイ先
    webapps.dir=${env.TOMCAT_HOME}/webapps

    # WAR ファイルに含めるべき JAR ファイル
    asm.jar=${env.GROOVY_HOME}/lib/asm-1.4.1.jar
    groovy.jar=${env.GROOVY_HOME}/lib/groovy-1.0-beta-4-snapshot.jar

build.xml

    <project name="GroovletExample" default="deploy">
      <property environment="env"/>
      <property file="build.properties"/>

      <target name="prepare">
        <mkdir dir="${build.dir}"/>
      </target>

      <target name="war" depends="prepare"
        description="creates WAR file">
        <war destfile="${war.file}" webxml="${web.dir}/web.xml">
          <fileset dir="${groovy.dir}"/>
          <lib file="${groovy.jar}"/>
          <lib file="${asm.jar}"/>
        </war>
      </target>

      <target name="deploy" depends="war"
        description="deploys WAR file">
        <delete dir="${webapps.dir}/${ant.project.name}"/>
        <delete file="${webapps.dir}/${war.file}"/>
        <copy file="${war.file}" todir="${webapps.dir}"/>
      </target>

    </project>

サンプルの Groovlet がデプロイされたら、ウェブブラウザから http://localhost:8080/GroovletExamples/SimpleGroovlet.groovy でアクセスできる。GroovletExample は Web アプリケーションの名前で、SimpleGroovylet.groovy が Groovlet の名前だ。このマッピングは web.xml に登録されている GroovyServleturl-pattern で行われる。

問題点

Groovy はまだ完璧ではない。Groovy の抱える問題を見るには、 http://groovy.codehaus.org にアクセスして Issue Tracker のリンクをクリックすればよい。 ここでは、報告されている問題の中からいくつかをピックアップする。 割り当てられている問題番号も併記した。

[*10]訳注: 1.0-beta4 でサポートされた。
[*11]訳注: 1.0-rc1 でサポート予定か?
[*12]訳注: 1.0-beta4 でサポートされた。
[*13]訳注: 1.0-rc1 でサポート予定。
[*14]訳注: 1.0-beta4 でサポートされた。
[*15]訳注: 1.0最終リリースでサポート予定。
[*16]訳注: 1.1でサポート予定。

まとめ

さて本記事では…… Groovy の文法と機能のいくつかをざっくり流してみた。 Groovy の提供する Java ショートカットは、あなたの生産性を上げそうだろうか? もっと Groovy を楽しんでみたいと思っただろうか? コードは読みやすくなりそうだろうか? それとも読みづらくなりそうだろうか? あなたからのフィードバックをとても楽しみしている。是非、[email protected] にメールして欲しい[*17]。また、フィードバックは Groovy のメーリングリスト(ここに詳細がある)でも共有したいと思う。

[*17]訳注: 日本語訳に関するフィードバックは [email protected] へお願いします。

参考文献


Copyright © 2004. Object Computing, Inc. All rights reserved.
日本語訳: かくたに