Javaプログラミング基礎 講義資料

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

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

配列を属性に持つクラス

本題に入る前に、配列を属性に持つクラスについて考えます。 まず、基本的な配列の使い方について復習します。

  1. 配列の変数を宣言
    int[] a;
    

    配列を使う際には、まず扱うデータに合わせた型で変数を宣言します。 上の例では、int型の配列を扱う変数 a を宣言しています。

  2. 配列の領域を確保
    a = new int[10];
    

    次に、new 演算子を使い実際にデータを格納する領域を確保します。 上の例での int の部分は領域を用意する型を示し、 10 は用意するデータの個数を示します。 この変数 a を使って10個のint型の値を扱うことできるようになります。

  3. 配列の利用
    a[0] = 1;
    System.out.println(a[1]);
    

    配列にデータを代入したり内容を取り出したりするには、 変数名と添字 (要素の番号) を指定して行います。 上の例では変数 a では 0〜9 までの10とおりの添字が指定できます。

それでは、int型の配列を属性に持ち、値の集合を扱うようなクラスについて考えましょう。

public class DataUser {
    public static void main(String[] args) {
        Data d = new Data(10);

        d.setValue(0,0);
        d.setValue(1,2);
        d.setValue(2,4);
        d.setValue(3,6);
        d.setValue(4,8);
        d.setValue(5,10);
        d.setValue(6,12);
        d.setValue(7,14);
        d.setValue(8,16);
        d.setValue(9,18);

        System.out.println("5番目は " + d.getValue(5));
        d.printAllValue();
    }
}

class Data {
    int[] value;

    Data(int count) {
        value = new int[count];
    }

    void setValue(int i, int v) {
        if (i < 0 || i >= value.length) {
            System.out.println("扱うことができる添字の範囲を越えています");
            return;
        }

        value[i] = v;
    }

    int getValue(int i) {
        if (i < 0 || i >= value.length) {
            System.out.println("扱うことができる添字の範囲を越えています");
            return -1; // -1はエラーを表すとする
        }

        return value[i];
    }

    void printAllValue() {
        for (int i = 0; i < value.length; i++)
            System.out.print("value[" + i + "]:" + value[i] + " ");
        System.out.println();
    }
}

クラスDataは属性にint型の配列である変数 value を持ち、 この配列を利用するメソッドを備えます。コンストラクタと各メソッドの働きを 説明します。

コンストラクタ
コンストラクタでは、新たにオブジェクトを生成する際に、 何個のデータを扱うかを指定して配列の領域を生成しています。 扱うデータの個数をあらかじめ決めておくのではなく、 コンストラクタで適切に指定できることにより、 大量のデータを扱う際には多くの領域を用意し データが少量の際には少ない領域を用意するといったことができます。
setValue, getValue
配列の添字を指定して値を登録するメソッドと、 同様にして値を取り出すメソッドです。 指定された添字が、配列として用意されている範囲内にあるかどうかを確かめ、 範囲外の場合はエラーを出すようにし、予期せずプログラムが停止してしまうことの ないようにします。
printAllValue
繰り返しを用いて配列全体を表示しています。

それでは、本題に入りましょう。

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

複数のオブジェクトからなるプログラムについて、 あるオブジェクトとあるオブジェクトに「全体-部分」 の関係が成り立っている場合、 この関係のことを「包含 (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;
    }

    double getGPA() {
	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 setStudent(int id, TDUStudent s) {
	students[id] = s;
    }

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

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

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

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

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

public class TDUManager {
    public static void main(String[] args) {
	TDUStudentsRecord record = new TDUStudentsRecord(3);
	
	TDUStudent mikio = new TDUStudent("電大 未来男", 2, 4, 3, 2);
	TDUStudent kumiko = new TDUStudent("電大 来未子", 1, 5, 2, 2);
	TDUStudent mikako = new TDUStudent("電大 未科子", 3, 3, 1, 0);

	record.setStudent(0, mikio);
	record.setStudent(1, kumiko);
	record.setStudent(2, mikako);

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

class TDUStudentsRecord {
    TDUStudent[] students;

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

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

    double averageGPA() {
	double sum = 0;
	for(int i = 0; i < students.length; i++)
	    sum = sum + students[i].getGPA();
	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;
    }

    double getGPA() {
	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, Music m) {
        songs[no] = m;
    }

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

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

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

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

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

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

public 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つの点の距離を求める

public 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つの点のうち近い点を選ぶプログラムです。

public 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 のいずれかの点オブジェクトを代入するために使うからです。