Javaプログラミング基礎

講義資料

オブジェクト指向プログラミング(4)

これまでのプログラムは、mainメソッドと、mainメソッドが利用するオブジェクトが存在するものでした。 複雑なプログラムでは、複数のオブジェクトが関わって仕事をしていくことになります。 今回は、複数のオブジェクトを使ったプログラムについて考えてみましょう。

オブジェクトが別のオブジェクトを保有する: 包含

複数のオブジェクトからなるプログラムについて、 あるオブジェクトとあるオブジェクトに「全体-部分」 の関係が成り立っている場合、 この関係のことを「包含 (composition)」や「集約 (aggregation)」と呼びます。 包含の関係には、 あるオブジェクトが別のオブジェクトを保有し管理責任を持つという意味や、 あるオブジェクトの機能の一部を切り出して別のオブジェクトに仕事を委ねるという意味があります。

例題: 機能の一部を委ねる

電大では、成績による順位を決めるために GPA (Grade Point Average) というポイントを計算しています。 GPA は以下の計算式で計算することができます。

GPA = (Sを取った科目の合計単位数 * 4 + Aを取った科目の合計単位数 * 3 +
   Bを取った科目の合計単位数 * 2 + Cを取った科目の合計単位数 * 1) / すべての合計取得単位数☆

☆ここでは単純化していますが正確には「履修単位数(自由科目を除く)」です。厳密な定義は事務部に確認してください。なお、早期卒業を目指す人は今から履修計画を練っておきましょう。

ここで、複数人の学生のGPAの成績を処理し、 平均GPA値を計算するプログラムを考えてみましょう。

このプログラムでは、学生のGPAを計算するわけですから 1人の学生を表しGPAを計算できるようなクラスが必要です。 そして、複数名の学生のGPAを集約して平均を求めるクラスも必要です。 今回は、1人学生を表すクラスを TDUStudent とし、 複数の学生を集約するクラスを TDUStudentsRecord とします。

まず、クラス TDUStudent には、 以下のように、 これまでのような形で一人の学生に関する属性とメソッドを定義しましょう。

class TDUStudent {
    String name;
    int numberOfS;
    int numberOfA;
    int numberOfB;
    int numberOfC;

    TDUStudent(String n, int s, int a, int b, int c) {
	name = n;
	numberOfS = s;
	numberOfA = a;
	numberOfB = b;
	numberOfC = c;
    }

    int getGradePointAvegage() {
	return (numberOfS * 4 + numberOfA * 3 + numberOfB * 2 + numberOfC * 1) / 
               (numberOfS + numberOfA + numberOfB + numberOfC);
    }
}

次に、クラス TDUStudent のオブジェクトを集約し、 複数の電大生のGPAを処理するクラス TDUStudentsRecord を定義します。 クラス TDUStudentsRecord の定義は次のようになります。

class TDUStudentsRecord {
    TDUStudent[] students;

    TDUStudentsRecord(int numberOfStudents) {
	students = new TDUStudent[numberOfStudents];
    }

    void registerStudent(int id, TDUStudent s) {
	students[id] = s;
    }

    int averageGPA() {
	int sum = 0;
	for(int i = 0; i < students.length; i++)
	    sum = sum + students[i].getGradePointAverage();
	return sum / students.length;
    }
}

クラス TDUStudentsRecord は、 複数の TDUStudent オブジェクトを属性に持っています。 このように、クラスが別のクラスのオブジェクトを取り込むような、クラス同士の関係を包含と言います。

複数の電大生のGPAの平均を求めるメソッド averageGPA を見てみましょう。 このメソッドでは、TDUStudent のGPAを求めるメソッド getGradePointAverageを下請けとして使っています。 学生個人のGPAの計算を、包含関係にあるクラス TDUStudent に委ねている、 というわけです。

このように、 クラスの機能の一部を包含関係にある別のクラスに委譲することができます。 包含によってクラス A がクラス B を取り込んだ状態を、 英語で Class A has a class B と言うことから、 has-a の関係と呼びます。

この例題で扱ったプログラムの全体を以下に示します。 (ファイル名: TDUManager.java)

class TDUManager {
    public static void main(String[] args) {
	TDUStudentsRecord record = new TDUStudentsRecord(3);
	
	TDUStudent s1 = new TDUStudent("電大 メディ男", 2, 4, 3, 2);
	TDUStudent s2 = new TDUStudent("電大 デン次", 1, 5, 2, 2);
	TDUStudent s3 = new TDUStudent("電大 デン子", 3, 3, 1, 0);

	record.registerStudent(0, s1);
	record.registerStudent(1, s2);
	record.registerStudent(2, s3);

	System.out.println("GPAの平均: " + record.averageGPA());
    }
}

