No Programming, No Life

プログラミング関連の話題や雑記

Java使いをGroovyに引き込むサンプル集

はじめに

Java使いをScalaに引き込むサンプル集 | mwSoft のGroovy版を書いてみました。
記事中に登場するサンプルコードや文章など多くの部分を引用させていただいております。
(動作確認: Groovy Version: 1.7.7 JVM: 1.6.0_22)

前書き

Groovyという言語をご存知ですか?
Javaと同じくコンパイルされるとclassファイルになり、実行時はJVM上で動作し、またスクリプトとしても記述可能なオブジェクト指向スクリプト言語です。
Groovyは後発の言語ということもあって、Javaを書いている時に感じる冗長さに対する様々な解が用意されています。
本記事では、GroovyとJavaのコードを比較しながら、JavaユーザがGroovyに移った際に得られるメリットを提示していきます。
尚、序盤のサンプルコードはJavaユーザに伝わりやすいように、returnを明記したり、メソッドは必ず{}で囲むなど、極力Javaっぽい記述をしています。

妥当なJavaコードはほぼそのまま妥当なGroovyコードです

極端な例を言えば、Hoge.javaの拡張子をHoge.groovyにするだけでGroovyコードのできあがりです。
ステップを追ってJavaをGroovyに変換する例は をご参照下さい。

初期化するだけで折り返しが必要になったこと、ありませんか

Javaを使っていると、大した処理でもないのにやたらと長い記述が必要になることがあります。たとえば初期化Javaには長い名前のクラス名がたくさんあります。
以下はDOMでXMLを解析する際の前処理です。

【Java】
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("foo.xml"));

ただ初期化するだけだというのに、ひどく長い文字数が費やされています。Genericsの記述などが絡めば、初期化だけで平気で折り返しが必要になることもあります。
Groovyではまず、動的型付けにより、クラス名を明記しなくても動的に動作します。
なので、記述的にはこんな風になります。

【Groovy】
def factory = DocumentBuilderFactory.newInstance()
def builder = factory.newDocumentBuilder()
def doc = builder.parse(new File("foo.xml"))

動的型付けのお陰で、記述がちょっとスッキリしました。

さらにGroovyでは、importしたクラスに別名を付ける機能も用意されています。

別名importは下記のような記述になります。

import javax.xml.parsers.DocumentBuilderFactory as DBFactory

これを利用した上で、最初のJavaのコードを記述すると、こんな風になります。

【Groovy】
def factory = DBFactory.newInstance()
def builder = factory.newDocumentBuilder()
def doc = builder.parse(new File("foo.xml"))

最初のJavaのコードに比べると自明な冗長な部分が減っています。
また、動的型付けによって余分なimportも減ります。Javaの例では3つのクラスをimportする必要がありますが、Groovyの例ではDocumentBuilderFactoryのみimportすれば、残りの2つは実行時に勝手に解決してくれます。

Listや配列の初期化が面倒だと思ったこと、ありませんか

JavaではListや配列の初期化がけっこう面倒です。普通に配列を初期化するだけなら、こんな風に書けます。

【Java】
String[] array = {"abc", "def", "ghi"};

これは楽なのですが、上記の式が使えるのは初期化の時のみ。下記のような記述はエラーになります。

【Java】
String[] array = null;
array = {"abc", "def", "ghi"};

なので初期化以外の場所、たとえば引数の中でさくっと配列を渡したい場合は、こんな記述になります。

【Java】
foo(new String[] { "abc", "def", "ghi" });

Listの初期化の場合は、Arrays.asListを使うのが速いでしょうか。

【Java】
Arrays.asList("abc", "def", "ghi")

Groovyではリテラルとしてリストを提供しています。["要素1", "要素2", ...] as String[] のように書けば配列が、["要素1", "要素2", ...] のように書けばList*1が生成できます。

【Groovy】
def array = ["abc", "def", "ghi"] as String[]
def list = ["abc", "def", "ghi"]

