Help us understand the problem. What is going on with this article?

【Monthly 2017/04】groovy入門!! ~Java文法・仕様比較~

More than 3 years have passed since last update.

はじめに!!

イェイ!! ノってるかい!?!!?!!!!
ということで、いまさらながらgroovyを学んでいきたいと思います。(ということで?)
まあ初心者なので内容が初歩的なのは大目に見てくださいww!!
(妙なテンションはここまでです・・・)

学ぶ動機は今後のJava活をより良いものにするということです。
気軽にライブラリ試したり、ツールを作ったりしたいのです。

まあ、やるなら基礎からとことんやりたい性分なので今回の内容は、ほとんどgroovyの言語仕様の把握です。

初めての投稿なのであまり上手くないですが(しかもふざけてますね・・・)
なんとか読めるものになっていれば幸いです。

Hello World!!

入門といえばHello World。まずはここから比較を始めていきます。
以下の両言語のHello Worldをご覧ください。

java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello java!!");
    }
}

/* 出力
Hello java!!
*/
groovy
System.out.println("Hello groovy!!"); // javaと同じ。でも動いちゃう。
System.out.println("Hello groovy!!")  // セミコロンなしでも動く。
System.out.println "Hello groovy!!";  // ()つけるのはもうやめよう。
System.out.println "Hello groovy!!"   // ;もついでにやめちゃおう。
println "Hello groovy!!"              // The groovy. こんなに簡略化できちゃう。

/* 出力
Hello groovy!!
Hello groovy!!
Hello groovy!!
Hello groovy!!
Hello groovy!!
*/

これだけでもgroovyのノリノリな感じ(※自由と言いたい)がおわかりいただけると思います。
printlnメソッドでの出力だけでも何通りもの書き方がありますね。
個人的に良いと思うのはSystem.outを書かなくてもいい点です。これだけでもコードがすっきりして見えるので。

逆にjavaでは上記以外の書き方が思いつきませんね。
人数の多いプロジェクトではメンテナンス性が高いかもしれませんが、
趣味とか簡単なツール作成ならgroovyがいいなって感じです。

型宣言

ここからは変数に関する比較を行います。
いちいちprintlnで出力するのは面倒なので、変数を[ 型 : 値 ]の形で出力するdisp()を定義しておきます。
コードはdisp()が定義されている前提で読んでください。

java
/**
 * [ 型 : 値 ]の形式で引数を出力する
 *
 * @param variable 変数
 */
static void disp(Object variable) {
   System.out.println("[ " + variable.getClass().toString() + " : " + variable.toString() + " ]");
}
groovy
/**
 * [ 型 : 値 ]の形式で引数を出力する
 *
 * @param variable 変数
 */
def disp(variable) {
    println "[ " + variable.class.toString() + " : ${variable} ]"
}

あ、groovy書きやすい・・・

文字列

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

        String jString = "java string";

        disp(jString);
    }
}

/* 出力
 * [ class java.lang.String : java string ]
 */
groovy
def gString = "groovy string"   // defで宣言
def gString2 = "${gString}"     // defで宣言。文字列中に${変数}を埋め込む

String jString = "java string"  // String型で宣言
String jString2 = "${jString}"  // String型で宣言。文字列中に${変数}を埋め込む

def gString3 = """\
一行目
二行目
三行目"""                       // """で囲んで、複数行の文字列を表現。\で改行をエスケープ可能

def gString4 = 'groovy string' // 'で囲んでもOK

disp gString
disp gString2
disp jString
disp jString2

disp gString3
disp gString4

/* 出力
 * [ class java.lang.String : groovy string ]
 * [ class org.codehaus.groovy.runtime.GStringImpl : groovy string ]
 * [ class java.lang.String : java string ]
 * [ class java.lang.String : java string ]
 * [ class java.lang.String : 一行目
 * 二行目
 * 三行目 ]
 * [ class java.lang.String : groovy string ]
 */

String型で注目したいのは、値の文字列中に${変数}を埋め込んだ際の動きです。
groovyは"で囲まれた${変数}の値を文字列中に展開してくれるのですが、挙動が宣言の仕方で変化します。

  • defによる宣言の場合は${変数}の値を参照として持ち、展開する変数値が変化すると文字列に反映されます。
  • Stringによる宣言の場合は、宣言時の変数値が文字列中に展開され、変数値が変化しても文字列には反映されません。

余談として、遅延評価の為の記法があります。
defによる宣言の場合に値として${-> 変数}を与えると変数の参照先が変化しても、その参照の変化を反映させることができます。
この場合は${変数}の値をクロージャとして持っています。表示の際にクロージャが返却する変数の値を展開する為、参照先が変化しても値の反映が可能となっています。

def string = ["groovy string"]

String gStringValue = "${string}"     // 値埋め込み
def gStringReference = "${string}"    // 参照
def gStringLazy = "${-> string}"      // 遅延評価

