Java8といえばSteamAPIとラムダ式ばかりが注目されていて紹介記事もよく見かける。
そういった記事に出てくるサンプルの中では、ソートをこんな風に書いているものが多い。
persons.stream()
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
これは従来Comparatorインターフェースのcompare()で実装していたコードをそのままラムダ式に置き換えただけのあまり好ましくない書き方だ。
Java8ではComparatorインターフェースにいくつかのstaticメソッドとdefaultメソッドが追加されていて、これらを使うとComparatorを宣言的で明快な記述に変える事ができる。
import static java.util.Comparator.*;
persons.stream()
.sorted(comparing(Person::getAge).reversed())
Comparatorの新しい使い方をちゃんと覚えて明快なソートが書けるようにしよう!
従来の書き方がダメな理由
-
o1 - o2
とo2 - o1
、昇順ソートの呪文はどっちだっけ? - 左辺と右辺で同じコード書くのってどうよ?
- 要素がnullだったらどうすんの?
- 要素のキーがnullだったらどうすんの?
- 複合ソートの場合は1つ目のキーを比較して、同じだったら2つ目のキーを比較して、2つ目も同じだったら...
例えばnullを考慮する場合、こんなコードを書く必要がある。
if (o1 == null && o2 == null) return 0;
else if (o1 != null && o2 == null) return -1;
else if (o1 == null && o2 != null) return 1;
else o1 - o2;
上のコードを見て、nullは先頭になるのか末尾になるのかぱっと見では分からないよね?
今までに何百回もComparatorを実装してきたソート職人にとってこんな呪文はすぐに唱えられるかもしれないけど、一般庶民にとっては結構しんどいよね?
だけどJava8でComparatorに追加されたメソッドを使えば、誰でも簡単にComparatorを書けるようになる。
それではComparatorに追加されたメソッドを見てみよう。
尚、ここからのコードは全てComparatorをstaticインポートしているものとする。
import static java.util.Comparator.*;
自然順/逆順ソート
naturalOrder()
とreverseOrder()
でそれぞれ自然順/逆順のComparatorが取得できるので、ソート対象がComparableを実装している場合はこれらを使用してソートできる。
list.sort(naturalOrder())
list.sort(reverseOrder())
これでもう(o1, o2) -> o1 - o2
というお決まりの呪文とはオサラバできる。
Comparatorを逆順にする
comparator.reversed()
を使用すると既存のComparatorの逆順を取得できる。
list.sort(String.CASE_INSENSITIVE_ORDER.reversed());
nullを含んだソート
nullの要素を含むリストをソートする場合は、nullsFirst()
とnullsLast()
でnull要素を先頭または末尾にするComparatorが取得できる。
第一引数にはnull以外の要素の比較方法を指定する。
list.sort(nullsFirst(naturalOrder()))
list.sort(nullsLast(reverseOrder()))
キーを指定したソート
ソート対象クラスがComparableを実装していない場合や、Comparableとは別のキーでソートしたい時にはcomparing()
を使用する。
comparing()
の第一引数はFunctionで、要素からキーを取り出す関数を指定する。
list.sort(comparing(String::length))
Functionをラムダ式で書く事もできるが、Comparatorをラムダ式で書くときのように同じ式を2回書く必要はない。
list.sort(comparing(x -> x.length()))
第二引数でComparatorを指定する事もできる。
list.sort(comparing(String::length, reverseOrder()))
キーがnullの可能性がある場合のソート
nulls***
とcomparing
を組み合わせて使う。
list.sort(comparing(Person::getAge, nullsLast(naturalOrder())))
複合ソート
複数のキーでソートするにはthenComparing()
を使用する。
list.sort(comparing(String::length).thenComparing(reverseOrder()))
まとめ
今までの書き方では煩雑になる比較処理を、Comparatorに追加されたメソッドを使う事で宣言的でシンプルに書けるようになる事が分かったと思う。
宣言的なコードを書くメリットは
- 一時変数や制御文などを使わないので、バグが入り込む余地が小さくなる。
- 可読性が高く要求仕様をそのまま写したようなコードが書ける。
というのもあるので、Comparatorを使う際はぜひ新しい書き方を使ってほしい。