はじめに こんにちは、Hiropyです。 今回は、 Java のComparatorについて簡単に解説できればと思います。 はじめに Comparatorとは? compareメソッドの使用方法 使用例 Comparableとの違い 主なメソッド comparing reversed naturalOrder・reverseOrder nullsFirst・nullsLast thenComparing まとめ 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、ひいてはソートの理解に少しでも役立てれば幸いです。