string.add "changed"

disp gStringValue
disp gStringReference
disp gStringLazy
println()

string = "replaced"

disp gStringValue
disp gStringReference
disp gStringLazy

/* 出力
 * [ class java.lang.String : [groovy string] ]
 * [ class org.codehaus.groovy.runtime.GStringImpl : [groovy string, changed] ]
 * [ class org.codehaus.groovy.runtime.GStringImpl : [groovy string, changed] ]
 * 
 * [ class java.lang.String : [groovy string] ]
 * [ class org.codehaus.groovy.runtime.GStringImpl : [groovy string, changed] ]
 * [ class org.codehaus.groovy.runtime.GStringImpl : replaced ]
 */

上記のような挙動を示すのは、出力結果から読み取れるように、変数の参照や遅延評価を行う場合は文字列型の実装として
org.codehaus.groovy.runtime.GStringImplが使用されている為です。
使いこなせればとても便利な機能です。

数値

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

        int jInt = 1;                                                            // int
        long jLong = 100000000000000L;                                           // 容積かよ
        BigInteger jBigInteger = new BigInteger("1000000000000000000000000000"); // 多分これが一番手軽

        disp(jInt);
        disp(jLong);
        disp(jBigInteger);

        double jDouble = 1.212121;                            // double
        float jFloat = 1.212121F;                             // 静電容量かよ
        BigDecimal jBigDecimal = new BigDecimal(1.212121);    // 少数値で渡すと尻尾ができちゃう
        BigDecimal jBigDecimal2 = new BigDecimal("1.212121"); // 多分これが一番手軽

        disp(jDouble);
        disp(jFloat);
        disp(jBigDecimal);
        disp(jBigDecimal2);
    }
}

/* 出力
 * [ class java.lang.Integer : 1 ]
 * [ class java.lang.Long : 100000000000000 ]
 * [ class java.math.BigInteger : 1000000000000000000000000000 ]
 * 
 * [ class java.lang.Double : 1.212121 ]
 * [ class java.lang.Float : 1.212121 ]
 * [ class java.math.BigDecimal : 1.2121210000000000039932501749717630445957183837890625 ]
 * [ class java.math.BigDecimal : 1.212121 ]
 */
groovy
def gInt = 1                                    // intで収まる
def gLong = 100000000000000                     // intじゃ収まらない
def gBigInteger = 1000000000000000000000000000  // Longにすら収まらない

disp gInt
disp gLong
disp gBigInteger
println()

def gBigDecimal = 2.121212                     // 少数を入れてみる
double jDouble = 2.121212                      // doubleで宣言
float jFloat = 2.121212                        // floatで宣言

disp gBigDecimal
disp jDouble
disp jFloat

/* 出力
 * [ class java.lang.Integer : 1 ]
 * [ class java.lang.Long : 100000000000000 ]
 * [ class java.math.BigInteger : 1000000000000000000000000000 ]
 *
 * [ class java.math.BigDecimal : 2.121212 ]
 * [ class java.lang.Double : 2.121212 ]
 * [ class java.lang.Float : 2.121212 ]
 */

groovyは数値を扱う場合、かなり優秀です。
上記コードのように、整数値の場合は代入された値に応じてintBigIntegerまでの型を自動で判断して扱ってくれますし、
少数値の場合は一律でBigDecimalとして扱います。少数値をそのまま渡してもjavaのように誤差の尻尾がつかないのも魅力です。
もちろん型を指定すれば、指定した型として扱ってくれます。

その他プリミティブ型

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

        char aznable = 'c';    // 赤い彗星
        disp(aznable);

        boolean jBool = true; // boolean型
        disp(jBool);
    }
}

/* 出力
 * [ class java.lang.Character : c ]
 * [ class java.lang.Boolean : true ]
 */
groovy
def gChar = 'c'
def gChar2 = 'c' as char
def gChar3 = 'c' as String
char gChar4 = 'c'

disp gChar
disp gChar2
disp gChar3
disp gChar4

/* 出力
 * [ class java.lang.String : c ]
 * [ class java.lang.Character : c ]
 * [ class java.lang.String : c ]
 * [ class java.lang.Character : c ]
 */


def gBool = true
def gBool2 = "true" as boolean
boolean gBool3 = true
boolean gBool4 = "true"

disp gBool
disp gBool2
disp gBool3
disp gBool4

/* 出力
 * [ class java.lang.Boolean : true ]
 * [ class java.lang.Boolean : true ]
 * [ class java.lang.Boolean : true ]
 * [ class java.lang.Boolean : true ]
 */

char

groovyにchar型のリテラル(値)表記はありません。
char型として文字を扱いたい場合はStringリテラルをchar型で宣言した変数に代入するか、asキーワードでchar型だと明示する必要があります。
また、上記のコードには無いですが、(char)でキャストしてchar型として扱うことも可能です。

