本記事のコードはJDK 1.8で書いています。
はじめに
自分で作ったクラスのオブジェクト同士の比較をする場合、何をもって大小とするかの比較ロジックを定義する必要があります。
定義方法には以下の2つがあります。
- オブジェクトのクラスに
Comparable
インタフェースを実装させ、compareTo
メソッドで定義。 -
Comparator
インタフェースを実装したクラスを定義し、compare
メソッドで定義。
どちらの方法でも定義可能なのですが、どちらで定義するかの指針みたいなのはあるので、それを説明します。
比較対象のクラス
本の情報を持つBook
クラスを比較することを考えます。
public class Book {
// タイトル
private String title;
// 値段
private int price;
// 発行日
private LocalDate dateOfIssue;
}
Comparable
による定義
java.lang
のComparable
インタフェースを実装し、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.Collections
のsort(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.util
のComparator
を実装したクラスを作成し、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 )