6.リスト

ここでいうリストとは、配列、JavaでいうArrayListなどのことです。
リストを使うことによって、一つの変数の中に、あたかも複数の値が格納されているかのように振る舞います。

  • Javaの場合
List<Integer> lists = Arrays.asList(1, 2, 3);
  • PHPの場合
$list = array(1, 2, 3, 4)

// 5.4以降
$list = [1, 2, 3, 4];

Groovyではこれらを List として扱います。
例えば上記の例はGroovyだと以下のように表現します。

List list = [1, 2, 3, 4]

では、このlistの値をそれぞれ画面に表示してみましょう。
Javaのように書くと以下のようになります。

List list = [1,2,3,4]
for(def i = 0; i < list.size(); i++) {
    println list[i]
}

Listの宣言はGroovy独特ですが、for文はJavaと特に変わりはありませんね。
この方法でも特に問題はありませんが、Groovyではこれをもっとシンプルに実現できます。
それが以下のコードです。

List list = [1,2,3,4]
list.each {
    println it
}

とてもシンプルになりましたね!
この例ではGroovyのListにあるeachというメソッドを利用しています。
eachメソッドは、Listの中身をそれぞれ順番に処理するためのメソッドです。
eachメソッドが自動的にListの中身を先頭から順番に、{...}の中に記述した処理に渡してくれて、{...}を実行してくれます。
詳しくは別章で扱いますが、{...}の部分をClosure(クロージャ)と呼びます。
クロージャの中にあるitは暗黙的に利用できる変数で、itにはeachメソッドが自動的にListの要素を渡してくれます。
itを別名で宣言したり、型の宣言をつけたりもできます。

List list = [1,2,3,4]

// eachメソッドがlistの各要素を順番にvalに格納して{...}の中身を実行する。
list.each {val ->
    println val
}

// 型の指定もできる。
list.each {Integer val ->
    println val
}

// list変数に格納しなくても実行できる
[1,2,3,4].each {
    println it
}

Groovyでは基本的にfor文やwhile文を利用せずに、Listに存在するこのeachメソッドのような専用のメソッドを利用して繰り返し処理を実行します。
メリットとして、今まではlistとfor/whileという少なくとも二人の登場人物が必要でしたが、それがlistオブジェクトだけでよくなります。
さらに、for文がなくなるということはiなどの添え字変数が必要なくなります。
シンプルになる、変数が少なくなるということはそれだけバグが潜り込む箇所が少なくなるということになります。
今までfor文などでずっとループを書いていた場合は、このGroovy(を含め関数型プログラミング言語に見られる)のListの処理方法には違和感を感じるかもしれません。
しかし、一旦慣れてしまえば必ずこの方法が好きになるはずです!

当然ただ値を表示するだけではなく、計算したり加工したりもできます。

// 各要素を2倍して表示する
[1,2,3,4].each {
    println it * 2
}

// "-" という文字列を、各要素の個数表示する
// Groovyでは、Stringに数字を掛けることでかけた回数分Stringが繰り替えされたものが得られる
[1,2,3,4].each {
    println "-" * it
}

Groovyのリストにはいくつかとても重要なメソッドが用意されています。それぞれ見ていきます。

6.1.collect

collectメソッドを使うと、リストの各要素を順番に処理できます。
つまり、eachメソッドと同じですね。
しかし重要な違いとして、 実行結果を返す という点が有ります。
そのため、元々あったリストを変換するために用いられます。

assert [1,2,3].collect { it * 2 } == [2, 4, 6]

{ it * 2 }の中身が実行されています。
すでに述べたように、リストの各要素を {...}の中で変換して、返します。
それを自動的に新しいリストに詰めなおしてくれます。
注意点は、元々あったリストは変更されず、collectメソッドによって中身が変更された 新しいリストが返さえれる という点です。 実際に試してみましょう。

def list = [1,2,3]
def list2 = list.collect {
    it * 2
}

assert [1,2,3] == list
assert [2,4,6] == list2

元々のlistの値は変わっておらず、list2collectメソッドによってlistの中身を変更された新しいリストが格納されていますね。
なお、お分かりの通り、実際の変換処理である{...}の中にはreturnが書かれていませんね。
ifの章で述べた3項演算子の用に、態々returnを書かなくても、Groovyは最後に評価された値を返してくれます。

この リストの中身を変換して、新しいリストとして返す という機能、考え方はGroovyに限らず、関数型プログラミングと呼ばれるパラダイムでは非常に重要な概念となります。
この処理を(意図的に古い)Javaのオーソドックスなパターンで記述すると以下のようになります。

def list = [1,2,3]
def list2 = []

for( def i = 0; i < list.size(); i++ ) {
    list2.add( list[i] * 2 )
}

