[Java8] 従業員のソートから考えるComparableとComparatorの適切な使い方

自己紹介

はじめまして、Gozalという労務管理サービスを作っている @ekuro です。
Gozalでは、労務管理をより適切にソフトウェアに落とし込むために、ドメイン駆動開発を行っています。
働き方改革などで業務プロセスに変更があった場合でも、簡単に対応ができるように、より良い汎用性の高いクラスについて日々模索中です。

今回のテーマ

今回は社員番号で従業員のリストをソートするために「何を使うべきか」「どこで定義すべきか」について考えてみました。

やったこと

前提

従業員のリスト List<Employee> employees; を社員番号でソートする処理について考えます。

Employee.java
@Getter
@AllArgsConstructor
public class Employee {
  // 社員番号
  private EmployeeNumber number;
  // 氏名・生年月日などの個人情報
  private Personal personal;
}

@Getter
@AllArgsConstructor
public class EmployeeNumber {
  private String number;
}

ソートメソッド

Java8でオブジェクトをソートする方法には、代表的なものとしてCollections.sortやStreamAPIのsortedメソッドがあります。

Collectionsを使う場合、下記の2種類のsortメソッドのどちらかを使うことになります。

public static <T extends Comparable<? super T>> void sort(List<T> list) {
  list.sort(null);
}
public static <T> void sort(List<T> list, Comparator<? super T> c) {
  list.sort(c);
}

またStreamAPIを使う場合も同じく2種類のメソッドが用意されています。

Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

どちらの場合も、1つ目のメソッドを使う場合は、ソートする対象のクラスがComparableを実装している必要があり、2つ目のメソッドを使う場合は、関数型インターフェースComparatorを引数で渡す必要があります。

ComparableもComparatorも大小比較を定義するクラスですが、まずはその違いから見ていきましょう。

ComparableとComparatorの違い

Comparableは public int compareTo(T o); のみ定義されているインターフェースです。
比較したいオブジェクトにComparableを継承させcompareToを実装することで、そのオブジェクトの比較方法を定義することができます。compareToメソッドでは引数と自分自身を比較します。
一方、Comparatorインターフェースは、 int compare(T o1, T o2); という引数を2つ持つメソッドが定義されています。比較したいオブジェクトに対して複数のComparatorを実装することができるため、色々なキーでのソートを定義したい場合に有効です。

Employeeの場合

Employeeクラスの場合を考えてみましょう。
Employeeクラスは社員番号の他に氏名などの個人情報を持っています。そのため今後様々な情報でソートをすることが考えられます。
したがってEmployeeクラスではComparableを継承するのではなく、比較する項目ごとにComparatorを作成する方が良さそうです。

実際に従業員リストを社員番号でソートをするためにComparatorを返すメソッドを書いてみます。1

Employee.java
@Getter
@AllArgsConstructor
public class Employee {
  // 社員番号
  private EmployeeNumber number;
  // 氏名・生年月日などの個人情報
  private Personal personal;

  public static Comparator<Employee> orderByNumber() {
    return Comparator.comparing(
      // 単純な社員番号の文字列比較
      (Employee employee) -> employee.getNumber().getNumber());
  }
}

実際にsortする場合は、

List<Employee> employees = ~;
Collections.sort(employees, Employee.orderByNumber());

と書けます。

比較処理を適切な場所で行う

上の例では比較メソッド orderByNumber() をEmployeeクラスに定義しました。
しかし本来、社員番号の比較は社員番号クラスが持つべき性質です。
そこでEmployeeNumberクラスに比較メソッドを移します。

Employee.java
@Getter
@AllArgsConstructor
public class Employee {
  // 社員番号
  private EmployeeNumber number;
  // 氏名・生年月日などの個人情報
  private Personal personal;

  public static Comparator<Employee> orderByNumber() {
    // EmployeeNumberを比較のキーとして、ソートの処理はEmployeeNumberのorderメソッドに任せます。
    return Comparator.comparing(Employee::getNumber, EmployeeNumber.order());
  }
}
EmployeeNumber.java
@Getter
@AllArgsConstructor
public class EmployeeNumber {
  private String number;

  public static Comparator<EmployeeNumber> order() {
    return Comparator.comparing(EmployeeNumber::getNumber);
  }
}

比較処理メソッドがEmployeeNumberで完結してより汎用性の高いクラスにすることができました。
今回EmployeeNumberの大小比較処理は簡単なロジックにしましたが、数字や文字列が混ざると少し複雑な処理になります。
その場合にEmployeeNumber内に比較処理が定義されていると見通しがよくなります。

Comparableが使えないか再度考えてみる

Comparatorは1つのオブジェクトに複数定義できるので、つい多用してしまいがちです。
なので本来Comparableを継承すべきところでComparatorを使ってしまっていないかチェックします。

今回のEmployeeNumberのソートは今後もnumber以外でソートすることはなさそうです。
なのでEmployeeNumberの比較処理はComparableを使う方がよいでしょう。

Employee.java
@Getter
@AllArgsConstructor
public class Employee {
  // 社員番号
  private EmployeeNumber number;
  // 氏名・生年月日などの個人情報
  private Personal personal;

  public static Comparator<Employee> orderByNumber() {
    // EmployeeNumberがComparableを実装することで、引数が減りました。
    return Comparator.comparing(Employee::getNumber);
  }
}
EmployeeNumber.java
@Getter
@AllArgsConstructor
public class EmployeeNumber implements Comparable<EmployeeNumber> {
  private String number;

  @Override
  public int compareTo(@Nonnull EmployeeNumber other) {
    return this.number.compareTo(other.number);
  }
}

EmployeeNumberがComparableを継承することで、不必要な記述がなくなり、処理内容がより洗練されました。

このように適切な場所に適切な処理を書いておくことで、今後「氏名でソートしたい」「誕生日でソートしたい」という要望にスムーズに応えられるようにもなります。

より適切に概念をとらえ、クラスを設計することは、楽に実装できるようになる・分かりやすいソースコードになるというだけでなく、より洗練された新しい業務フローの発見にもつながると考えています。

Gozalでは、エンジニア発信で世の中をアッと言わせる新しい労務体験を創出するために、概念の捉え方にトコトンこだわってまいります。

労務管理の世界から無駄な手作業を絶滅させるエンジニア募集!
ギュルンギュルン動く労務管理SaaSを生み出すフロントエンジニア募集!


  1. Comparatorインターフェースに用意されているstaticのcomparingメソッドは引数が1つだけの場合は、Functionを引数にとって、Functionの結果に対してcompareToで比較をしてくれます。