ソフトウエア入門
ohmi@rsch.tuis.ac.jp

解答例

参考:教科書 pp.116-121

オブジェクト指向超入門

オブジェクト指向の定義は、専門家の間でも微妙に食い違っているが、 「ソフトウェアで扱う事柄について、データと操作(メソッド)をまとめて1つのオブジェクトとして捉える」 ということは共通している。オブジェクト指向ではオブジェクトを基本単位として考え、 1つのオブジェクトの中には、データとメソッドが備わっているとする。

なお、データとメソッドの個数はそれぞれ自由であり、 データのないオブジェクトやメソッドのないオブジェクト(C言語でいう構造体、Pascalでいうレコード型)を作ることが可能である。 しかし、これらはオブジェクト指向らしくない使い方であり、あまり使うことはない。


図: オブジェクトはフィールド(データ)とメソッドを一つにまとめたもの

データとは

データはプログラム中で扱う値であり、いわゆる変数である。 Javaではオブジェクトが持つデータのことをフィールド(field)と呼ぶ。 フィールドとして定義できるものは、今まで変数として使ってきたものと同じである。 つまり、int, float, double などの原始型、String型や配列などの参照型のフィールドが 定義できる。

フィールドは、オブジェクトの性質(特性)を持つという意味で、属性(attribute, property)とも呼ばれる。

メソッドとは

前回習ったメソッドである。ただし、前回のメソッドには先頭に"static"が付いていたが、 オブジェクトに対して使う場合は、staticを付けない。詳細は次回の演習で述べる。

メソッドの目的はオブジェクト内のフィールド(データ)を操作することと言える。 また見方を変えると、メソッドはオブジェクトに対するメッセージと言える。 例えば、友人に「マンガを貸して」と言う(メッセージを送る)と、マンガを貸してくれる (貸してくれないかもしれないが)ように、例えば、あるオブジェクトに対して、 rent(貸してくれ)というメッセージを送ると、受け取ったオブジェクトの中のrentメソッド が実行され、何かを貸してくれるという動作をするようにプログラムを書くことができる。


図:メッセージを送ると対応するメソッドが実行される

メッセージを送ると返事が返ってくる。返事はメソッドの返り値である。 つまり、int型の返り値なら整数値の返事が返ってくる。void型なら、 「返事なし」である。

メソッドはオブジェクトに挙動を与えるものであるという意味合いから、 メソッドの役割のことを振る舞い(behavior)ということがある。

クラスとは

多くのオブジェクト指向言語では、クラスという仕組みがある。 クラスには、オブジェクトを作る際のデータやメソッドを定義してあり、 いわばオブジェクトの設計図である。 オブジェクトを作る時には、必ずクラス(型)を指定する必要がある。 指定したクラスに定義されたデータとメソッドがオブジェクトに備わるのである。

あるクラスのオブジェクトを作る場合に、そのオブジェクトのことをインスタンス(実例)という場合もある。 本によってはインスタンスという用語を使っているものもあるがオブジェクトと同じ意味だと考えて差し支えない。

クラスのないオブジェクト指向言語もある。 そのような言語では、既にあるオブジェクトをコピーしてオブジェクトを作ったりする。 また、SmalltalkやRubyなど、クラスもオブジェクトである言語もある(実はJavaも実行中に存在するクラス をClassクラスのオブジェクトとして扱っている)。

クラスは以下のように定義する。

class クラス名 {
    フィールド(データ)の定義
    フィールド(データ)の定義
    ...
    メソッドの定義
    メソッドの定義
    ...
}

以前のメソッドの演習では、上記のメソッドの定義のみがあって、フィールドの定義が 1つもない状態であった。今まで演習で作ってきたプログラムも全てクラスを作っていたということである。

ここで実例を挙げる

class VideoTape {
    public int position=0;			// 現在の位置
    public void forward(int t) {		// tだけ先に早送りする
        position += t;
    }
    public void back(int t) {			// tだけ巻き戻す
        position -= t;
    }
}

これは、ビデオテープのクラスである。フィールド(データ)としてpositionがあり、 これはテープが現在、先頭からどの位置に巻かれているかを示す。 position=0とあるので、最初は先頭の位置である。 メソッドは2つある、forwardとbackである。forwardは引数mの時間だけテープを早送りする。 backは引数mの時間だけテープを巻き戻す。

フィールドとメソッドの定義には先頭に"public"が付いていることに注意してほしい。 この意味は後で述べる。

次にこのVideoTapeクラスを使ってみよう。