引数の中でも、再代入の際でも、どこでもこれ1つでListやArrayを生成できるので、Javaよりちょっと楽です。

デフォルト引数を欲しいと思ったこと、ありませんか

Javaでは引数にデフォルト値を入れたい場合、メソッドを複数個定義する必要が出てきます。

例えば、消費税を計算する際に、デフォルトを5%にして、任意で税率を引数に渡せる処理を書く場合、以下のように2つメソッドを定義します。

【Java】
public int shohizei(int kane) {
    return shohizei(kane, 1.05f);
}
public int shohizei(int kane, float zei) {
    return (int) (kane * zei);
}

Groovyではスクリプト言語などで良く用いられているデフォルト引数が用意されています。
デフォルト引数を使えば、以下のように1つのメソッドを定義するだけで上のコードと同じ振る舞いをさせることができます。

【Groovy】
def shohizei(yen, rate = 1.05) {
    (yen * rate).toInteger()
}

Groovyではメソッド呼び出し時に名前付きで渡す方法も用意されています。
この方法を使えば、以下のように3つの引数のうち、2つ目と3つ目に値を指定するという記述もできるようになります。

def foo(Map param) {
  println(
    (param?.c1 ?: 'A') +
    (param?.c2 ?: 'B') +
    (param?.c3 ?: 'C')
  )
}
// そのまま呼び出すとデフォルト引数が適用される
foo()
  // => ABC

// 名前付きで指定すれば、2つ目と3つ目に適用するような指定もできる
foo(c2: 'E', c3: 'F')
  // => AEF

便利。

補足
  • ?.演算子はレシーバがnullだった場合、NullPointerExceptionを発生させず、単にnullを返却します。
  • ?:演算子*2は a ? a : b だった場合に a ?: b と書けるようにした略記法です。
  • ここはScalaの名前付き引数のほうが便利な気がする。

throwsを書くのを面倒だと思ったこと、ありませんか

Javaでは発生しうる例外をメソッドの宣言時に記述するthrowsというキーワードがあります。

【Java】
public Connection getConnection() throws SQLException {
    return DriverManager.getConnection("jdbc:sqlite:hoge.sqlite3");
}

throwsを指定されたメソッドは、利用する際に指定された例外をcatchかthrowsする記述を入れないとコンパイルエラーになるので、例外処理を忘れずに記述できるという点で効果があります。

ただ、多くの開発現場でそれらは適切に使われずに、RuntimeExceptionを継承したオリジナルの例外でラップしたり、面倒だからExceptionでひとまとめにcatchするなど、適切に使われていないケースも良く見かけます。

【Java】
// 面倒だからExceptionでまとめてthrows
public void hoge() throws Exception {
    // 処理
}
// RuntimeExceptionでラップしてthrow
public void fuga() {
    try {
        // 処理
    } catch (Exception e) { 
        throw new RuntimeException(e);
    }
}

便利な機能ではあるけど、人間が管理しようとするとthrowsはけっこうヘビーです。
というわけで、Groovyではthrowsを亡き者にしました。

【Groovy】
// throwsを指定しなくても呼び出せる
def getConnection() {
    DriverManager.getConnection("jdbc:sqlite:hoge.sqlite3")
}

Javaのクラスと連携するために、アノテーションでthrowsを指定することはできますが、Groovyだけを使ってる場合は関わることはなくなります。

文字列の比較は罠だと思ったこと、ありませんか

Javaの文字列の比較は、equalsメソッドを利用します。

== を使用した場合は同一の参照先を指しているかを比較することになるので、その文字列を生成した際の状況によって結果が変わったりします。

【Java】
String str1 = "テスト";
String str2 = "テスト";
String str3 = new String("テスト");

System.out.println(str1 + " == " + str2 + " = " + (str1 == str2));
  //=> テスト == テスト = true
System.out.println(str1 + " == " + str3 + " = " + (str1 == str3));
  //=> テスト == テスト = false

慣れてしまえばそれまでなんですが、ここは多くの人が一度は引っかかるポイントです。

