倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

あんたに GDK の何が分かるっていうの!? (Object 編3) コンテナ・メソッド 前編

今回は GDK が Object クラスに追加しているコンテナ・メソッド。 ちょっと多かったので前後編に分けます(後編は findXxxx() メソッド)。 コンテナ・メソッドとはコレクションや配列のような、要素を保持する入れ物を指して使っています。 今回扱うメソッドは、通常のオブジェクトに対しては「要素がそれ自身だけのコンテナ」として機能するのであまり便利というわけではありませんが、コレクションや配列に使う目的で定義されているようです*1。 今回扱うメソッドは以下の通り:

Object each(Closure closure)
Object eachWithIndex(Closure closure)

Collection grep(Object filter)
Collection grep()

List collect(Closure transform)
Collection collect(Collection collector, Closure transform)
Collection collect()

Object inject(Closure closure)
Object inject(Object initialValue, Closure closure)

boolean every(Closure closure)
boolean every()

boolean any(Closure closure)
boolean any()

Collection split(Closure closure)

Iterator iterator()

引数をとらない grep(), collect(), every(), any() は、Closure.IDENTITY (恒等変換、引数をそのまま返すクロージャ)を渡したのと同じになります。 各メソッドをもう少し詳しく表にするとこんな感じ:

メソッド 返り値 since 説明
each(Closure)
eachWithIndex(Closure)
Object 1.0 各要素を走査する
grep(Object)
grep()
Collection 1.5.6
1.8.1
引数のオブジェクトを isCase() によるフィルターとして適用する
collect(Closure)
collect(Collection, Closure)
collect()
List
Collection
Collection
1.0
1.0
1.8.5
各要素に変換を施して、結果のオブジェクトを集めたコレクションを返す
every(Closure)
every()
boolean 1.0
1.5.0
各要素に boolean 値を返すクロージャを適用し、その論理積を返す
any(Closure)
any()
boolean 1.0
1.5.0
各要素に boolean 値を返すクロージャを提供し、その論理和を返す
inject(Closure)
inject(Object, Closure)
Object 1.8.7
1.5.0
各要素を走査して値を計算する(リダクション)
split(Closure) Collection 1.6.0 各要素に boolean 値を返すクロージャを適用し、
true/false で分割した(多重)コレクションを返す
iterator() Iterator 1.0 各要素を走査する Iterator を返す

collect(Closure) の返り値が Collection ではなく List なのは何か意味あるのかな? まぁ、スクリプトとして使う分には大して型を気にする必要はないんですが。 それはともかく、以下でサンプルコードを見ていくに際してちょっと前提を。 まずは単なるオブジェクトに対して。

class Person{
    String name
    int age
}

new Person(name:'倭マン', age:100).each{ println it }

これを実行すると

Person@a1234b

のような表示がされ、通常のオブジェクトは自分自身のみを要素とするコンテナ・オブジェクトとして扱われていることがわかります。 これだと全然面白くないので、以下では List オブジェクトに対して各メソッドを実行して行きます。

each(), eachWithIndex() メソッド

each() メソッドは各要素を列挙して引数のクロージャを適用するメソッドです。 eachWithIndex() はクロージャの第2引数にインデックスが割り当てられます:

def lang = ['Java', 'Groovy', 'Scala', 'Clojure']

lang.each{ println it }
// Java
// Groovy
// Scala
// Clojure
// と表示される

lang.eachWithIndex{ s, i -> println "$i) $s"}    // クロージャの第2引数にはインデックスが割り当てられる
// 0) Java
// 1) Groovy
// 2) Scala
// 3) Clojure
// と表示される

each() は結構多用するメソッドですね。

grep() メソッド

grep() は引数のオブジェクトを isCase() メソッドをもとにしたフィルターとして適用するメソッドです。 isCase() メソッドを条件式として扱う方法は以前の記事でも見ました。

def lang = ['Java', 'Groovy', 'Scala', 'Clojure', 'Kotlin', 'Jython', 'JRuby']
def ints = (0..10)

// grep()
assert lang.grep{ it.startsWith 'J' } == ['Java', 'Jython', 'JRuby']    // フィルターとしてのクロージャ
assert lang.grep(~/J\w*/) == ['Java', 'Jython', 'JRuby']    // フィルターとしての正規表現
assert ints.grep() == (1..10)    // Closure.IDENTITY をフィルタとして使用(0は false)