/* VideoExample.java - ビデオテープクラスの使用例
*/
class VideoTape {
    public int position=0;			// 現在の位置
    public void forward(int t) {		// tだけ先に早送りする
        position += t;
    }
    public void back(int t) {			// tだけ巻き戻す
        position -= t;
    }
}

public class VideoExample {
    public static void main(String[] args) {
        VideoTape vt = new VideoTape();
        System.out.println("最初の位置" + vt.position + "秒");
        vt.forward(600); // 600秒進める
        System.out.println("進めた後の位置" + vt.position + "秒");
        vt.back(150); // 150秒戻す
        System.out.println("巻き戻した後の位置" + vt.position + "秒");
   }
}

上のプログラムには、VideoTapeとVideoExampleという2つのクラスが定義されている。 VideoExampleは、今まで習ったものと同様でmainメソッドが書かれている。 ちなみにこのような場合、javaファイルの名前は、mainメソッドがあるクラスの名前にする。 上の場合は、VideoExample.javaである。

メンバ

クラスやオブジェクトが持つフィールドとメソッドのことをメンバという。 今後メンバという用語が出てきたら、フィールドあるいはメソッドのことだと理解してほしい。

クラス名、メンバ名の付け方

Javaでは、慣習として、クラス名は頭文字を大文字に、 メンバ名(フィールド名とメソッド名)は頭文字を小文字にする(ただし、定数は大文字)。また、変数名やメソッドの引数の名前も頭文字を小文字にする。

例えば、上記の例でいうと、VideoTapeクラス、forwardメソッド、positionフィールド、forwardメソッドの引数tなどである。

ただし、これはあくまで慣習であって規則ではない。 したがって、守らなくてもエラーが出るようなことはない。 しかし、一般のJavaプログラムはこの慣習を守っているため、 混乱を避けるためにも慣習に従うべきである。

オブジェクトの使い方

それでは、上記のmainメソッドの中を見ていこう。 まず最初にVideoTapeクラスのオブジェクトを作っている(VideoTape vt = new VideoTape();)。 これは正確に言うと、new VideoTape() でVideoTape型のオブジェクトを生成し、それをvtに代入している。
※ オブジェクトの生成については、参照と配列を見て復習せよ。new演算子を使って指定したクラスに基づいたオブジェクトを作るのである。

次におなじみのprintlnメソッドを使って vt.position を表示している。 vt.positionはvtというオブジェクトが持つ、positionフィールド(データ) を意味する。このように、オブジェクトが持っているフィールドにアクセスするには、 オブジェクト名.フィールド名と書く。間に.を打つわけである。

次の文では、forwardメソッドを呼び出している(vt.forward(600);)。 このようにオブジェクトが持っているメソッドを呼び出すには、 オブジェクト名.メソッド名(引数...)と書く。 (引数...)のところがあるのがフィールドとの違いである。 つまり引数のないメソッドを呼び出す場合は、オブジェクト名.メソッド名()となる。

vt.forward(600);でvtというオブジェクト、つまりVideoTape型の オブジェクトのforwardメソッドが呼ばれる。 VideoTapeクラスで定義されているforwardメソッドが呼ばれるわけである。 forwardメソッドには、position += t;とかかれている。 これで引数として渡した600(秒)がpositionに足されるのである。

mainメソッドからpositionにアクセスするのに、vt.positionと書いたのに、forwardメソッド内では、 単にpositionと書いていることに注目してほしい。そのクラスに定義されているフィールドや メソッドを使う時にはオブジェクト名を書かなくて良い。この場合だと、forwardメソッドと positionフィールドは同じクラス(VideoTape)に属しているからである。対して、自分以外の オブジェクトに属しているメソッドやフィールドを使う場合は、オブジェクト名を書く必要がある。

実は、同一のクラスのフィールドやメソッドを使う場合、厳密には、 this.フィールド名もしくはthis.メソッド名(引数...) と書く。つまり、オブジェクト名を書かない場合は、この"this."が省略されているのである。 this. は他の名前と重なるような曖昧なことがなければ省略できる。

次の、printlnメソッドで、再び vt.position を表示している。すると、先ほど forwardメソッドが 呼ばれて600秒が加算されたので、vt.positionが600になっていることが分かる。

次に、backメソッドを呼び出している(vt.back(150);)。 もちろん、理屈は上記のforwardメソッドと同様である。backメソッドでは、 position -= t;が実行されるので、引数の150(秒)が positionから引かれる。次のprintln文では、positionの値、つまり450(秒)が表示されるわけである。