Groovyはintやdoubleといった数値についてもプリミティブ型ではなくオブジェクト*3なので参照先を比較する == の出番というのはほとんどなくなってしまいます。
というわけで、Groovyでは == は値を比較するようになりました。

【Groovy】
def str1 = "テスト"
def str2 = "テスト"
def str3 = new String("テスト")

println "${str1} == ${str2} = ${str1 == str2}"
  //=> テスト == テスト = true
println "${str1} == ${str3} = ${str1 == str3}"
  //=> テスト ==  テスト = false

new String()した値も、== すればtrueと判定されます。
参照先の比較を行う場合は、isを用います。

【Groovy】
println "${str1} is ${str2} = ${str1.is(str2)}"
  //=> テスト is テスト = true
println "${str1} is ${str3} = ${str1.is(str3)}"
  //=> テスト is テスト = false
補足
  • Groovyでは文字列に ${...} という形式で変数や式を埋め込むことができます。

分かりきってる記述は省略したいと思ったこと、ありませんか

Groovyは省略できることは省略することを良しとしている言語です。

例えばJavaで2つの数を足して返すメソッドを記述すると、以下のようになります。

【Java】
public int sum(int i1, int i2) {
    return i1 + i2;
}

Groovyで上記のコードを極力似たように記述すると、こうなります。

【Groovy】
public int sum(int i1, int i2) {
    return i1 + i2;
}

そうですね、Javaと全く同じ記述ですが、これでも立派なGroovyコードです。
さて、省略できるところを1つずつ削っていってみましょう。
まず、Groovyではデフォルトの可視性はpublicなので省略可能です。

【Groovy : public抜き】
int sum(int i1, int i2) {
    return i1 + i2;
}

行末がセンテンスの切れ目になることは自明のことです。わざわざ自明のことのためにセミコロンを何度もタイプするのは無駄です。Groovyでは1行に2つの文を詰め込みたい時を除いてセミコロンは必要ありません。

【Groovy : セミコロン抜き】
int sum(int i1, int i2) {
    return i1 + i2
}

Groovyでは最後に評価した値が自動的に戻り値になります。なのでreturnはいりません。

【Groovy : return抜き】
int sum(int i1, int i2) {
    i1 + i2
}

型の指定はなくてもダックタイプが働きます。

【Groovy : 型指定抜き】
def sum(i1, i2) {
    i1 + i2
}

ついでにワンライナーにしましょう。

【Groovy : 型指定抜き】
def sum(i1, i2) { i1 + i2 }

ふぅ、スッキリしました。

このようにGroovyはいろんな記述を省略できます。最低限の可読性を保ちつつJavaコードから推測しやすいレベルですので、慣れてしまえばタイプ数が減らせるありがたい機能だと思えるようになります。

補足
  • ちなみに残念ながら引数がないメソッドの場合でも括弧は省略はできません。def hello() {} が最大限に省略した形です。

try catchの記述を共通化したくなったこと、ありませんか

Javaでコードを書いていると、いろんな箇所にtry catchの記述が現れます。

【Java】
try {
    // 処理
} catch (SQLException e) {
    e.printStackTrace();
}

このよくある処理を部品化しようとすると、インタフェースを作ってそれを実装したクラスを共通のtryが入ったクラスの中で呼び出すような、ちょっとした構造を考える必要があります。

Groovyには引数に処理ブロックを渡すことができるクロージャ(Closure)があります。これを使えばtry catchとか、最後に必ずcloseするとか、そういった定形の処理を簡単に記述できます。

たとえばこんな感じです。

【Groovy】
// メソッドを引数に取って、try catchの中で実行させるメソッド
def tryCatch(Closure proc) {
    try {
        return [proc.call()] // 結果をリストに詰めて返却
    } catch (Exception e) {
        return [] // 空リストを返却
    }
}

// 上のtryCatchメソッドを使って割り算するメソッド
def divide(i, j) {
  tryCatch {
    (i / j) as int
  }
}