boolean

booleanに関しては上記のコード通りです。(手抜きじゃない。書くことが無いんだ)

List&配列&範囲

ここからはめんどくさくなってきたので比較する必要性が薄い場合はgroovyコードのみ掲載します。
『今までも大してなかったのでは?』なんて言っちゃ駄目。
以下サンプル。

groovy
def list = [1, 2, 3]
def linkedList = [1, 2, 3] as LinkedList
LinkedList linkedList2 = [1, 2, 3]

def array = [1, 2, 3] as int[]

def range = 1..3

disp list
disp linkedList
disp linkedList2
disp list[0]
println()
disp array
disp array[0]
println()
disp range
disp range[0]

/* 出力
 * [ class java.util.ArrayList : [1, 2, 3] ]
 * [ class java.util.LinkedList : [1, 2, 3] ]
 * [ class java.util.LinkedList : [1, 2, 3] ]
 * [ class java.lang.Integer : 1 ]
 *
 * [ class [I : [1, 2, 3] ]
 * [ class java.lang.Integer : 1 ]
 *
 * [ class groovy.lang.IntRange : [1, 2, 3] ]
 * [ class java.lang.Integer : 1 ]
 */

Listも配列もRangeも同じ構文で要素を取得できます。簡単。
しかし、配列のクラス名がなんのこっちゃって感じです。
ちなみにint配列のクラスは[I、boolean配列は[Z、Stringは[Ljava.lang.String;でした。
なんか気持ち悪いですね・・・

Map

groovy
def map = [ name : "ぽんた", age : 20, attend : true]
def hashMap = [ name : "ぽんた", age : 20, attend : true] as HashMap
TreeMap treeMap = [ name : "ぽんた", age : 20, attend : true]

disp map
disp hashMap
disp treeMap
println()
println map.getClass()
println hashMap.getClass()
println treeMap.getClass()
println()
disp map.name
disp map.age
disp map.attend
println()
disp map["name"]
disp map."name"

/* 出力
 * [ null : [name:ぽんた, age:20, attend:true] ]
 * [ null : [name:ぽんた, age:20, attend:true] ]
 * [ null : [age:20, attend:true, name:ぽんた] ]
 *
 * class java.util.LinkedHashMap
 * class java.util.LinkedHashMap
 * class java.util.TreeMap
 *
 * [ class java.lang.String : ぽんた ]
 * [ class java.lang.Integer : 20 ]
 * [ class java.lang.Boolean : true ]
 *
 * [ class java.lang.String : ぽんた ]
 * [ class java.lang.String : ぽんた ]
 * [ class java.util.ArrayList : [true, 12, string] ]
 * [ class java.lang.Boolean : true ]
 * [ class java.lang.Integer : 12 ]
 * [ class java.lang.String : string ]
 */

出力を見た瞬間一番驚きました。だってclassがnullなんだもん。
これは間違いなくバグだろ、と思いましたが調べてみると仕様です。
これを説明するには2つのgroovyの仕様を知る必要があります。

  • オブジェクトのgetterをフィールドにアクセスする構文でよびだせる。(Object#getClass()Object.classで呼び出せる)
  • Mapオブジェクトのvalueをフィールドにアクセスする構文でよびだせる。(keyがnameの場合はmap.nameで呼び出せる)

上記の仕様の競合を後者を優先する形で解決したために、今回の不幸が起こったようです。(存在しないkeyにアクセスするとnullが返る)
きちんとgetClass()を呼び出せばクラスを取得できました。
全部LinkedHashMapですが・・・
言語仕様なんでしょうかね?
ここはよくわかりませんでした・・・

上記は表示する変数ミスってたせいでした(全部mapとか恥ずかしい)
@uehajさん、ありがとうございます。

余談ですがgroovyはmap."name"のような形でもフィールドなどにアクセスできます。
${変数}と組み合わせたら軽い黒魔術ができそうです。

まとめ

javaは静的型付け言語groovyは動的型付け言語です。
javaは変数宣言を行う際に、変数の型を明示しなければなりませんが、
groovyはdefでOKです。defで変数を宣言した場合は、変数に格納されるリテラル(値)によって型が動的に変化します。
ただし、groovyでは静的に型を宣言することも可能です。
さらに、Stringの例のようにgroovyではリテラルの書き方などで、かなり柔軟な動的型付けを行っています。

検証環境

Groovy-2.4.10
jdk1.8.0_60
IntelliJ IDEA Community Edition 2017.1.2

参考サイト

http://groovy-lang.org
https://www.slideshare.net/nobeans/javagroovy
http://npnl.hatenablog.jp/entry/20100605/1275736594
http://qiita.com/abey1192/items/661766fb728fd9b872e2
http://qiita.com/opengl-8080/items/46790e2292cdd0beb1af

P.S

全然関係ないですがIntelliJ IDEAが滅茶苦茶使いやすかったです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away