アクセス制御

ところで、以上のプログラムは別にメソッドを呼び出さなくてもテープの位置を変えられる。 forwardやbackメソッドを呼ばすに、例えば vt.position = vt.position + 600; というふうに直接テープの位置に手を加えれば良い。 しかし、このようなことは望ましくない。

例えば、ビデオテープには、有限の長さがある。120分テープで200分の位置まで進めることはできない。 また、-100分の位置もありえない。しかし、直接positionの値を変えられるようだと、そのようなことが 可能になってしまう。

このようにテープの長さの制限をうまく反映させるには、直接フィールドに手を加えるのではなく、テープの位置を変えるメソッドだけを呼び出すようにすれば良い。 例えば、上記のforwardメソッドを以下のように書く。

    public void forward(int t) {
        if (position+t > 120*60) {
            position = 120*60;
        } else {
            position += t;
        }
    }

こうすれば、テープの終わりよりもさらに早送りしようとしてもテープの最後(120分=120×60秒) で止まるようにできる。ただし、ルールを破ってmainメソッドなどから vt.position = 10000; とされれば台無しである。このような場合に、vt.positionを直接触れないようにすることができる。

class VideoTape {
    private int position=0;
    public void forward(int t) {
〜〜(途中省略)〜〜
}

このようにprivateにすることで、定義されているクラスの中からしか使えないようにすることが可能である。 つまり、この場合は、VideoTapeクラスからでのみpositionを使うことができ、それ以外、例えばmainメソッドが あるVideoExampleクラスからはpositionを使うことができなくなる。しかし、これでは現在のテープの位置を mainメソッドなどから知ることができなくなる。この場合、現在の位置を知るメソッドをVideoTapeクラスに作れば良い。

/* VideoExample2.java - ビデオテープクラスの使用例2
*/
class VideoTape {
    private int position=0;			// 現在の位置
    public int get_position() {			// 現在の位置を得る
        return position;
    }
    public void forward(int t) {		// tだけ先に早送りする
        position += t;
    }
    public void back(int t) {			// tだけ巻き戻す
        position -= t;
    }
}

public class VideoExample2 {
    public static void main(String[] args) {
        VideoTape vt = new VideoTape();
        System.out.println("最初の位置" + vt.get_position() + "秒");
        vt.forward(600); // 600秒進める
        System.out.println("進めた後の位置" + vt.get_position() + "秒");
        vt.back(150); // 150秒戻す
        System.out.println("巻き戻した後の位置" + vt.get_position() + "秒");
   }
}

上記の場合には、VideoTapeクラスにget_positionメソッドを追加して、 mainメソッドからは vt.get_position() としてテープの現在の位置を得ている。 position は、privateとして宣言したために、mainメソッドから vt.position としてテープの位置を得ることはできなくなった。

このようにメンバ(フィールドとメソッド)には、外部から使える(見える)、使えない(見えない) という制御が可能であり、定義の頭に"private"や"public"といったアクセス修飾子をつけることで行われる。

オブジェクト指向では、オブジェクトの中身で外に見せるべきでないところを隠し、 見せるべきところだけを公開する方法を取る。 これをカプセル化といい、見せるべきでないところを隠すことを情報隠蔽という。 これらを記述するのがアクセス修飾子である。

アクセス修飾子には、他に"protected"と修飾子なし(何も書かない)があるが、 これらを使う場合は、継承やパッケージの概念を学ぶ必要があるため、 現状では、"private"と"public"の2種類を使うこととする。

以後、アクセス修飾子を書かない例があるが、次回で継承に触れるまでは、 "public"と同じと考えて差し支えない。

オブジェクトがオブジェクトを持つ

以上で、オブジェクトがフィールド(データ)を持つことを説明した。 今までの説明では、フィールドとしてintなどの原始型を定義した。 それ以外にも、フィールドとして配列やオブジェクト(への参照)を定義することができる。 つまり、オブジェクトが他のオブジェクトを持つことができる。

あるオブジェクトのフィールドがそのオブジェクト自身を参照することも可能である。 これを自己参照という。

例えば、以下のプログラムを見よ。

class Tyre {			// タイヤクラス
    double size;		// 大きさ
    〜〜〜
}
class Engine {			// エンジンクラス
    double displacement;	// 排気量
    int cylinders;		// 気筒数
    〜〜〜
}
class Car {			// 自動車クラス
    Tyre fl, fr, rl, rr;	// タイヤ
    Engine eg;			// エンジン