assert [1,2,3] == list
assert [2,4,6] == list2

パット見で明らかに冗長そう、という点もそうなのですが、上記の例だと、一旦list2という空のリストを生成して、for文の中でそのリストに値を追加していく、とう処理になっています。
この方式だと、実際に重要な処理をしているのは当然forなのですが、このforの中で外側の変数を使っている、という所に落とし穴が潜んでいます。
明らかに肝心な処理の外側に、関心を寄せる対象(list2という変数)が存在している点です。
コレがもし長い処理の末に実行されていたとしたら、常にその変数(list2)に気を配らなければなりません。
その点、collectの場合は、処理対象のリストから順番に値が渡され、その値を処理して返すだけなので、その肝心の処理をどうするかだけを考えれば良いわけです。

当然、必ずそうしなければならない、というわけではありません。場合によってはオールドスクールな方法のほうが良い場合も当然有ります。
場合によって使い分ければOKです。

6.2.inject

さて、次にinjectメソッドです。
このメソッドは慣れるまで非常に分かりづらいです。
なのでまずは実際に以下のコードを自分で実行してみてください。

assert [1,2,3].inject{a, b -> a + b} == 6

この短い例だとわかりやすいですね。どうやら[1,2,3]というリストの合計が求められたようです。
injectメソッドは、リストの先頭要素とその次の要素を{...}にそれぞれ渡して、その実行結果を返す、そして、その実行結果と次の要素を{...}にそれぞれ渡して、その実行結果を返す…という処理をリストの最後まで繰り返すメソッドです。

この例だと、
1. injectメソッドがリストの0番目と1番目の要素を抜き出してクロージャにそれぞれ渡す。(今回だと1と2という値)
2. クロージャではその渡された値を利用して処理をする。(1 + 2)
3. クロージャの処理結果がリターンされる。(3という計算結果)
4. injectはその実行結果を取得し、その実行結果と次の2番めの要素(今回は3という値)をクロージャに渡す。(つまり3と3という値)
5. クロージャではその渡された値を利用して処理をする。(3 + 3)
6. injectはその実行結果を取得し、もう全てのリスト要素を走査したので、最終的な実行結果である6を返す。

となります。
コレを応用して、リストから最大値を抜き出す、という処理を以下のように記述できます。

assert [5,3,6,10,8,0,1].inject {a,b ->
    a > b ? a : b
} == 10

さて、ではココでも(意図的に古い)Javaのオーソドックスなパターンで試してみましょう。

def list = [5,3,6,10,8,0,1]
Integer max = 0

for (def i = 0; i < list.size(); i++) {
    if ( max < list[i] ) {
        max = list[i]
    }
}

assert max == 10

collectの時と同じように、forの外側に実行結果を格納する変数を用意しなければなりません。
つまり同じような問題が発生する可能性がある、ということですね。

6.3.find

さて、ちょっと難しかったですがここから少し楽になります。
メソッド名から分かるように、findメソッドはリストの中から{...}で指定する条件に合致するものから最初のものを返します。

assert [1,2,3].find { it % 2 != 0} == 1

そのまんまですね。
注意点としては、条件に合致するものが複数あっても、その最初の1つしか返さないという点です。
条件に合致する全てのものが欲しい場合、次のfindAllメソッドを利用します。

6.4.findAll

リストの中から{...}で指定する条件に合致するものを全て返します。 複数の値が帰ってくるので、当然実行結果はリストになります。

assert [1,2,3].findAll { it % 2 != 0} == [1,3]

合致するものが1つだけであろうと、リスト形式で返されます。

assert [1,2,3].findAll { it % 2 == 0} == [2]

6.5.まとめ

どうだったでしょうか。
Rubyなどを使ったことがある人なら特に違和感の無い内容だとは思いますが、Java7までしか使ったことがない場合などは中々難しい内容だったのではないかと思います。
重要なのは、リストを処理するには、 リストの中身を変換して、新しいリストとして返す/別の値を返す ということです。

さて、 新しいリストとして返す という点に注目してみましょう。
collectは新しいリストを返します。つまり、その結果に対して更に別のリスト用メソッドを適用することが出来るのです!
それでは最後にサンぷるとして、1から10までの整数のうち、偶数だけを抜き出して、それらをさらに2倍して、合計を求める処理を書いてみます。
なお、この 1から10 のようにレンジを求める際、Groovyでは(1..10)と表現できます。それも合わせて見てみましょう。

assert 60 == (1..10).findAll {
    it % 2 == 0 // 偶数のものだけ抽出
}.collect {
    it * 2 // リストの中身をそれぞれ2倍に変換
}.inject {a,b ->
    a + b // リストの中身をそれぞれ足し合わせていく
}

コレ以外にも様々な便利なメソッドが存在します。
色々試してみましょう。