(o1, o2) -> o1 - o2 なんて呪文はもうやめて! - Java8でのComparatorの使い方

  • 285
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

Java8といえばSteamAPIとラムダ式ばかりが注目されていて紹介記事もよく見かける。
そういった記事に出てくるサンプルの中では、ソートをこんな風に書いているものが多い。

従来の比較処理をラムダ式に置き換えただけの書き方
persons.stream()
        .sorted((o1, o2) -> o2.getAge() - o1.getAge())

これは従来Comparatorインターフェースのcompare()で実装していたコードをそのままラムダ式に置き換えただけのあまり好ましくない書き方だ。

Java8ではComparatorインターフェースにいくつかのstaticメソッドとdefaultメソッドが追加されていて、これらを使うとComparatorを宣言的で明快な記述に変える事ができる。

Comparatorの新規メソッドを使った書き方
import static java.util.Comparator.*;

persons.stream()
        .sorted(comparing(Person::getAge).reversed())

Comparatorの新しい使い方をちゃんと覚えて明快なソートが書けるようにしよう!

従来の書き方がダメな理由

  • o1 - o2o2 - 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以外の要素の比較方法を指定する。

nullを先頭、null以外を自然順ソート
list.sort(nullsFirst(naturalOrder()))
nullを末尾、null以外を逆順ソート
list.sort(nullsLast(reverseOrder()))

キーを指定したソート

ソート対象クラスがComparableを実装していない場合や、Comparableとは別のキーでソートしたい時にはcomparing()を使用する。
comparing()の第一引数はFunctionで、要素からキーを取り出す関数を指定する。

Stringリストを文字列長の昇順でソート
list.sort(comparing(String::length))

Functionをラムダ式で書く事もできるが、Comparatorをラムダ式で書くときのように同じ式を2回書く必要はない。

Stringリストを文字列長の昇順でソート
list.sort(comparing(x -> x.length()))

第二引数でComparatorを指定する事もできる。

Stringリストを文字列長の降順でソート
list.sort(comparing(String::length, reverseOrder()))

キーがnullの可能性がある場合のソート

nulls***comparingを組み合わせて使う。

Personリストを年齢でソート、年齢がnullの場合は末尾にする。
list.sort(comparing(Person::getAge, nullsLast(naturalOrder())))

複合ソート

複数のキーでソートするにはthenComparing()を使用する。

文字列リストを長さでソート、同じ長さなら文字列の逆順でソートする
list.sort(comparing(String::length).thenComparing(reverseOrder()))

まとめ

今までの書き方では煩雑になる比較処理を、Comparatorに追加されたメソッドを使う事で宣言的でシンプルに書けるようになる事が分かったと思う。

宣言的なコードを書くメリットは

  • 一時変数や制御文などを使わないので、バグが入り込む余地が小さくなる。
  • 可読性が高く要求仕様をそのまま写したようなコードが書ける。

というのもあるので、Comparatorを使う際はぜひ新しい書き方を使ってほしい。