まぁ、そんなに難しくはないかと。 ただし、次回やる予定の findAll() メソッドもフィルターとして使えるメソッドですが、少々区別が必要。 クロージャをフィルタとして使う分には同じように使えばいいんですが、findAll() メソッドはクロージャ以外のオブジェクトを引数としてとれません:

def lang = ['Java', 'Groovy', 'Scala', 'Clojure', 'Kotlin', 'Jython', 'JRuby']

assert lang.findAll{ it.startsWith 'J' } == ['Java', 'Jython', 'JRuby']    // grep() と同じ

// 次のようにはできない! findAll() はクロージャのみを引数としてとる
// assert lang.findAll(~/J\w*/) == ['Java', 'Jython', 'JRuby']

collect() メソッド

collect() メソッドは各要素に変換を施して新たなコレクションを生成するメソッド。

def lang = ['Java', 'Groovy', 'Scala', 'Clojure', 'Kotlin', 'Jython', 'JRuby']

assert lang.collect{ it.toLowerCase() } == ['java', 'groovy', 'scala', 'clojure', 'kotlin', 'jython', 'jruby']

assert lang.collect(new LinkedList()){ it.toUpperCase() } instanceof LinkedList

assert lang.iterator().collect() == ['Java', 'Groovy', 'Scala', 'Clojure', 'Kotlin', 'Jython', 'JRuby']

使用したいコレクションの型があるなら、2つ目のようにそのインスタンスを第1引数として渡します。 引数を録らない collect() は、Iterator のようなコレクションではないコンテナからコレクションを生成したいときや、コレクションのコピーが欲しいときに使うとよいようです。 ちなみに Groovy 特有の演算子*.」も collect() と似たような働きをします:

assert lang*.size() == [4, 6, 5, 7, 6, 6, 5]

collect() メソッドも結構多用するメソッド。

inject() メソッド

inject() メソッドは、リダクション用のメソッドです。 つまり、数値のリストに対してその要素の和を計算したりするのに使います。 和を計算する sum() メソッドは Collection クラスに定義されていますが、同じことを inject() メソッドで簡単に実行できます:

def ints = (0..10)

// inject()
assert ints.inject{ sum, i -> sum + i } == 55    // 0から10までの和
assert ints.grep().inject(1){ prod, i -> prod * i } == 3628800    // 1から10までの積

2つ目では0をかけるとまずいので grep() で取り除いています(0は false と評価されるので grep() のフィルターを通らない)。 1は初期値です。

every(), any() メソッド

次は論理演算の every() と any()。 各要素に対して引数のクロージャ(boolean 値を返す)を適用し、その結果に対して論理演算を施します。 every() が論理積(AND)、any() が論理和(OR)です。 英単語の意味を考えれば分かるかと。 サンプルはこんな感じ:

def lang = ['Java', 'Groovy', 'Scala', 'Clojure', 'Kotlin', 'Jython', 'JRuby']
def falseObjects = [0, 0.0, '', [], [:], [].iterator(), null]
def ints = (0..10)

// every()
assert lang.every{ it.size() >= 4 }
assert !lang.every{ it.contains 'oo' }

assert !falseObjects.every()    // すべて false
assert !ints.every()    // 0が false

// any()
assert lang.any{ it.contains 'oo' }
assert !lang.any{ it.contains 'z' }

assert !falseObjects.any()    // すべて false
assert ints.any()    // 0が false

split() メソッド

split() メソッドは各要素に引数のクロージャ(boolean 値を返す)を適用し、true を返すもの、false を返すものをそれぞれまとめてリストにして、そのリスト(リストのリスト)を返します:

def lang = ['Java', 'Groovy', 'Scala', 'Clojure', 'Kotlin', 'Jython', 'JRuby']
assert lang.split{ it.contains 'o' } == [
    ['Groovy', 'Clojure', 'Kotlin', 'Jython'],    // it.contains('o') == true
    ['Java', 'Scala', 'JRuby']    // it.contains('o') == false
]

まぁ、やってることは簡単ですね。

どれもコンテナ・オブジェクトのためにあるメソッドばかりでしたね。 次回はコンテナ・メソッドの後編。 findXxxx() という名前のメソッドを見ていきます。

プログラミングGROOVY

プログラミングGROOVY

*1:コレクションと配列には継承関係がないので、Object クラスにこれらを定義するしかなかったんじゃないかと。