最近お仕事でJava+Groovyを使っています。
Groovyを使うと、Javaのまどろっこしいところを気持ちよく書けます。
Javaのお仕事をしながらスクリプト言語に触れることができ、他の言語を学習する際にも良い影響を受けています。
で、最近知ったGroovyのソートキーの挙動についてちょっとハマったので残しておく。
GroovyでのCollectionのソート
GroovyでCollectionをソートするとき、ソートの引数としてjava.util.Comparatorの他に、クロージャを渡すことができます。
ソート対象のオブジェクトのプロパティ値を返すクロージャーを渡すことで、そのプロパティ値をソートキーとすることができるのです。
def member = [[name:"a", age:22],[name:"ab", age:18],[name:"abc", age:42],[name:"b", age:33]] println member.sort{it.name} //-> [[name:a, age:22], [name:ab, age:18], [name:abc, age:42], [name:b, age:33]]
Google先生でこのソート方法を調べていると
「ソート対象のオブジェクトのプロパティ値をリストで返せば、第1ソートキー・第2ソートキーとして使用することができる」
という情報がちらほら。なんと便利な。
しかし、Grな日々(uehajの日記)の記事 – Groovyのsortにまつわる衝撃の事実では、このソートは実装されているわけではなく単なる偶然だ、と書かれていました。
この記事が書かれたのは2009年、今はどうなっているのかな?
ということで、ちょっと試してみました。
試してみた
def member = [[name:"a", age:22],[name:"ab", age:18],[name:"abc", age:42],[name:"b", age:33]] // 1) 文字列型のプロパティを返す println member.sort{it.name} //-> [[name:a, age:22], [name:ab, age:18], [name:abc, age:42], [name:b, age:33]] // 2) 文字列型のプロパティを要素に持つリストを返す ★ println member.sort{[it.name]} //-> [[name:a, age:22], [name:b, age:33], [name:ab, age:18], [name:abc, age:42]] // 3) 数値型のプロパティを返す println member.sort{it.age} //-> [[name:ab, age:18], [name:a, age:22], [name:b, age:33], [name:abc, age:42]] // 4) 数値型のプロパティを要素に持つリストを返す println member.sort{[it.age]} //-> [[name:ab, age:18], [name:a, age:22], [name:b, age:33], [name:abc, age:42]] // 5) 数値型のプロパティと文字列型のプロパティを要素に持つリストを返す ★ println member.sort{[it.age, it.name]} //-> [[name:a, age:22], [name:b, age:33], [name:ab, age:18], [name:abc, age:42]]
リストを返すクロージャを渡した時、かつ、プロパティ値が文字列型の場合、期待するソート結果が返ってきませんでした。
リストを返すクロージャーで複数ソートキーを指定できるのだとしたら、1)と2)で挙動が異なる時点で明らかにおかしいです。
先ほど紹介させていただいた Grな日々(uehajの日記)の中でその理由として、「リストについて特別扱いをしていない。リストとしての辞書ソートみたいなものが実現された結果、ソートキー順にソートされているように見えていた」と書かれています。なるほど。
記事が2009年だったので今のバージョン(1.8.5)の該当ソースコード(ここからダウンロードできる)を読んでみました。
現行バージョンでも、リストを返すクロージャーについて特別扱いしていないなぁ。(単純なリストの比較のみ)
結論:
現バージョンでもクロージャーにリストを返したとき、それらの要素がソートキーとして働くような実装はされていない
ちなみにプロパティの型が数値(Integer)だと上手いこといくようにみえます。
def member2 = [[weight:40, age:22],[weight:75, age:18],[weight:75, age:42],[weight:65, age:33],[weight:50, age:18]] println member2.sort{it.weight} //-> [[weight:40, age:22], [weight:50, age:18], [weight:65, age:33], [weight:75, age:18], [weight:75, age:42]] println member2.sort{[it.age]} //-> [[weight:50, age:18], [weight:75, age:18], [weight:40, age:22], [weight:65, age:33], [weight:75, age:42]] println member2.sort{[it.age, -it.weight]} //-> [[weight:75, age:18], [weight:50, age:18], [weight:40, age:22], [weight:65, age:33], [weight:75, age:42]]
Groovy JDK API SpecificationのCollection#sort(Closure)にもソートキーをリストで渡す方法は明示されていませんでした。
第1ソートキー、第2ソートキーを使用したい場合は自分でComparatorを実装するのが正解ですかね。