<<前ページ目次次ページ>>
5.比較する その1
〜java.lang.Comparable java.util.Arrays〜
 あるクラスのオブジェクトが二つあるとき、その大小はどうやって決めるのでしょうか?さらに一歩進めて、配列のソートをやってみましょう。
1列に並べー!
 息子は小学生ですが、先生が「1列に並べー」というと、背の順に並ぶそうです。「男子と女子は別だけどね、あと班ごとに3列に並ぶときもあるよ。」とのことですが、その辺はややこしいので、この際、話から省きます。

 では生徒というクラスを考えてみましょう。名前をStudentとします。Studentはインスタンス変数として、出席番号(no)、氏名(name)、身長(height)を持っていることとします。
public class Student {
        private int no;
        private String name;
        private double height;
        
        public Student(int no, String name, double height) {
                this.no = no;
                this.name = name;
                this.height = height;
        }
               
        public int getNo() {
                return this.no;
        }
        
        public String getName(){
                return this.name;
        }
        
        public double getHeight() {
                return this.height;
        }
        
        public void printStudent() {
                StringBuffer sb = new StringBuffer();
                sb.append(this.no);
                sb.append(" ");
                sb.append(this.name);
                sb.append(" ");
                sb.append(this.height);
                System.out.println(sb);
        }
}
 最後のメソッドprintStudentは、生徒のデータを出力しています。各データを+でつなげて出力すればよさそうですが、前の章でやった効率の悪さを思い出して、appendでつなげてみました。

 このクラスのオブジェクトとして相田くんと湯水くんを生成します。
   Student a = new Student(1, "相田 徹", 152.5);
   Student b = new Student(5, "湯水 敦", 145.0);
 相田くんは出席番号1番で身長152.5cm、湯水くんは出席番号5番で身長145.0cmです。背の順では相田くんは、湯水くんの後ろになります。


 たくさんのデータをある基準で並べ替えることをソートといいます。小さい順に並べるのを昇順、大きい順に並べるのを降順といいます。ちょっと待ってください。一体、相田くんと湯水くん二つのオブジェクトの大小はどうやって決めるのでしょうか?

 確かに普段は背の順に並ぶ子供たちも、予防接種やドキドキの通知表をもらうときなどは、出席番号順なのではないでしょうか。この章では普段の背の順で、オブジェクトの大小を決めることにしましょう。出席番号順は特別の場合として、次の章でやることにします。

 背の順ならばインスタンス変数heightを比較すればいいですね。あるクラスから生成されるオブジェクトの大小関係は、そのクラスの中でcompareToというメソッドを使って定義できます。上のクラスStudentに次のメソッドを追加します。仮引数はObject型、戻り値はint型にしてください。
        public int compareTo(Object o) {
                double h1 = this.height;
                double h2 = ((Student)o).height;
                
                if(h1 == h2) {
                        return 0;
                } else if(h1 > h2) {
                        return 1;
                } else {
                        return -1;
                }
        }       
 このメソッドを呼び出したオブジェクトの身長がh1、引数のオブジェクトの身長がh2になります。
  h1とh2が等しいとき 0を返す
  h1>h2のとき     1を返す
  h1<h2のとき     −1を返す
となっています。

 さらにStudentはinterfaceのComparable(java.lang.Comparable)を実装していると宣言しておきます。ComparableのメソッドはただひとつcompareToのみです。「implements Comparable」があることで、Studentから生まれるオブジェクトは、メソッドcompareToで比較できるんだな、とわかりますね。
public class Student implements Comparable {
        private int no;
        private String name;
        private double height;
        
        public Student(int no, String name, double height) {
                this.no = no;
                this.name = name;
                this.height = height;
        }
        
        public int compareTo(Object o) {
                double h1 = this.height;
                double h2 = ((Student)o).height;
                
                if(h1 == h2) {
                        return 0;
                } else if(h1 > h2) {
                        return 1;
                } else {
                        return -1;
                }
        }
       :
       (略)
       :       
}
 では相田くんと湯水くんのオブジェクトを生成して、compareToで比較してみましょう。
public class Main0 {
        public static void main(String[] args) {
                Student a = new Student(1, "相田 徹", 152.5);
                Student b = new Student(5, "湯水 敦", 145.0);
        
                System.out.println(a.compareTo(b));
                System.out.println(b.compareTo(a));
        }
}
 出力結果は
  1
  -1
となります。

 まず最初の
   a.compareTo(b)
では、aとメソッド内のthisが対応するんでしたね。compareTo内を見ていきましょう。h1には相田くんの身長が入ります。さて仮引数oの型はObjectです。つまりどんなオブジェクトの参照値でも代入できるわけです。ここでは湯水くんのオブジェクトの参照値が入ります。o.heightとはできませんので、まず「(Student)o」でoの値をStudentに型変換してから、インスタンス変数heightを取り出します。このとき「(Student)o.height」とすると、「(Student)(o.height)」のように演算されることになりエラーになります。括弧の位置に注目しましょう。h2は湯水くんの身長になります。
 次の
   b.compareTo(a)