// 実行してみる
divide(10, 3)
  // => [3]

divide(10, 0)
  // => []

まずtryCatchという、例外が起きたら空リスト()を、それ以外だったら処理した結果を返すメソッドを用意します。結果をリストでラップすることで失敗時を表現しています。((ScalaだったらOption[T]があるので便利なのですが、Groovyには残念ながらない。))
次にdivideというメソッドで、tryCatchメソッドを呼び出しつつ、中で i / j を実行するように記述しています。
divideを呼び出すと、iをjで割った値をintにキャストして返します。この時、0で除算すると当然例外が起きるわけですが、その場合はtryCatchの中でcatchされて空リスト(
)が返ります。このtry catchはどんな処理に対してでも適用できます。
クロージャ(Closure)を使うと、Javaでは部品化するのに記述量がけっこうかかりそうなことが、こんな風に割とあっさり記述できます。

補足
  • ちなみに上記の例でtryCatchメソッドでreturnが省略できないのは、try-catchが値を返さないからです。

Listを回して中身を処理することって、多いよね

プログラムを書いていると、リストの中身を回してちょっと加工して、新しいリストを作る、という要件に頻繁に出くわします。
Groovyには、ループを記述してその中に処理を書きこまなくても簡単にデータの加工ができる、便利な機能が大量に用意されています。たとえば以下のような。

【Groovy】
// こんなListがあったとさ
def list = [1, 2, 3, 5, 3, 5, 7, 8]

// ユニークな要素を抽出する (注:#unique()は破壊的!)
list.unique()
  // => [1, 2, 3, 5, 7, 8]

// 偶数と奇数で分けてみる
list.groupBy{ it % 2 }
  // => [1:[1, 3, 5, 3, 5, 7], 0:[2, 8]]

// 合算してみる
list.sum()
  // => 34

// 左から順に乗算していく
list.inject(1){ x, y -> x * y }
  // => 25200

// すべての値を2倍したListを作る
list.collect{ it * 2 }
  // => [2, 4, 6, 10, 6, 10, 14, 16]

// 2つの要素の差分を取る
[1, 2, 3, 4, 5] - [1, 4, 5]
  // => [2, 3]

しかもこれらの機能は、GroovyがJavaのListを拡張しているため、何もしなくてもそのまま利用することができます。

補足
  • コメントでも書いてありますが、#unique()はリストに対して破壊的操作を施します。よって、新しいリストのインスタンスが返ってくるわけではなく、リストの内容が直接書き換えられてしまいます。

後書き

というわけで、Java使いの皆さんに馴染み深そうな部分でGroovyのメリットを並べてみました。
Groovyはまだまだ日本では普及しておらず、そもそもそんな言語があることすら知らないという人がほとんどだと思います。Groovyは動的言語ということでIDEサポートが弱かったり、動作がJavaより遅いと言われていたりします*4。Groovyを仕事で使う機会なんてほとんど出会えなかったり等、新興の言語にありがちな問題をいくらか抱えてます。それらの問題が十分に解決されれば、今後、この言語が主流になっていくこともありえるんじゃないかと個人的には予想しているのですが、どうでしょう。
Groovyは元記事Scalaと同じJavaVM上で動く言語ということで、いわば兄弟のような存在です。ここの記事で紹介された内容は今日、今すぐにでも始められるような内容が多いと思います。Scalaが取っているアプローチと比較しながら楽しんでいただけたら幸いです。

おわりに

素敵な記事を書いていただいた元記事作者(mwSoft様)に感謝致します。

*1:正確にはjava.util.ArrayList です。["要素1", "要素2", ...] as LinkedList などのように as でキャストできます。

*2:エルビス演算子と呼ばれています。

*3:intはjava.lang.Integerの別名, doubleはjava.lang.Doubleの別名

*4:ただこれらについては[http://code.google.com/p/groovypptest/:title=Groovy++]という面白い試みも現在進行中で成されており、今後の動向に期待を寄せているところです