    public void run() {		// 走行せよ
        〜〜〜
    }
}

Carクラスには、フィールドとしてTyreオブジェクト(への参照)4つとEngineオブジェクト(への参照)1つが定義されている。 つまり、CarオブジェクトはTyreオブジェクト4つとEngineオブジェクト1つを持つことになる。 これで「自動車が4つのタイヤと1つのエンジンを持っている」ということを表現できるのである。

このようにフィールドにオブジェクトを定義することで、「ある物はいくつかの部品が組み合わさったものである」「全体は部分が集まったものである」という実世界であたりまえのことがうまく表現できる。 このような関係を has-a 関係という。

オブジェクト指向の利点

オブジェクト指向が一番活躍できる場は、大規模なソフトウェア開発である。 オブジェクトとして物事をまとめ、適切に外部に見せるもの見せないものを決めると、 巨大なソフトウェアの一部分を作る際に、 知らないといけないことを必要最小限にすることができる。 特にJavaにはクラスライブラリと呼ばれる、 適切に設計されたプログラムが最初から豊富に用意されており、 それらを利用することで大規模なソフトウェアを開発しやすくなっている。

課題1

オブジェクトとクラスの関係を4〜6行程度で説明せよ。

課題2

上記のVideoExample.javaのVideoTapeクラスにあるbackメソッドの定義を以下に示すものに書きかえよ。
backメソッドで直接positionの値を変えるのではなく、backメソッドからforwardメソッドを呼び出して動作するようにする。
ヒント: back(巻き戻し)はforward(早送り)の逆である。

課題3

上記のVideoExample.javaのVideoTapeクラスは60分テープであるとする。 この定義されている、forwardメソッドで現在の位置が60分を超えないように、また、 backメソッドで0分を下回らないように、双方のメソッドを書きかえよ。

課題4

上記 VideoExample2.javaのmainメソッド中で、vt.position と書いた場合どうなるかを 報告し、それがどういうことか説明せよ。

課題5

課題3について、さらにテープの長さをオブジェクトが持つようにせよ。 VideoTapeクラスに length というフィールドを追加し、set_lengthという メソッドを定義せよ。set_lengthはテープの長さ(秒)の引数を持ち、 呼ぶとそのVideoTapeオブジェクトのテープの長さが設定される。
※ もちろんテープの現在の位置がテープの長さを超えないようにせよ。

課題6

以下は平面上の点を表すオブジェクトのクラスである。
class Point {
    double x=0, y=0;
}
このPointクラスに、点の位置を移動するmoveメソッドを定義せよ。 移動量はmoveメソッドに与える引数で指定する。 例えば、以下の場合、p.move(20, -30)で、点pの位置をx方向に20、y方向に-30移動させる。 つまり、moveを呼び出す前の表示が、x=100 y=50、呼び出した後の表示が、x=120 y=30 となれば良い。
public class PointTest {
    public static void main(String[] args) {
        Point p = new Point();
        p.x = 100;
        p.y = 60;
        System.out.println("x=" + p.x + " y=" + p.y);
        p.move(20, -30);
        System.out.println("x=" + p.x + " y=" + p.y);
    }
}

課題7

課題6で、Pointオブジェクトのx座標の値とy座標の値を交換する swap_xyメソッドを定義せよ。 その動作が確認できるようなmainメソッドも記述すること。

課題8

課題6で、Pointクラスのxとyをprivateとし、点のx座標を得るメソッドgetxと、y座標を得るメソッドgety、 点の位置をセットするメソッドset(xとyを引数に指定)を定義せよ。それに伴い課題6のmainメソッドも書きかえよ。

課題9

線分を表すLineクラスを定義せよ。線分とは、2点間を結ぶ直線である。 課題6のPointクラスが参考になるだろう。さらに、線分を移動するmoveメソッドも定義せよ。 それらの動作が確認できるようなmainメソッドも記述すること。
※ Lineクラスは、x1, y1, x2, y2 を持てばよい。

課題10

課題9と同様にLineクラスを定義せよ。ただし、x1, y2, x2, y2ではなく、 p1, p2 という2つのPointオブジェクトを持ち、その2点間を結ぶ直線とせよ。
※ Lineクラスの定義に以下のコンストラクタ(次回触れる)を書く必要がある。
class Line {
    〜〜〜
    Line() {
        p1 = new Point();
        p2 = new Point();
    }
    〜〜〜
}

ソフトウエア入門

ohmi@rsch.tuis.ac.jp