カタカタブログ

SIerで働くITエンジニアがカタカタした記録を残す技術ブログ。Java, Oracle Database, Linuxが中心です。たまに数学やデータ分析なども。

Java ComparableとComparatorの違いについて

今回はJavaのオブジェクト同士を比較するための以下の二つのインタフェースの違いについて、勉強したことをまとめる。

  • java.lang.Comparable<T>インタフェース
  • java.util.Comparator<T>インタフェース

どちらもオブジェクトを順序付けし、比較するためのインタフェースだが、
今回は例として、以下のidnameフィールドだけを持つ従業員クラスEmployeeを比較することを考える。

class Employee implements Comparable<Employee> {
       private int id;
       private String name;
       public Employee(int id, String name) {
              this.id = id;
              this.name = name;
       }
       public int getId() {
              return this.id;
       }
       public String getName() {
              return this.name;
       }
       public String toString() {
              return this.id + ":" + this.name;
       }
       @Override
       public int compareTo(Employee emp) {
              return this.id - emp.id;
       }
}

このときに、TreeSetを使って、以下を実行する。
TreeSetは順序性を持つ集合であり、内部的にオブジェクト同士の比較を行っている。

public class Work {
    public static void main(String[] args) {
       Set<Employee> empSet = new TreeSet<>();
       empSet.add(new Employee(2, "Yamada"));
       empSet.add(new Employee(1, "Sato"));
       empSet.add(new Employee(3, "Suzuki"));
       System.out.println(empSet);
    }
}

実行結果は以下が出力される。

[1:Sato, 2:Yamada, 3:Suzuki]

id順になっていることが分かる。

java.util.Comparator<T>インタフェース

java.util.Comparator<T>インタフェースの特徴は以下。

  • 二つのオブジェクトを比較するためのインタフェース
  • 比較処理を実装した新規クラスとしてインタフェースを実装する(ただし後述のComparator.comparingメソッドで生成可能)
  • 比較したいオブジェクト二つを引数に取るcompareメソッドをオーバーライドする
  • compare(T o1, T o2)は二つのオブジェクトの大小関係に応じて以下の値を返す
  • o1がo2より大きい => 正の値
  • o1がo2より小さい => 負の値
  • o1とo2は同じ大きさ => 0

・原則としてSerializableインタフェースも実装する※以下の実装では省略

今度は、従業員の名前の辞書順で従業員同士を比較するためのComparatorを実装する。
なお、Employeeクラスは先ほど実装したようにComparableインタフェースを実装したクラスのままとする。

class EmployeeNameComparator implements Comparator<Employee> {
       @Override
       public int compare(Employee emp1, Employee emp2) {
              return emp1.getName().compareTo(emp2.getName());
       }
}

このときに、以下を実行するとnameで辞書順にソートされた結果が得られる。
Employeeクラスは上でComparableを実装したため標準ではidでソートするが、
今回はTreeMapのコンストラクタにEmployeeNameComparatorを明示することでソートをnameの辞書順に変更している。

public class Work {
    public static void main(String[] args) {
       Set<Employee> empSet = new TreeSet<>(new EmployeeNameComparator());
       empSet.add(new Employee(2, "Yamada"));
       empSet.add(new Employee(1, "Sato"));
       empSet.add(new Employee(3, "Suzuki"));
       System.out.println(empSet);
    }
}

実行結果は以下が出力される。

[1:Sato, 3:Suzuki, 2:Yamada]

nameの辞書順にソートされていることが分かる。

Comparator.comparingメソッド

今回、Compartorクラスを実装したクラスを新規に作成したが、
その場限りで順序を指定した場合などに毎回新規クラスを実装するのは煩わしい。
そこで、Java SE 8からはComparatorインタフェースのstaticメソッドとしてcomparingメソッドが追加された。
これはcompareメソッドを適切にオーバーライドしたComparatorクラスを生成するもので、以下のようなシグネチャを持つ。

static <T,U extends Comparable<? super U>>Comparator<T> comparing(Function<? super T,? extends U> keyExtractor)

このままだと非常にわかりづらいが、要するに、
ソートキーとなる値を返すFunctionを引数に与えると、適切にそのキーで比較してくれるComparatorを返してくれるというものである。
これはラムダ式を使って簡単に書けるので、TreeMapコンストラクタの引数に直接書くことでシンプルに比較順を変更できる。

具体的に実装例を見た方が分かりやすいので、先ほど作ったEmployeeNameComparatorと同等の実装をしてみる。
ラムダ式には、比較したいキーとなる値を返す関数を指定すればよいので、getNameをメソッド参照すると簡潔に書ける。

public class Work {
    public static void main(String[] args) {
       Set<Employee> empSet = new TreeSet<>(Comparator.comparing(Employee::getName));
       empSet.add(new Employee(2, "Yamada"));
       empSet.add(new Employee(1, "Sato"));
       empSet.add(new Employee(3, "Suzuki"));
       System.out.println(empSet);  
    }
}

実行結果は以下が出力される。

[1:Sato, 3:Suzuki, 2:Yamada]

nameの辞書順にソートされていることが分かる。
同じ結果が得られるので、こちらの方がより簡潔で見やすい。
何より、キーを指定するのみで、生々しい比較ロジックを書く必要がないのが便利。

まとめ

Javaのオブジェクト比較のための二つのインタフェースの違いについて見た。
Comparableはそのオブジェクト自身が本来持つデフォルトの比較ルールを実装するのに対し、
Comparatorは外から特定クラスのオブジェクト同士の比較ルールの適用を可能とする。
さらに、ComparatorComparator.comparingメソッドを使えば、ラムダ式を使って簡潔に書けるので便利。

参考文献

Effective Java 第2版 項目12「Comparableの実装を検討する」

EFFECTIVE JAVA 第2版 (The Java Series)

EFFECTIVE JAVA 第2版 (The Java Series)

以上