ソフトウエア基礎
ohmi@rsch.tuis.ac.jp

参考:教科書 pp.120-128

オブジェクト指向プログラミングの基本機構(復習)

メソッドのオーバーロード

参考:教科書 p.122

今までは、メソッドを呼び出す場合は、ある1つの決まったメソッドを呼び出していた。 Javaでは、メソッドの名前は同じだが、引数の個数や型が違うメソッドを複数定義することができる。 たとえば、前に挙げたforwardというメソッドの引数は1個でint型であった。これとは別に 引数が2個のforwardメソッドや、引数は1個だがdouble型のforwardメソッドを定義できる。

このように名前が同じメソッドを複数定義できることを、メソッドのオーバーロード(多重定義)という。

以下は以上のメソッドの定義である。

class VideoTape {
    private int position; // テープの位置を示すフィールド(メソッドではない)

    public void forward(int t) { // (A)引数が1個でint型のforwardメソッド
        position += t;
    }
    public void forward(int m, int s) { // (B)引数が2個でどちらもint型のforwardメソッド
        position += m*60+s;
    }
    public void forward(double t) { // (C)引数が1個でdouble型のforwardメソッド
        position += (int)t;
    }
}

メソッドをオーバーロードした場合に、どのメソッドを実行することになるかは、 メソッドを呼び出す場合の実引数の型で決まる。以下の例を見て理解せよ。

public class VideoExample3 {
    public static void main(String[] args) {
        int a = 300;
        double b = 40.3;
        VideoTape vt = new VideoTape();

        vt.forward(a);		// (A)のメソッドが呼ばれる
        vt.forward(1000);	// (A)のメソッドが呼ばれる
        vt.forward(2, 50);	// (B)のメソッドが呼ばれる
        vt.forward(b);		// (C)のメソッドが呼ばれる
        vt.forward(b, a);	// エラー:該当するメソッドがない
    }
}

なお、メソッドの引数の個数や型が同じで、返り値の型が違うメソッドは複数定義できない。

コンストラクタ

参考:教科書 p.120

多くのオブジェクト指向言語には、オブジェクトを作る時に呼ばれるコンストラクタ という特殊なメソッドがある。Javaの場合は、new演算子を使う時にコンストラクタが呼び出される。

今までは、コンストラクタを定義しなかったが、その場合引数のないコンストラクタが暗黙のうちに 用意されており、それが呼ばれる。もちろん引数のないコンストラクタも自分で定義できる。 以下にコンストラクタの例を示す。

class VideoTape {
    private int position=0;

    VideoTape() { // 引数のないコンストラクタ この場合、何もしない
    }
    VideoTape(int t) { // 1つの引数があるコンストラクタ テープの位置をセットする
        position = t;
    }
    VideoTape(int m, int s) { // 2つの引数があるコンストラクタ 分、秒でセット
        position = m*60+s;
    }
    int get_position() {
        return position;
    }
}

このようにコンストラクタはクラス名と同じ名前のメソッドとして書く。 また、以上のようにオーバーロード(多重定義)できる。

以下に上記のコンストラクタを呼び出す例を示す。

public class VideoExample4 {
    public static void main(String[] args) {
        VideoTape vt1 = new VideoTape();
        VideoTape vt2 = new VideoTape(100); // 100秒の位置にセット
        VideoTape vt3 = new VideoTape(10, 30); // 10分30秒の位置にセット
        System.out.println("vt1=" + vt1.get_position());
        System.out.println("vt2=" + vt2.get_position());
        System.out.println("vt3=" + vt3.get_position());
    }
}

なお、コンストラクタには返り値は書かない(書けない)。 コンストラクタの返り値は、生成されたオブジェクト(への参照)しかありえないからである。

また、オブジェクトを作る際に、そのオブジェクトが参照型のフィールドを持つ場合、 そのフィールドは自動的にオブジェクトを作ってはくれない。以下の例を見よ。

class Engine {
    .... // 何か定義がある
}
class Car {
    public Engine e;
    ... // 何か定義がある
}

ここで new Car() として、Carオブジェクトを作るとする。この場合、Carオブジェクトは 生成されるが、Carオブジェクトが持っているEngleオブジェクトeは生成されない (eはnull(参照されていない)を指す)。このため、Carオブジェクトを生成する時に、 Engineオブジェクトも生成したい場合は、以下のようにコンストラクタを定義する必要がある。

