並べ替えのクラス設計

作成:2008年4月10日

吉田誠一のホームページ   >   ソフトウェア工学   >   技術コラム   >   オブジェクト指向

データを並べ替えるプログラムは、どのように書けば良いのでしょうか。

…と言っても、ここでは、ソートのアルゴリズムについてお話ししたい訳ではありません。

データの並べ替えは、たくさんのソフトウェアで必要となる機能です。しかし、ソフトウェアを作るたびに、いちいちデータを並べ替えるプログラムを書いていたのでは大変です。多くのソフトウェアから共通に利用できる、汎用的な枠組みを作っておくと便利です。

では、汎用的な並べ替えの枠組みとは、どのようなものでしょうか。どんなデータも、どのようにも並べ替えられるためには、どんなクラスを作れば良いのでしょうか。

ここでは、データの並べ替えを、クラス設計の視点から考えてみます。

目次

  1. まったく汎用性のないプログラム
    1. 並べ替えるのは、「社員」と「商品」
    2. 「社員」を並べ替えよう
    3. 「商品」も並べ替えよう
    4. いろいろな順に並べ替えよう
    5. 同じような関数がいくつもできてしまった
  2. 汎用化への道
    1. 異なるデータを並べ替える、共通の仕組みを作る
    2. いろいろな順に並べ替える、共通の仕組みを作る
    3. 2つの仕組みを比べてみよう
  3. さらなる汎用化への道
    1. 数値以外の属性で並べ替えよう
    2. もう一度、異なるデータを並べ替える、共通の仕組みを作る
    3. もう一度、いろいろな順に並べ替える、共通の仕組みを作る
    4. もう一度、2つの仕組みを比べてみよう
  4. Java言語の枠組み
    1. 提供されている2つの枠組み
    2. Java言語の開発者はこう判断した

まったく汎用性のないプログラム

並べ替えるのは、「社員」と「商品」

ここでは例として、「社員」と「商品」という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 ( ) {
                // 値段を返す
        }
}
          

この仕組みをクラス図で表すと、次のようになります。

Valueインターフェースを使う仕組み

しかし、これでは、「社員」はいつも「年齢」でしか並べ替えることができません。

いろいろな順に並べ替える、共通の仕組みを作る

次に、同じ「社員」でも、時によって「年齢」で並べ替えたり、「身長」で並べ替えたりできるような、共通の仕組みを考えます。

そのために、並べ替えの基準となる値を取り出す部分を、外部クラスとして分離します。具体的には、以下のような外部クラスを作ります。

// 並べ替えの基準となる値を取り出す外部クラスのインターフェース
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();
        }
}
          

この仕組みをクラス図で表すと、次のようになります。

外部クラスValueGetterを使う仕組み

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の「値段」の大小を判定した結果を返す
        }
}
          

この仕組みをクラス図で表すと、次のようになります。

Comparableインターフェースを使う仕組み

しかし、これでは、「社員」はいつも「名前」でしか並べ替えることができません。

もう一度、いろいろな順に並べ替える、共通の仕組みを作る

次に、同じ「社員」でも、時によって「名前」で並べ替えたり、「年齢」で並べ替えたりできるような、共通の仕組みを考えます。

そのために、データの大小をデータ自身に判定させる部分を、外部クラスとして分離します。具体的には、以下のような外部クラスを作ります。

// データの大小を判定するす外部クラスのインターフェース
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の「値段」の大小を判定した結果を返す
        }
}
          

この仕組みをクラス図で表すと、次のようになります。

外部クラスComparatorを使う仕組み

もう一度、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を実装させ、そのまま並べ替えができるようにした方がシンプルである、と判断したのだと思います。

Copyright(C) Seiichi Yoshida ( comet@aerith.net ). All rights reserved.