では、h1とh2が逆になるので、h1<h2で-1が戻されます。

 メソッドcompareToは、thisが仮引数のオブジェクトより大きい場合に正の値、小さいときに負の値、等しいときに0を返すようにしてください。ここでは身長h1とh2を比べて、0か1か-1を返しています。

(注)ここでh1==h2とdouble型の値を==で比較していますが、もしh1やh2が計算した結果の値であれば、このようなことやらないでください。ライブラリ編第2章でやったように、浮動小数点データは誤差を伴うので、計算結果がピタリ一致するというより、h1-h2の値が十分小さければ等しい、とします。


1列に並べー!

 せっかくソートに触れたので、一クラス分の生徒を背の順に並べ替えてみましょう。といっても何十人もの生徒では解説の図を作るのも大変なので、ぐっと小規模に5人のクラスにしましょう。Student型の長さ5の配列に、5人分の生徒のオブジェクトの参照を記憶させます。配列名はumeにしましょう。(私は高校3年間はずーっと梅組でした。)添字を出席番号にすればいいのですが、ここでは説明上最初は適当な順で生徒を突っ込みます。

 クラスStudentにcompareToが定義されていれば、java.util.Arraysのクラスメソッドsortを使って次のように一発で昇順にソートできます。
import java.util.Arrays;
public class Main1 {
        public static void main(String[] args) {
                Student[] ume = new Student[5];
                ume[0] = new Student(2, "木下 保美", 141.5);
                ume[1] = new Student(5, "湯水 敦", 145.0);
                ume[2] = new Student(1, "相田 徹", 152.5);
                ume[3] = new Student(4, "目加田 重三", 136.0);
                ume[4] = new Student(3, "橋 航", 145.0);

                Arrays.sort(ume); 
      
                for (int i = 0; i < ume.length; i++) {
                        ume[i].printStudent();
                }
        }
}
 出力結果は次のようになります。

4 目加田 重三 136.0
2 木下 保美 141.5
5 湯水 敦 145.0
3 橋 航 145.0
1 相田 徹 152.5

 どうですか、背の順にソートされていますね。

 ソート前とソート後の配列の様子を図にします。



 並べ替えると言っても、子供があちこち動いて整列するのと違い、図のように、オブジェクトの位置は変わらず、配列要素に記憶されているオブジェクトへの参照が変更されるのみです。もしオブジェクトがあちこち移動しなければならないとしたら、えらく時間が掛かりそうですね。

 さてソートの方法にはいろいろあって、データの数が少なければどれでも大差ないのですが、データ数が多くなると、どの方法を選ぶかで処理時間に雲泥の差が出てきます。バブルソート、クイックソート、マージソートなどアルゴリズムを調べるとまたこれはこれで面白いのですが、せっかく速いソートをしてくれるArrays.sortを使わない手はありません。たった1行で済みます。またint型やdouble型の配列についてもソートしてくれます。使ってみてください。

 ソートのさまざまな方法について、「安定しているかどうか」という指標があります。上の例で、湯水くんと橋くんは身長が同じです。背の順に並べ、と言われればどちらが前でもかまわないのですが、安定したソートでは、元の順序、この場合湯水くんのほうが橋くんより前にいたので、並べ替え後も湯水くんが橋くんの前に来ます。Arrays.sortは安定したソートです。

 とは言え、compareToの中身によっては、「おや?Arrays.sortは安定したソートのはずなのにぃ、バグかな?」などと思い悩むこともあります。クラスStudent中のcompareToを次のようにしたら、同じデータでも、並べ替え後の湯水くんと橋くんの順序が上と逆になります。
        public int compareTo(Object o) {
                double h1 = this.height;
                double h2 = ((Student)o).height;
                
                return h1 > h2 ? 1 : -1;
        }
 「?:」はこれでひとつの演算子になります。上では、「h1>h2」が真の場合、「:」の左側、つまり1がこの式の値になり、偽の場合、「:」の右側の-1が式の値になります。簡単なif文が1行で書けるので、こんな場合に便利です。
 さて順序の異変は、比較する二つのオブジェクトのheightが等しいとき、つまりh1とh2が等しいとき、0を返さず、-1を返しているため起こったことです。compareToの定義は慎重に行いましょう。わかりづらいバグにつながりかねません。


 今まで登場した標準ライブラリ中のクラスも、compareToが定義されています。例えばクラスDoubleは、オブジェクトが持っているdouble型の値の比較をします。クラスStringでは、文字列の比較を辞書順で行います。辞書で前にあるほうが小さいと考えるわけですね。

 もうひとつだけ。クラスObjectで定義されている、メソッドtoStringやequalsとちがって、compareToは標準では提供されません。自前のクラスを作ったとき、もしオブジェクトの比較が必要になるのならば、自分でcompareToを定義してください。もちろん「implements Comparable」を忘れずに。


 背の順に並ぶのが基本の梅組の小学生たちも、予防接種や身体検査のときは、出席番号順に並びます。次の章では、出席番号順にソートするにはどうしたらいいかをやろうと思います。乞うご期待。
<<前ページ目次次ページ>>
Copyright (c) 2004-2005 Nagi Imai All Rights Reserved..