RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

JavaのComparatorまとめ

はじめに

こんにちは、Hiropyです。
今回は、JavaのComparatorについて簡単に解説できればと思います。

Comparatorとは?

Comparatorは、「比較者」という和訳の通りオブジェクト同士の比較を行うインタフェースで、主にList等のソート(並べかえ)に使われます。
型パラメータTを持つ関数型インタフェースで、抽象メソッドのcompare(T o1, T o2)を実装することで大小比較ができるようになります。
後述する通り便利なメソッドが複数あり、毎回compareを実装する必要がないこともポイントです。

※この記事には関数型インタフェースやラムダ式が特に説明なく登場します。よくわからない方はまずそちらから調べてみてください。

compareメソッドの使用方法

compare(T o1, T o2)は、o1が小さい(ソートしたとき先に並ぶ)ときは返り値が負に、o1が大きい(後に並ぶ)ときは正に、等しいときは0になるように実装します。
例えば、String型を文字数で比較(短いほうを「小さい」と判定)したい場合、実装は以下のようになります。

// s1のほうが短いときはs1.length() < s2.length()なので負の値を返す、s1のほうが長いときはその逆で正を返す
Comparator<String> stringComparator = (String s1, String s2) -> s1.length() - s2.length();

より複雑な実装も可能です。
以下のComparatorは、heightが小さいほうを「小さい」と判定し、heightが同じ場合は、weightが大きい方を「小さい」と判定する一風変わったものです。

class Person {
    public int height;
    public int weight;
}

Comparator<Person> personComparator = (p1, p2) -> {
    if(p1.height != p2.height) {
        return p1.height - p2.height;
    }
    return p2.weight - p1.weight;
};

使用例

配列やリストには、Comparatorを用いたソートを行うメソッドが用意されています。
以下に代表的なものを挙げますが、基本的にソート後の並びは昇順(小さい順)です。
(無論ソートメソッドの実装次第で並び順が降順やその他の順になっているものも存在し得ます)
以下の例では、先程のstringComparatorを用いてListを文字数の昇順で並べ替えています。

Comparator<String> stringComparator = (String s1, String s2) -> s1.length() - s2.length();

List<String> wordList = new ArrayList<>(Arrays.asList("Today", "is", "a", "good", "day"));
System.out.println(wordList); // >[Today, is, a, good, day]

// stream().sortedは元のリストを変更せずに新しいストリームを返す
List<String> sortedList = wordList.stream().sorted(stringComparator).toList();
System.out.println(sortedList); // >[a, is, day, good, Today]

// List.sortは元のリストを変更する
wordList.sort(stringComparator);
System.out.println(wordList); // >[a, is, day, good, Today]

List<String> wordList2 = new ArrayList<>(Arrays.asList("This", "is", "a", "pen"));
// Collections.sortも元のリストを変更する
Collections.sort(wordList2, stringComparator);
System.out.println(wordList2); // >[a, is, pen, This]

もちろん、1回しか使わないときはいちいちComparator変数を定義しなくてもソートする関数の引数内に記述すればOKです。

List<String> wordList = new ArrayList<>(Arrays.asList("Today", "is", "a", "good", "day"));
List<String> sortedList = wordList.stream().sorted((String s1, String s2) -> s1.length() - s2.length()).toList();
// >[a, is, day, good, Today]

Comparableとの違い

Comparatorと似た名前の比較用インタフェースとして、Comparableが存在します。
Comparableも型パラメータTを持つ関数型インタフェースで、実装時にはcompareTo(T o)を実装する必要があります。
このcompareToはクラス内蔵のComparatorのようなもので、インスタンスの比較がしたいクラスであらかじめ比較方法を実装しておくものです。
Comparableを実装したオブジェクトをソートするときにメソッドを引数なしや引数nullで呼び出すと、メソッドにもよりますがcompareToを使用した昇順ソートがなされることが多いです。
なお、compareToの比較による順序付けのことを自然順序付けといいます。

class Person implements Comparable<Person>{
    public int age;

    public Person(int age) {
        this.age = age;
    }

    @Override
    // thisが小さいときは負の数を、thisが大きいときは正の数を、同じときは0を返すよう実装
    public int compareTo(Person p) {
        return this.age - p.age;
    }
}

List<Person> people = new ArrayList<>(Arrays.asList(new Person(10), new Person(30), new Person(20)));

// stream.sortedでは引数なしで呼び出す
List<Person> sortedPeople = people.stream().sorted().toList();
// List.sortでは引数nullで呼び出す
people.sort(null);
// いずれも[10, 20, 30]

Comparableは

  • クラス内で一度実装すれば毎回ソートの際の比較方法を書かなくていい

というメリットがあります。

一方でComparatorはソート時に毎回実装するので、

  • 同じクラスでもその場に合わせた色々な基準でソートできる
  • ソートする場所に比較方法を書くのでパッと見て何でソートするのかわかりやすい

といった点がメリットです。

両方使用して、「Comparableを実装しておいて普段は自然順序付け、特別な並べかえ方法を使いたいときはComparator」といった運用方法も可能です。

ちなみに、JDKに含まれているメジャーなクラスにも、Comparableを実装しているクラスは以下のように複数存在します。

  • IntegerやDouble,BigDecimalなど数値系は、数値の昇順に並ぶ
  • Stringは、辞書順の昇順に並ぶ
  • LocalDateTimeなどの日付時刻系は、日付時刻の昇順に並ぶ