class Car {
    public Engine e;
    Car() {
        e = new Engine();
    }
}

継承

参考:教科書 pp.126-128

※ 継承については、Javaのクラスと継承の実際も参考にせよ。

多くのオブジェクト指向言語には継承(inheritance)という仕組みが備わっている。 継承はクラスが持つ機能や性質の共通点をまとめたり、逆に特殊なものを分けたりするのに適した仕組みである。

たとえば、長方形を考えてみる。長方形は四角形の一種であり、四角形としてはほかに 平行四辺形やひし形、台形などがある。また、正方形は辺の長さがすべて等しい 長方形の一種と考えることができる。これを図にすると以下のようになる。

四角形 ---+--- 長方形 ------ 正方形
          |
          +--- ひし型
          |
          +--- 台形

四角形は長方形、ひし型、台形、その他の四角形をまとめた一般的な図形であり、 長方形は四角形の一種、つまり特殊な形と言える。また、正方形は長方形の特殊な形と言える。 つまり以上の図は、左にいくほど一般的で右にいくほど特殊である。 オブジェクト指向では、特殊なクラスは一般的なクラスから継承する。 また、一般的なほうに向かうことを抽象化あるいは汎化(generalize)、特殊なほうに向かうことを 特殊化あるいは特化(specialize)という。

たとえば、四角形クラスにはすべての四角形が持つ機能(メソッド)や性質(フィールド)を定義しておく。 たとえば、四つの頂点を持つ、位置を平行移動できる、といったことを定義できる。 そしてこの四角形を継承することで、長方形クラスを作ることができる。

長方形クラスは四角形クラスで定義された機能や性質をそのまま引き継いでいる (だからこそ継承という)。たとえば、4つの頂点の位置を持っていたり、位置を 平行移動できるという四角形クラスで定義したメソッドやフィールドが使える。

長方形クラスでは長方形特有の機能や性質を定義すればよい。 たとえば、長方形は、対角上の2つの頂点の位置さえ分かれば、形が決まる。そこでその2頂点だけを与えれば、 形が決められるメソッドが定義できる。
※ この場合の長方形は、それぞれ辺がx軸、y軸と水平であると仮定している。

このような継承を使う一番の理由は、新たなクラスを作る場合に、 既に用意されているクラスから継承すれば、 作ろうとしているクラス特有な所だけを書くだけでクラスができるということである。 つまり、書くプログラムを少なくできる(横着できる)。 これを差分プログラミングという。違うところ(差分)だけを書けばよいからである。 継承により、共通の部分をまとめ、差異を別に記述することで、プログラムの見通しを 良くでき、プログラムの修正などがしやすくなるという利点もある。

継承の関係は、「BはAの一種である(B is a A)」といえ、これをis-a関係という。 たとえば、「長方形は四角形の一種である」ということである。 前にオブジェクトが持つフィールドは、has-a関係であることを述べた。 こちらは「BはAを持つ(B has a A)」という関係であり、たとえば、 「自動車はタイヤを持つ」ということである。 オブジェクト指向は、このis-aとhas-aの関係を駆使して、実世界や仮想の オブジェクトをコンピュータ上に実現する方式といえる。

継承の実際

たとえば上記の長方形と正方形のクラスを定義してみよう。
※ この例では、辺はx軸とy軸にそれぞれ平行であるものに限っている。

class Rectangle { // 長方形クラス
    public double x1, y1;		// 頂点の1つ
    public double x2, y2;		// 頂点の1つ (x1,y2)の対角
    public void set(double x, double y, double width, double height) {
        // 左下の位置と幅と高さを指定
        x1 = x; y1 = y; x2 = x+width; y2 = y+height;
    }
}
class Square extends Rectangle { // 正方形クラス
    public void set(double x, double y, double side) { // 左下の位置と辺の長さを指定
        x1 = x; y1 = y; x2 = x+side; y2 = y+side;
    }
}

この場合、Square(正方形)クラスは、Rectangle(長方形)クラスから継承されている。 class Square extends Rectangle の「extends Rectangle」がRectangleクラスから継承されていることを示している。 この場合、Rectangleクラスは、Squareクラスのスーパークラスという。 また逆に、Squareクラスは、Rectangleクラスのサブクラスという。