class TDUStudentsRecord {
    TDUStudent[] students;

    TDUStudentsRecord(int numberOfStudents) {
	students = new TDUStudent[numberOfStudents];
    }

    void registerStudent(int id, TDUStudent s) {
	students[id] = s;
    }

    int averageGPA() {
	int sum = 0;
	for(int i = 0; i < students.length; i++)
	    sum = sum + students[i].getGradePointAverage();
	return sum / students.length;
    }
}

class TDUStudent {
    String name;
    int numberOfS;
    int numberOfA;
    int numberOfB;
    int numberOfC;

    TDUStudent(String n, int s, int a, int b, int c) {
	name = n;
	numberOfS = s;
	numberOfA = a;
	numberOfB = b;
	numberOfC = c;
    }

    int getGradePointAvegage() {
	return (numberOfS * 5 + numberOfA * 3 + numberOfB * 2 + numberOfC * 1) / 
               (numberOfS + numberOfA + numberOfB + numberOfC);
    }
}

例題: 複数のオブジェクトを集約するクラスを作る

1曲の音楽を表すクラス Music を定義してみましょう。 曲には「曲名」と「アーティスト名(演奏者)」という属性があるとします。

class Music {
    String name;
    String artist;

    Music(String n, String a) {
        name = n;
        artist = a;
    }

    String getName() {
        return name;
    }

    String getArtist() {
        return artist;
    }
}

クラス Music のオブジェクトを集めて、 たくさんの音楽がつまったジュークボックスを定義してみます。 このジュークボックスで音楽が演奏される様子をプログラムで再現してみましょう。

クラス JukeBox の定義は次のようになります。

class JukeBox {
    Music[] songs;

    JukeBox() {
	songs = new Music[10];
    }

    JukeBox(int numberOfSongs) {
	songs = new Music[numberOfSongs];
    }

    void setMusic(int no, String title, String artist) {
        songs[no] = new Music(title, artist);
    }

    void play(int no) {
        System.out.println("Now playing: " + songs[no].getName() + " by " +
	                   songs[no].getArtist());
    }
}

クラス JukeBox は、たくさんの音楽を格納することができます。 このように、複数のオブジェクトを集約し管理するような クラスを作ることができます。

例題: 仕様の変更に対応できるように機能の一部を分割する

クラスを設計する際には、 1つのクラスの役割が大きくなりすぎないように注意する必要があります。 役割が大きすぎると感じたときには、 クラスを細かい対象物に分割し、包含を使うと良い場合があります。 また、将来変更の可能性のある部分は、 独立したクラスとして設計し、包含関係を定義することが良い場合があります。

次の例は社員の給料を計算するプログラムです。 給与体系は正社員とアルバイトで異なるため、 日給計算を行うクラスを独立させることにします。 そして、正社員の日給を計算するクラスと、 アルバイトの日給を計算するクラスに分けて設計してみます。

以下のプログラムは正社員の月給を計算するプログラムですが、 下線の Staff の部分だけを Arbeit に変更すれば、 アルバイトの月給を計算するプログラムに変えることができます。

class SalaryMan {
    public static void main(String[] args) {
	SalaryCalculation company = new SalaryCalculation();

	company.setMember(new Staff("電大 メディ男"));

	// 9-18時で24日間働いたときの1ヶ月の給料を計算
        int thisMonthsSalary = company.getMonthlySalary(9, 18, 24);

	System.out.println("今月の給料: " + thisMonthsSalary);
    }
}

class SalaryCalculation {
    Staff member;

    void setMember(Staff m) {
	member = m;
    }

    // 始業時間、就業時間、労働日数から1ヶ月の給料を計算
    int getMonthlySalary(int from, int to, int days) {
	return member.getDairySalary(from, to) * days;
    }
}

// 正社員
class Staff {
    String name;

    Staff(String n) {
        name = n;
    }

    // 1日の給料は就業時間に関わらず 10000 円
    int getDairySalary(int from, int to) {
	return 10000;
    }
}

// アルバイト
class Albeit {
    String name;
    
    Albeit(String n) {
        name = n;
    }

    // 1日の給料は時給800円
    int getDairySalary(int from, int to) {
	return (to - from) * 800;
    }
}

2つのオブジェクトの相互作用

複数のオブジェクトが登場するプログラムの理解を深めるために、 2つのオブジェクトが相互にやりとりをしながら仕事をしていくような プログラムの例をいくつか見ていきましょう。