主なメソッド

前述の通りComparatorにはcompare以外にもstatic・defaultメソッドがいくつか用意されています。
これらを使用することで、より短く、見やすいコードで比較・ソートが行えます。

comparing

Comparator.comparing(Function keyExtractor)では、引数に「Comparatorを実装したオブジェクトを返すメソッド」を実装したFunctionを入れることで、そのメソッドの返り値を自然順序付けで比較するComparatorが返されます。

オブジェクトの比較は、数値型(Integer, Doubleなど)の変数やlengthなど何らかの数値を使って行うことが多いかと思いますが、
この際compareを実装してComparatorを作る場合は同じメソッドの呼び出しを2回書くことになるほか、「どっちからどっちを引くと昇順だっけ?」と迷うことも少なくありません。
comparingを使用するとメソッドを1回書くだけで済むので、コーディングミスを減らすことが可能です。

Comparator<String> stringComparator = Comparator.comparing(s -> s.length());
// Comparator<String> stringComparator = (String s1, String s2) -> s1.length() - s2.length(); と同じ

// メソッド参照を使用するとより簡潔に書ける
Comparator<String> stringComparator2 =Comparator.comparing(String::length);

数値型に限らず、例えばStringを返すメソッドを入れた場合、辞書順で比較するComparatorになります。

class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

List<Person> people = new ArrayList<>(Arrays.asList(new Person("Hinata"), new Person("Emily"), new Person("Tsubasa")));
List<Person> sortedList = people.stream().sorted(Comparator.comparing(Person::getName)).toList();
// > [Emily, Hinata, Tsubasa]

また、引数を2つ取るcomparing(Function keyExtractor, Comparator keyComparator)もあり、この場合は自然順序付けではなく第2引数のComparatorで比較します。

reversed

このメソッドを呼び出したComparatorの逆順で判定を行うComparatorを返します。
降順でソートしたい場合や既存のComparatorの逆順でソートしたい場合、このメソッドを使うと便利です。

List<String> wordList = new ArrayList<>(Arrays.asList("Today", "is", "a", "good", "day"));
List<String> sortedList = wordList.stream().sorted(Comparator.comparing(String::length).reversed()).toList();
// >[Today, good, day, is, a]
naturalOrder・reverseOrder

Comparator.naturalOrder()は、Comparableを実装したオブジェクトについて、自然順序付けで比較をするComparatorを返します。
Comparator.reverseOrder()は、Comparableを実装したオブジェクトについて、自然順序付けの逆順で比較をするComparatorを返します。

List<Integer> numberList = new ArrayList<>(Arrays.asList(4, 2, 3, 5, 1));

List<Integer> naturalSortedList = numberList.stream().sorted(Comparator.naturalOrder()).toList();
// > [1, 2, 3, 4, 5]
// Comparableの項で書いた通り、以下のように書いても同じ結果になる
// List<Integer> naturalSortedList = numberList.stream().sorted().toList();

List<Integer> reverseSortedList = numberList.stream().sorted(Comparator.reverseOrder()).toList();
// > [5, 4, 3, 2, 1]
nullsFirst・nullsLast

Comparator.nullsFirst(Comparator comparator)は、nullをnull以外より小さいとみなし、両方がnull以外なら引数のcomparatorによる比較を行うComparatorを返します。
つまり、ソート時にnullが先頭に来ることになるのです。

Comparatorを実装する際に比較対象にnullが入ってくるとNullPointerExceptionが発生する場合がありますが、

List<String> wordList = new ArrayList<>(Arrays.asList("Today", "is", "a", "good", "day", null));
List<String> sortedList = wordList.stream().sorted(Comparator.comparing(String::length)).toList();
// > NullPointerException

nullsFirstを使用することで例外発生を防ぐことができます。

List<String> wordList = new ArrayList<>(Arrays.asList("Today", "is", "a", "good", "day", null));
List<String> sortedList = wordList.stream().sorted(Comparator.nullsFirst(Comparator.comparing(String::length))).toList();
// > [null, a, is, day, good, Today]

nullsLastはその逆で、nullをnull以外より大きいとみなす、つまりソート時にnullが末尾に来るようなComparatorを返します。

thenComparing

複数条件でソートするときに使用するメソッドです。
Comparatorの後ろにつけて引数で比較条件を指定することで、そのComparatorが0を返したときに追加の条件で比較することができます。
引数にはComparatorの他、comparingと同様FunctionやFunction+Comparatorを入れることが可能です。

以下の例では、Stringのリストを文字数の昇順で、文字数が同じものについてはStringの自然順序付け(辞書順)で並べ替えています。

List<String> wordList = new ArrayList<>(Arrays.asList("cat", "dog", "apple", "banana", "elephant", "ant", "zebra"));
List<String> sortedList = wordList.stream().sorted(Comparator.comparing(String::length).thenComparing(Comparator.naturalOrder())).toList();
// > [ant, cat, dog, apple, zebra, banana, elephant]

まとめ

ComparatorはJavaでソートを行う際に非常に重要になるインターフェースです。
WebアプリなどではそもそもSQLのORDER BYでソートしているからJavaでソートをする場面は必ずしも多くないかとは思います。
とはいえJava側での処理の途中でソートを行う処理もあったりするので、使いかたを覚えておいて損はないでしょう。
本記事がComparator、ひいてはソートの理解に少しでも役立てれば幸いです。

Copyright © RAKUS Co., Ltd. All rights reserved.