まったく汎用性のないプログラム
並べ替えるのは、「社員」と「商品」
ここでは例として、「社員」と「商品」という2種類のデータを並べ替えるプログラムを考えてみましょう。
「社員」は、「年齢」「身長」「名前」という3つの属性を持っています。
public class Person { public int getAge ( ) { // 年齢を返す } public double getHeight ( ) { // 身長を返す } public String getName ( ) { // 名前を返す } }
「商品」は、「値段」という属性を持っています。
public class Product { public int getPrice ( ) { // 値段を返す } }
「社員」を並べ替えよう
まずは、「社員」を「年齢」の順に並べ替えるプログラムを書いてみましょう。
Person[] sortPersonByAge ( Person[] src ) { Person[] dst = new Person[src.length]; for ( int i = 0 ; i < src.length ; i++ ) { dst[i] = src[i]; } for ( int i = 1 ; i < dst.length ; i++ ) { for ( int j = 0 ; j < dst.length - i ; j++ ) { if ( dst[j].getAge() > dst[j+1].getAge() ) { Person swap = dst[j]; dst[j] = dst[j+1]; dst[j+1] = swap; } } } return dst; }
このプログラムは、正しく動作します。
「商品」も並べ替えよう
しかし、このプログラムは、汎用性がまったくありません。
次に、「商品」を「値段」の順に並べ替えるプログラムを書くことにしましょう。すると、先ほどとほとんど同じプログラムを、もう一度書かなくてはなりません。
Product[] sortProductByPrice ( Product[] src ) { Product[] dst = new Product[src.length]; for ( int i = 0 ; i < src.length ; i++ ) { dst[i] = src[i]; } for ( int i = 1 ; i < dst.length ; i++ ) { for ( int j = 0 ; j < dst.length - i ; j++ ) { if ( dst[j].getPrice() > dst[j+1].getPrice() ) { Product swap = dst[j]; dst[j] = dst[j+1]; dst[j+1] = swap; } } } return dst; }
いろいろな順に並べ替えよう
「社員」は、「年齢」の順に並べ替えるだけではありません。「社員」を「身長」の順に並べ替えるプログラムも書くことにしましょう。すると、ほとんど同じプログラムを、またしても書かなくてはなりません。
Person[] sortPersonByHeight ( Person[] src ) { Person[] dst = new Person[src.length]; for ( int i = 0 ; i < src.length ; i++ ) { dst[i] = src[i]; } for ( int i = 1 ; i < dst.length ; i++ ) { for ( int j = 0 ; j < dst.length - i ; j++ ) { if ( dst[j].getHeight() > dst[j+1].getHeight() ) { Person swap = dst[j]; dst[j] = dst[j+1]; dst[j+1] = swap; } } } return dst; }
同じような関数がいくつもできてしまった
結局、ほとんど同じような関数を、3つも作る羽目になってしまいました。
関数名 | 内容 |
---|---|
sortPersonByAge | 「社員」を「年齢」の順に並べ替える |
sortProductByPrice | 「商品」を「値段」の順に並べ替える |
sortPersonByHeight | 「社員」を「身長」の順に並べ替える |
このように、データを並べ替えるたびに、いちいち関数が増えていったのでは大変です。こんなことにならないように、汎用的な枠組みを考えることにしましょう。
汎用化への道
異なるデータを並べ替える、共通の仕組みを作る
まずは、「社員」も「商品」も並べ替えられるような、共通の仕組みを考えます。
そのために、並べ替えの基準となる値を取り出す部分を、インターフェースとして汎用化します。具体的には、以下のようなインターフェースを用意します。
// 並べ替えの基準となる値を取り出すインターフェース public interface Value { public double getValue ( ); }
データを並べ替える関数は、このインターフェースを使うように書き換えます。
Value[] sort ( Value[] src ) { Value[] dst = new Value[src.length]; for ( int i = 0 ; i < src.length ; i++ ) { dst[i] = src[i]; } for ( int i = 1 ; i < dst.length ; i++ ) { for ( int j = 0 ; j < dst.length - i ; j++ ) { if ( dst[j].getValue() > dst[j+1].getValue() ) { Value swap = dst[j]; dst[j] = dst[j+1]; dst[j+1] = swap; } } } return dst; }
この仕組みを使って、「社員」を「年齢」で並べ替えるためには、「社員」クラスがこのインターフェースを継承すれば良いです。
public class Person implements Value { // オーバーライド public double getValue ( ) { // 並べ替えの基準として、年齢を返す return getAge(); } public int getAge ( ) { // 年齢を返す } }
また、この仕組みを使って、「商品」を「値段」で並べ替えるためには、「商品」クラスがこのインターフェースを継承すれば良いです。
public class Product implements Value { // オーバーライド public double getValue ( ) { // 並べ替えの基準として、値段を返す return getPrice(); } public int getPrice ( ) { // 値段を返す } }
この仕組みをクラス図で表すと、次のようになります。
しかし、これでは、「社員」はいつも「年齢」でしか並べ替えることができません。
いろいろな順に並べ替える、共通の仕組みを作る
次に、同じ「社員」でも、時によって「年齢」で並べ替えたり、「身長」で並べ替えたりできるような、共通の仕組みを考えます。
そのために、並べ替えの基準となる値を取り出す部分を、外部クラスとして分離します。具体的には、以下のような外部クラスを作ります。
// 並べ替えの基準となる値を取り出す外部クラスのインターフェース public interface ValueGetter { public double getValue ( Object obj ); } // 並べ替えの基準となる値を取り出す外部クラス public class AgeGetter implements ValueGetter { // オーバーライド public double getValue ( Object obj ) { // 並べ替えの基準として、年齢を返す return ((Person)obj).getAge(); } } // 並べ替えの基準となる値を取り出す外部クラス public class HeightGetter implements ValueGetter { // オーバーライド public double getValue ( Object obj ) { // 並べ替えの基準として、身長を返す return ((Person)obj).getHeight(); } }
データを並べ替える関数は、この外部クラスを使うように書き換えます。
Object[] sort ( Object[] src, ValueGetter getter ) { Object[] dst = new Object[src.length]; for ( int i = 0 ; i < src.length ; i++ ) { dst[i] = src[i]; } for ( int i = 1 ; i < dst.length ; i++ ) { for ( int j = 0 ; j < dst.length - i ; j++ ) { if ( getter.getValue(dst[j]) > getter.getValue(dst[j+1]) ) { Object swap = dst[j]; dst[j] = dst[j+1]; dst[j+1] = swap; } } } return dst; }
「社員」を「年齢」で並べ替える場合は、sort関数の引数に、AgeGetterのインスタンスを渡せば良いです。「社員」を「身長」で並べ替える場合は、sort関数の引数に、HeightGetterのインスタンスを渡せば良いです。
また、この仕組みを使って、「商品」を「値段」で並べ替える場合は、下記のような、「値段」を取り出す外部クラスを作れば良いです。
// 並べ替えの基準となる値を取り出す外部クラス public class PriceGetter implements ValueGetter { // オーバーライド public double getValue ( Object obj ) { // 並べ替えの基準として、値段を返す return ((Product)obj).getPrice(); } }
この仕組みをクラス図で表すと、次のようになります。
2つの仕組みを比べてみよう
異なるデータを並べ替えるために導入した、Valueインターフェースを使う仕組みでは、データクラスがValueインターフェースを継承したため、並べ替えの枠組みに組み込まれていました。逆に言えば、枠組みに組み込まれていないクラスは、並べ替えができませんでした。そのため、新しいデータクラスを作るたびに、もしも将来、並べ替える可能性があるのなら、念のために、Valueインターフェースを継承させておかなくてはなりませんでした。これは困った制約に思えます。
いろいろな順に並べ替えるために導入した、外部クラスValueGetterを使う仕組みは、並べ替えの対象である「社員」や「商品」といったデータクラスを、余計な継承関係からフリーにできるのが利点です。並べ替えたいクラスに対して、適切なValueGetterを用意すれば、どのようなクラスでも並べ替えることができます。データクラスに手を加える必要がありません。
しかし、これまでに考えた仕組みでは、並べ替えの基準は、「年齢」や「身長」などの、数値に限られています。このままでは、「社員」を「名前」で並べ替えることができません。
さらなる汎用化への道
数値以外の属性で並べ替えよう
そこで、文字列のような、数値以外の属性でも並べ替えができるような仕組みを考えます。
そのために、並べ替えの基準となる値を取り出すのではなく、データの大小を、データ自身に判定させるようにします。
例えば、「社員」を「名前」で並べ替えるには、「社員」クラスに以下のようなメソッドを作ります。
public class Person { // 自身とtargetの「名前」の大小を判定する。 public int compareNameTo ( Person target ) { // 自身の方が小さければ、-1を返す // 自身の方が大きければ、1を返す // 同じであれば、0を返す } }
データを並べ替える関数は、このメソッドを使うように書き換えます。
Person[] sortPersonByName ( Person[] src ) { Person[] dst = new Person[src.length]; for ( int i = 0 ; i < src.length ; i++ ) { dst[i] = src[i]; } for ( int i = 1 ; i < dst.length ; i++ ) { for ( int j = 0 ; j < dst.length - i ; j++ ) { if ( dst[j].compareNameTo(dst[j+1]) > 0 ) { Person swap = dst[j]; dst[j] = dst[j+1]; dst[j+1] = swap; } } } return dst; }
このプログラムは、正しく動作します。
しかし、この仕組みには、まったく汎用性がありません。そこで、さきほどと同じように、汎用的な枠組みを考えていくことにしましょう。
もう一度、異なるデータを並べ替える、共通の仕組みを作る
まずは、「社員」も「商品」も並べ替えられるような、共通の仕組みを考えます。
そのために、データの大小をデータ自身に判定させる部分を、インターフェースとして汎用化します。具体的には、以下のようなインターフェースを用意します。
// データの大小を判定するインターフェース public interface Comparable { public int compareTo ( Comparable target ); }
データを並べ替える関数は、このインターフェースを使うように書き換えます。
Comparable[] sort ( Comparable[] src ) { Comparable[] dst = new Comparable[src.length]; for ( int i = 0 ; i < src.length ; i++ ) { dst[i] = src[i]; } for ( int i = 1 ; i < dst.length ; i++ ) { for ( int j = 0 ; j < dst.length - i ; j++ ) { if ( dst[j].compareTo(dst[j+1]) > 0 ) { Comparable swap = dst[j]; dst[j] = dst[j+1]; dst[j+1] = swap; } } } return dst; }
この仕組みを使って、「社員」を「名前」で並べ替えるためには、「社員」クラスがこのインターフェースを継承すれば良いです。
public class Person implements Comparable { // オーバーライド public int compareTo ( Comparable target ) { // 自身とtargetの「名前」の大小を判定した結果を返す } }
また、この仕組みを使って、「商品」を「値段」で並べ替えるためには、「商品」クラスがこのインターフェースを継承すれば良いです。
public class Product implements Comparable { // オーバーライド public int compareTo ( Comparable target ) { // 自身とtargetの「値段」の大小を判定した結果を返す } }
この仕組みをクラス図で表すと、次のようになります。
しかし、これでは、「社員」はいつも「名前」でしか並べ替えることができません。
もう一度、いろいろな順に並べ替える、共通の仕組みを作る
次に、同じ「社員」でも、時によって「名前」で並べ替えたり、「年齢」で並べ替えたりできるような、共通の仕組みを考えます。
そのために、データの大小をデータ自身に判定させる部分を、外部クラスとして分離します。具体的には、以下のような外部クラスを作ります。
// データの大小を判定するす外部クラスのインターフェース public interface Comparator { public int compare ( Object obj1, Object obj2 ); } // データの大小を判定するす外部クラス public class NameComparator implements Comparator { // オーバーライド public int compare ( Object obj1, Object obj2 ) { // obj1とobj2の「名前」の大小を判定した結果を返す } } // データの大小を判定するす外部クラス public class AgeComparator implements Comparator { // オーバーライド public int compare ( Object obj1, Object obj2 ) { // obj1とobj2の「年齢」の大小を判定した結果を返す } }
データを並べ替える関数は、この外部クラスを使うように書き換えます。
Object[] sort ( Object[] src, Comparator cmp ) { Object[] dst = new Object[src.length]; for ( int i = 0 ; i < src.length ; i++ ) { dst[i] = src[i]; } for ( int i = 1 ; i < dst.length ; i++ ) { for ( int j = 0 ; j < dst.length - i ; j++ ) { if ( cmp.compare(dst[j], dst[j+1]) > 0 ) { Object swap = dst[j]; dst[j] = dst[j+1]; dst[j+1] = swap; } } } return dst; }
「社員」を「名前」で並べ替える場合は、sort関数の引数に、NameComparatorのインスタンスを渡せば良いです。「社員」を「年齢」で並べ替える場合は、sort関数の引数に、AgeComparatorのインスタンスを渡せば良いです。
また、この仕組みを使って、「商品」を「値段」で並べ替える場合は、下記のような、「値段」の大小を判定するクラスを作れば良いです。
// データの大小を判定するす外部クラス public class PriceComparator implements Comparator { // オーバーライド public int compare ( Object obj1, Object obj2 ) { // obj1とobj2の「値段」の大小を判定した結果を返す } }
この仕組みをクラス図で表すと、次のようになります。
もう一度、2つの仕組みを比べてみよう
Comparableインターフェースを使う仕組みでは、データクラスがComparableインターフェースを継承したため、並べ替えの枠組みに組み込まれていました。逆に言えば、枠組みに組み込まれていないクラスは、並べ替えができませんでした。そのため、新しいデータクラスを作るたびに、もしも将来、並べ替える可能性があるのなら、念のために、Comparableインターフェースを継承させておかなくてはなりませんでした。これは困った制約に思えます。
外部クラスComparatorを使う仕組みは、並べ替えの対象である「社員」や「商品」といったデータクラスを、余計な継承関係からフリーにできるのが利点です。並べ替えたいクラスに対して、適切なComparatorを用意すれば、どのようなクラスでも並べ替えることができます。データクラスに手を加える必要がありません。
Java言語の枠組み
提供されている2つの枠組み
Java言語のコアクラスライブラリには、データを並べ替えるための汎用的な枠組みが、2つ用意されています。
- java.lang.Comparable
-
これは、本稿の「もう一度、異なるデータを並べ替える、共通の仕組みを作る」で作ったComparableインターフェースと同じものです。
- java.util.Comparator
-
これは、本稿の「もう一度、いろいろな順に並べ替える、共通の仕組みを作る」で作った外部クラスComparatorと同じものです。
コアクラスライブラリの中では、java.lang.Comparableが良く使われています。基本的なデータクラス、例えば
- 整数を表すクラス(Byte, Short, Integer, Long, BigInteger)
- 実数を表すクラス(Float, Double, BigDecimal)
- 日時を表すクラス(Date, Time)
- 文字を表すクラス(Character)
- 文字列を表すクラス(String)
などは、すべてjava.lang.Comparableインターフェースを実装しており、並べ替えができるようになっています。
Java言語の開発者はこう判断した
ところで、本稿では、並べ替えの枠組みを比べて、次のように述べました。
- Comparableインターフェースを使う仕組みは、データクラスが並べ替えの枠組みに組み込まれてしまうため、望ましくない。
- データクラスを余計な継承関係からフリーにできる、外部クラスComparatorを使う仕組みの方が良い。
この観点からすると、Integerのような基本的なデータクラスが、java.lang.Comparableを継承して、並べ替えの枠組みに組み込まれてしまっているのは、望ましくありません。java.lang.Comparableではなく、java.util.Comparatorを使う枠組みを利用して、Integerは何も継承せず、Integerの大小を判定するIntegerComparatorクラスをコアクラスライブラリに含めて提供する方が良い、ということになります。
しかし、Java言語のコアクラスライブラリは、逆の考えで作られています。
java.lang.Comparableとjava.util.Comparatorは、どちらもJ2SE 1.2で導入されました。よって、Integerがjava.lang.Comparableを実装しているのは、歴史的な経緯が理由ではありません。実際、J2SE 1.3以降に追加されたデータクラスにも、java.lang.Comparableを実装しているものがあります。Java言語の開発者は、java.lang.Comparableの方が良い仕組みだと判断したのです。
基本的なデータクラスを、いろいろな枠組みに組み込んでしまうと、やたらたくさんのインターフェースを実装することになります。これは、望ましいことではありません。
とはいえ、
基本的なデータクラスは、1つもインターフェースを実装してはいけない。あらゆる枠組みから、完全にフリーでなくてはいけない。
というのも、極端すぎます。
データの並べ替えは、たいへん良く使われる機能です。IntegerやStringのオブジェクトを並べ替えることも多いはずです。であれば、わざわざComparatorを別に用意するのは、面倒なだけです。基本的なデータクラスにjava.lang.Comparableを実装させ、そのまま並べ替えができるようにした方がシンプルである、と判断したのだと思います。