上では、setメソッドで長方形(Rectangle)の左下の位置(座標の値が小さいほう)と幅と高さで長方形の 形をセットできる。正方形(Square)では幅と高さは等しいので、3つの引数(x,y,辺の長さ)で形を セットするsetメソッドを作ることができる。Squareクラスのsetメソッドで使われている x1,y1,x2,y2はRectangleクラスで定義されたフィールドである。継承されているので スーパークラスであるRectangleクラスで定義されたフィールドが使えるのである。

メンバのオーバーライド

継承する時に、メンバ(フィールドとメソッド)を再定義することができる。 これをメンバのオーバーライドという (オーバーロードと混同しないように!)。

たとえば、以下のように鳥をあらわすBirdクラスとニワトリを表すChickenクラス、鷲を表す Eagleクラスを定義する。

class Bird { // 鳥のクラス(飛べる)
    public void fly() {
        System.out.println("パタパタ(飛んでます)");
    }
}
class Chicken extends Bird { // ニワトリのクラス(飛べない)
    public void fly() {
        System.out.println("飛べません");
    }
}
class Eagle extends Bird { // 鷹のクラス(今のところ特に中身なし)
}
public class BirdExample {
    public static void main(String[] args) {
        Eagle e = new Eagle();
        Chicken c = new Chicken();
        e.fly();
        c.fly();
    }
}

まず、Eagleオブジェクトではflyメソッドを呼び出すと、スーパークラスであるBirdクラスで定義されているflyメソッドが実行される。 これに対してChickenクラスではオーバーライド(再定義)されている。 このため、Chickenオブジェクトからflyメソッドを呼び出すと、Chickenクラスで定義されているflyメソッドが呼び出されるのである。 この例では、鳥は空を飛べるという鳥が備えている一般的な機能をBirdクラスで定義しておき、 Eagleなど通常の鳥はその機能を使うようにする。そして、例外的なニワトリに限っては、 flyメソッドを再定義して、空を飛べないということを実現しているのである。

なお、このようにメンバを再定義すると、スーパークラスで定義されたメンバが見えなくなってしまう。 こういう場合には、以下のようにsuperを使う。

class Swallow extends Bird { // ツバメのクラス
    public void fly() {
        super.fly();
        System.out.println("すごい速さです。");
    }
}

この場合、Swallowオブジェクトに対してflyメソッドが呼び出されると、そのスーパークラス であるBirdクラスのflyメソッドが呼び出される(super.fly())。そして、その後に、すごい速さです と表示しているのである。

クラスメンバとインスタンスメンバ

参考:教科書 pp.112,118-119

今までオブジェクト指向の説明で使ってきたメンバ(フィールドとメソッド)は、 オブジェクトが持っているものであった。これをインスタンスメンバ (インスタンスフィールドとインスタンスメソッド)と呼ぶ。

これに対して、クラスが持つメンバを定義することができ、クラスメンバ (クラスフィールドとクラスメソッド)と呼ぶ。

class Dog {
    public String name;
    public static int number=0;
    Dog() {
        number++;
    }
    Dog(String n) {
        number++;
        name = n;
    }
}
public class DogExample {
    public static void main(String[] args) {
        System.out.println("犬の数=" + Dog.number);
        Dog pochi = new Dog();
        System.out.println("犬の数=" + Dog.number);
        Dog shiro = new Dog("シロ");
        System.out.println("犬の数=" + Dog.number);
        System.out.println("犬の数=" + shiro.number);
    }
}

クラスメンバを定義するには、前にstaticを付ければよい。 上の例では、numberというクラスフィールドを定義している。 クラスメンバは1個のクラスに1個存在するものである。 つまり、そのクラスのインスタンス(オブジェクト)がいくつあっても(0個でも、1個でも、2個でも、3個でも…) 1つだけ存在するのである。上の場合は、Dogインスタンスがいくつあっても、 numberは1つである。Dogインスタンスごとにnumberが存在するわけではない。

上の例では、numberにはDogインスタンスがいくつ存在するかが入っている。 コンストラクタで1つ加算しているので、Dogインスタンスが作られるごとに1つ増えるからである。 クラスメンバを使うには、「クラス名.」 をつける (上記ではDog.number)。あるいは既にそのクラスのインスタンスが存在する場合には、 「インスタンス名.」をつけても使える(上記では shiro.number)。

今までに、飽きるほど出てきたmainメソッドは、クラスメンバである。 このためmainメソッドにはstaticがついているのである。


ソフトウエア基礎

ohmi@rsch.tuis.ac.jp