例題: 2つの点の距離を求める

class DistanceBetweenPoints {
    public static void main(String[] args) {
        Point p1 = new Point(0,0);
        Point p2 = new Point(7,7);

        System.out.println("2つの点の距離: " + p1.calcDistance(p2));
    }
}

class Point {
    int x;
    int y;

    Point(int lx, int ly) {
        x = lx;
        y = ly;
    }

    int getX() {
        return x;
    }
    int getY() {
        return y;
    }

    /** 他の点との距離 */
    double calcDistance(Point p) {
        return Math.sqrt((x - p.getX())*(x - p.getX()) +
                         (y - p.getY())*(y - p.getY()));
    }
}

このプログラムは2次元平面上の「点」をクラスとした プログラムです。

メソッド calcDistance の実行の様子を見てみましょう。

System.out.println("2つの点の距離: " + p1.calcDistance(p2));

....
....

double calcDistance(Point p) {
    return Math.sqrt((x - p.getX())*(x - p.getX()) +
                     (y - p.getY())*(y - p.getY()));
}

p1p2 は、独立したオブジェクトであることを思い出してください。 p1p2 は「点クラスの仲間」という性質は共通ですが、 内部に持つ情報は別のものです。 ここで、p1.calcDistance(p2) と書くと、 p1 ((0,0)の点) に関する calcDistance が実行されます。

このメソッドの引数は、クラス Point のオブジェクト p です。 これまで、メソッドの引数には主に int 型や double 型の値を使ってきましたが、 オブジェクトもメソッドの引数に使うことができるのです。

このメソッド内部の計算を詳しく見てみましょう。

Math.sqrt((x - p.getX())*(x - p.getX()) +
          (y - p.getY())*(y - p.getY()));

Math.sqrt( ..... ) は、あらかじめ Java に備わっている メソッドの一種です。 引数に指定されたの値の平方根 (ルート) を計算することができます。

さて、この式での xy は、 言うまでもなく p1 の座標です。

では、 p.getX(), p.getY() はどうでしょうか。 メソッドcalcDistance内の p は、 メソッドの実行元の引数 p2 のことです。 p2 に対して getX(), getY() を実行した結果、 すなわち、p2の x 座標と y 座標を求めているのです。

p1p2 の座標が分かれば、 2つの点の間の距離を計算するのは簡単です。

メソッド calcDistance の働きを整理してみましょう。 このメソッドは、自分の点と引数に指定された別の点との距離を計算するメソッドだ、 と言うことができます。 このように、メソッドでは引数に別のオブジェクトを受け取り、 自分自身の属性と、引数に指定された別のオブジェクトの情報を使って 計算を行うこともできます。

例題: ある点から見て2つの点のうち近い方を選ぶ

もう1つ、2次元平面上の点を題材にしたプログラムを示します。 自分の点から見たとき2つの点のうち近い点を選ぶプログラムです。

class CloseBetweenPoints {
    public static void main(String[] args) {
        Point origin = new Point(0,0);
        Point p1 = new Point(7,7);
        Point p2 = new Point(5,9);
        Point closePoint;

        closePoint = origin.chooseCloser(p1, p2);

        System.out.println("近い方の点の座標は (" +
                           closePoint.getX() + ", " + closePoint.getY() + ")");
    }
}

class Point {
    int x;
    int y;

    Point(int lx, int ly) {
        x = lx;
        y = ly;
    }

    int getX() {
        return x;
    }
    int getY() {
        return y;
    }

    /** 2つの点から近い方の点を返す */
    Point chooseCloser(Point a, Point b) {
        double distanceToA = Math.sqrt((x - a.getX())*(x - a.getX()) +
                                       (y - a.getY())*(y - a.getY()));
        double distanceToB = Math.sqrt((x - b.getX())*(x - b.getX()) +
                                       (y - b.getY())*(y - b.getY()));
        if (distanceToA < distanceToB)
	    return a;
        else
            return b;
    }
}

メソッド chooseClose は、 引数に与えられた 2 つの点オブジェクトと自分自身の座標から距離を計算し、 近い方の点オブジェクトを返す働きをするメソッドです。 このように、オブジェクトそのものを return 文で返すことができます。 chooseClose メソッドは、Point close( .... ) のように、 Point クラスのオブジェクトを返すように宣言しておきます。

メソッド main 内の変数 closePoint の宣言を見てみましょう。 この変数は new 演算子で新たなオブジェクトを生成する必要はありません。 メソッド close から返される、p1p2 のいずれかの点オブジェクトを代入するために使うからです。