Edited at

Java ComparableとComparator どちらを使うか

本記事のコードはJDK 1.8で書いています。


はじめに

自分で作ったクラスのオブジェクト同士の比較をする場合、何をもって大小とするかの比較ロジックを定義する必要があります。

定義方法には以下の2つがあります。


  • オブジェクトのクラスにComparableインタフェースを実装させ、compareToメソッドで定義。


  • Comparatorインタフェースを実装したクラスを定義し、compareメソッドで定義。

どちらの方法でも定義可能なのですが、どちらで定義するかの指針みたいなのはあるので、それを説明します。


比較対象のクラス

本の情報を持つBookクラスを比較することを考えます。

public class Book {

// タイトル
private String title;

// 値段
private int price;

// 発行日
private LocalDate dateOfIssue;

}


Comparableによる定義

java.langComparableインタフェースを実装し、compareToメソッドをオーバーライドして比較ロジックを定義します。

例えばtitleで比較させたい場合は、以下のように実装します。


public class Book implements Comparable<Book>{

// タイトル
private String title;

// 値段
private int price;

// 発行日
private LocalDate dateOfIssue;

@Override
public int compareTo(Book book){
return title.compareTo(book.title);
}

}

titleの比較結果をそのまま返すようにしています。Bookオブジェクトのリストをソートすると、titleの昇順で並び替えられます。

ソート例

    Book book1 = new Book();

book1.setTitle("ドラえもん");
book1.setPrice(500);
book1.setDateOfIssue(LocalDate.of(2010,10,10));

Book book2 = new Book();
book2.setTitle("パーマン");
book2.setPrice(400);
book2.setDateOfIssue(LocalDate.of(2012,10,10));

Book book3 = new Book();
book3.setTitle("オバQ");
book3.setPrice(300);
book3.setDateOfIssue(LocalDate.of(2014,10,10));

List<Book> bookList = Arrays.asList(book1, book2, book3);

System.out.println("ソート前:" + bookList);

Collections.sort(bookList);

System.out.println("ソート後:" + bookList);

java.util.Collectionssort(List<T> list)は、要素のオブジェクトが持つcompareToにより要素同士を比較し、ソート実施します。

実行結果

ソート前:[Book(title=ドラえもん, price=500, dateOfIssue=2010-10-10), Book(title=パーマン, price=400, dateOfIssue=2012-10-10), Book(title=オバQ, price=300, dateOfIssue=2014-10-10)]

ソート後:[Book(title=オバQ, price=300, dateOfIssue=2014-10-10), Book(title=ドラえもん, price=500, dateOfIssue=2010-10-10), Book(title=パーマン, price=400, dateOfIssue=2012-10-10)]

compareToでは比較ロジックを一つしか定義できないことに注意してください。


Comparatorによる定義

java.utilComparatorを実装したクラスを作成し、compareメソッドをオーバーライドして比較ロジックを定義します。例えばtitleで比較させたい場合は、以下のように実装します。

public class BookTitleComparator implements Comparator<Book> {

@Override
public int compare(Book book1, Book book2){
return book1.getTitle().compareTo(book2.getTitle());
}

}

これを使ってソートさせてみましょう。

ソート例

        Book book1 = new Book();

book1.setTitle("ドラえもん");
book1.setPrice(500);
book1.setDateOfIssue(LocalDate.of(2010,10,10));

Book book2 = new Book();
book2.setTitle("パーマン");
book2.setPrice(400);
book2.setDateOfIssue(LocalDate.of(2012,10,10));

Book book3 = new Book();
book3.setTitle("オバQ");
book3.setPrice(300);
book3.setDateOfIssue(LocalDate.of(2014,10,10));

List<Book> bookList = Arrays.asList(book1, book2, book3);

System.out.println("ソート前:" + bookList);

bookList.sort(new BookTitleComparator());

System.out.println("ソート後:" + bookList);

実行結果

ソート前:[Book(title=ドラえもん, price=500, dateOfIssue=2010-10-10), Book(title=パーマン, price=400, dateOfIssue=2012-10-10), Book(title=オバQ, price=300, dateOfIssue=2014-10-10)]

ソート後:[Book(title=オバQ, price=300, dateOfIssue=2014-10-10), Book(title=ドラえもん, price=500, dateOfIssue=2010-10-10), Book(title=パーマン, price=400, dateOfIssue=2012-10-10)]

Comparatorを実装したクラスは、以下のように比較ロジックに応じていくつも作ることができます

値段比較のComparator

public class BookPriceComparator implements Comparator<Book> {

@Override
public int compare(Book book1, Book book2){
return Integer.valueOf(book1.getPrice()).compareTo(book2.getPrice());
}

}

発行日比較のComparator

public class BookDateOfIssueComparator implements Comparator<Book> {

@Override
public int compare(Book book1, Book book2){
return book1.getDateOfIssue().compareTo(book2.getDateOfIssue());
}

}


追記

Comparatorのstaticメソッドcomparingを使えば、クラスを作る必要もありません。


// タイトル順
bookList.sort(Comparator.comparing(Book::getTitle));

// 値段順
bookList.sort(Comparator.comparing(Book::getPrice));

// 発行日順
bookList.sort(Comparator.comparing(Book::getDateOfIssue));


で、どっち使う?

結論から言いますと、基本的にComparatorを使うのがよいかと思います。

Comparableはクラスに属するcompareToを一つしか定義できないので、そのクラスにとって最も自然と思われる比較ロジックを実装すべきと言われています。例えばLocalDateは日付同士の比較が自然と思われるため、「前の日付 < 後の日付」のロジックでcompareToが実装されています。

では今回のBookではどのフィールドで比較するのが自然なのでしょうか?なんとなくタイトルかな?と思われますが、人によっては発行日での比較が自然な可能性もありますし、将来bookIdなんてフィールドが追加されたらさらにブレますよね?もうバグの予感しかしませんね。

Comparatorクラス名ではっきり何の比較かを明示できますので、このようなブレを防ぐことができます。また、タイトルが同じ場合は発行日でさらに比較…のような複合的な比較をしたい場合も、別途Comparator実装クラスを追加すれば可能です。


まとめ



  • Comparableでの比較ロジックは自然な比較にするべきだが、何が自然かは状況によって変わる。


  • Comparatorは実装するクラス名で比較ロジックの内容を明示できる。


  • Comparableでは1つの比較ロジックしか実装できないが、Comparatorではクラスの数だけ実装可能。

  • 自作のクラス同士の比較ロジックは基本的にComparatorで実装しよう!


参考

Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで

Comparable (Java Platform SE 8 )

Comparator (Java Platform SE 8 )