Print Friendly, PDF & Email

この記事では、弊社の新人エンジニア研修の参考にJava8を解説します。

前回はインスタンスの活用について解説しました。

今回は継承(拡張)について解説します。

継承はとても巧妙な仕組みです。

ただし、少し直感的に分かりにくいところがあります。

手続き指向のプログラムではソースコードをざっと読めばどのような処理をしているかが一目瞭然なのですが、

オブジェクト指向では一見、自分のクラスが持っていないはずのメソッドを使うことができたりするのです。

そのあたりのところ今回はお話ししたいと思います。

 

1.すべてのクラスのスーパークラスObject

今までにこの研修ではいくつかのクラスが出てきました。

前回のインスタンスの活用で作成したオリジナルクラスNewEngineerもありました。

標準APIに用意されていた

System(標準API

String(標準API

Math(標準API

などもそうですね。

 

Javaの標準APIを参照すると先頭部分に以下のように記述されています。

以下は、Systemクラスの例です。

クラスSystem

この記述が言わんとしているのはSystemクラスにはObjectクラスというスーパークラスがいますよ、ということです。

そして、スーパークラスの持っているメンバ(フィールドやメソッド)はサブクラスが使うことができるのです。

しかし、その逆はありません。

 

人間も先祖から遺伝情報をもらっていますね。

あなたの特徴やスキルにもご自身で獲得したものと、先祖から伝わっているものがあるかと思います。

先祖から伝わっているものを使えるのが継承の仕組みです。

 

親(先祖)の持っているものを子供は使える。

子(子孫)の持っているものを親は使えない。

なんだか身につまされませんか?

※なお、本書では直接継承関係のあるクラスを親クラス、子クラスと表現することがありますのでご了承下さい。

 

Objectクラスはその名の通り"モノ”を表すクラスです。

ObjectクラスはJavaのすべてのクラスのスーパークラスなのです。

Objectクラスには例えばequalsメソッドが定義されていますので標準APIで確認して下さい。

よって、すべてのクラスでequalsメソッドが使えるのです。

ただし、オーバーライドという仕組みにより同じ名前のメソッドであってもスーパークラスの処理内容とサブクラスの処理の内容が違う場合があります。

この点も大切なポイントなので後述しますね。

 

2.継承(拡張)とは

継承とは、すでにあるクラスのフィールドやメソッドを新しいクラスが引き継ぐことをいいます

英語では“inheritance”で「継承や相続」といった意味があります。

継承を使うことで機能の拡張ができることから拡張と呼ばれることもあります。

拡張は英語で“extends”です。

皆さんは髪の毛のエクステを知っていますか?

それがextendsのイメージです。

 

この継承の仕組みは皆さんも利用することが可能です。

ただし、何でもかんでもクラス間に継承関係をつくってフィールドやメソッドを取り込めばよいかというとそれは違います。

そこには論理的必然性が必要になります。

そうでないと体系がこんがらがってしまいます。

 

継承関係を作る際の基準としては、

サブクラスは一種のスーパークラスである

と言えるかどうかが重要です。

例えば、

車は一種の「乗り物」です。

飛行機は一種の「乗り物」です。

船は一種の「乗り物」です。

よって「乗り物」は車、飛行機、船のスーパークラスです、という具合に。

 

サブクラスは一種のスーパークラスであるというのを簡潔に「is-a」関係といいますので覚えておいてください。

A car is a Vehicle.

An airplane is a Vehicle.

A ship is a Vehicle.

というわけですね。

 

スーパークラスには共通のメソッドを定義して、サブクラスにそれを拡張できるようにします。

乗り物クラスに共通のメソッドの例としては、例えば、

  • 進む
  • 止まる
  • 人を乗せる

等が考えられるでしょう。

皆さんも身の回りで継承関係が作れそうなオブジェクトの例を考えてみてください。

 

親子関係を宣言するにはextendsというキーワードを使用します。

extendsという英語には拡張するという意味があるのでしたね。

以下の3つのサンプルプログラムを見てください。

package chap10;

public class Employee {

    int id;
    String name;
}
package chap10;

public class Engineer extends Employee {

    String skill;
}
package chap10;

public class Example01 {

    public static void main(String[] args) {
        Engineer se = new Engineer();
        se.id = 1;
        se.name = "今井";
        se.skill = "Java";
        System.out.println("私は" + se.name + "。私のスキルは "+ se.skill + " です。");
    }
}

<結果>

私は今井。私のスキルは Java です。

Employeeクラスのフィールドはidとname。

Engineerクラスで宣言したフィールドはskill。

EngineerクラスはEmployeeクラスをextendsしているため3つのフィールドを持っているのです。

しかし、ぱっと見はクラスの持っているフィールドやメソッドが分かりにくいですね。

そこで登場するのが以下のようなクラス図というものです。

継承のクラス図

継承のクラス図

クラス図ではクラスを四角形で表します。

四角形を3つに区切って、一番上がクラス名、真ん中がフィールド、一番下がメソッドです。

今回の2つのクラスではメソッドがありませんので空欄になっています。

そして、2つのクラスの関連である継承関係を白抜きの実線矢印で表現します。

クラス図の矢印の向きは重要です。

なぜなら、依存関係を表現するからです。

上記の例で言えば、親クラスが無いと子クラスが成立しません。

一方、子クラスがなくても親クラスは成立します。

なお、Objectクラスのみはextendsキーワードなしに継承されます。

すべてのクラスが自動的にObjectクラスのサブクラスや孫クラスになることが保証されていますのでわざわざクラス図で表現しません。

さらに詳細説明は研修の目的に応じて講師からいたします。

 

このクラス図により、Engineerクラスには3つのフィールドがあることが明らかになります。

このあともいろいろなパターンが出てきますのでその都度ご紹介しますね。

 

継承のメリットは以下の2点です。

1.サブクラスはスーパークラスの差分だけをプログラミングすればよい。
※これを差分プログラミングといいますが現在ではこれをメリットと捉えない考え方も有力です。

2.サブクラスをスーパークラスの変数で扱える
※これを応用してポリモーフィズムを実現します。これは明らかにメリットです。

1については上記の例で簡単に説明できましたので、2について説明します。

 

3.継承関係にあるサブクラスをスーパークラスの参照で扱える

Javaでは、すべてのクラスのスーパークラスにはObjectクラスがあります。

つまり、すべてのクラスにはObjectクラスのメソッドがあるということになります。

書いてないけれど使える。

それが継承(拡張)の仕組みでした。

では、Objectクラスを覗いてみることにしましょう。

そろそろ慣れた頃かと思いますが、講師と一緒にJavaのソースコード・ツアーに出かけましょう。

そして以下のようなequalsメソッドが定義されているところを探してください。

    public boolean equals(Object obj) {
        return (this == obj);
    }

これまでの知識でこのメソッドを読み解いてみましょう。

戻り値は true or false のいずれかの boolean です。

仮引数は Object obj となっています。

これはつまり、Object なら何でもという意味です。

サブクラスをスーパークラスの変数で扱えるからできることですね。

 

そして、

this == obj

thisというのはこのクラスがインスタンス化したときの自分自身(の参照)を意味しています。

まだ、インスタンス化されていない状態でコードが書かれているため参照がなくthis(これ)としか表現しようがないわけです。

これは参照が同一かどうかを見ているわけですね。

Objectの同一性は参照が同じであるということで判断しているということです。

なお、Stringクラスでは独自のequalsを定義してそちらを優先させていたというのは7.文字と文字列の扱いでご紹介しました。

 

例えば以下の3つのサンプルコードを見てください。

package chap10;

public class Vehicle {

    void run() {
        System.out.println("I'm running.");
    }
}
package chap10;

public class Car extends Vehicle {
}
public class Example02 {

    public static void main(String[] args) {
        Vehicle v1 = new Car();
        v1.run();
    }
}

<結果>

I'm running.

vehicle(乗り物)クラスとCarクラスがあり、親子関係があります。

車は一種の乗り物と言えますか?

言えますね。

では、車に向かって「乗り物よ!動け!」ということはできるでしょうか?

論理的に間違っているところはありませんね。

そのようなことをしているのが上記のコードのメインメソッドなのです。

つまり、

サブクラスをスーパークラスの参照で扱える

ということになります。

念のためクラス図も書いておきます。

サブクラスをスーパークラスの参照で扱える

サブクラスをスーパークラスの参照で扱える

 

ただし、次のようなプログラムは実行できませんので注意しましょう。

package chap10;

public class Airplane extends Vehicle {

    void fly() {
        System.out.println("I'm flying.");
    }
}
package chap10;

public class Example03 {

    public static void main(String[] args) {
        Vehicle v = new Airplane();
        v.fly();
    }
}

このプログラムは、あたかも「乗り物よ飛べ!」といっているようなものです。

「おれ中身は飛行機だけれど、今は乗り物って呼ばれてるから飛べないんだよな」

という声が聞こえそうですね。

 

冗談はさておき、スーパークラス型の変数でサブクラスを扱う時、サブクラス独自のメソッドをそのままでは呼び出せません。

しかし、実態は飛行機なのです。

ですから、いったんは乗り物型のインスタンス変数に紐づいた飛行機も、飛ばすことは可能です。

プリミティブ型でもみたキャストを使います。

以下のプログラムを見てください。

package chap10;

public class Example04 {

    public static void main(String[] args) {
        Vehicle v = new Airplane();
        Airplane a = (Airplane) v;
        a.fly();
    }
}

※より厳密にはインスタンスをキャストしてるのではなく、参照をキャストしている。

<結果>

I'm flying.

ただ、これだけでは、サブクラスをスーパークラスの参照で扱えることの嬉しさがあまり伝わらないかもしれません。

もう少し説明を加えますね。

 

4.メソッドのオーバーライド(Stringクラスの場合)

オーバーライドとは、サブクラスでスーパークラスのメソッドを優先させることでポリモーフィズムを可能にします

ポリモーフィズムとは、同じメソッドに対して違った処理をさせることです。

順を追って説明します。

まず、サブクラスをスーパークラスの参照で扱うメリットをStringクラスのequalsメソッドを使って説明しましょう。

 

その前にinstanceof演算子について簡単にご紹介しておきます。

この演算子はインスタンスのクラスを調べるときに有効です。

使用例です。

String s = "Hello";
System.out.println(s instanceof String);

tureと表示されます。

 

その上で、Stringクラスのequalsメソッドを読み解いていきましょう。

ソースコードを読む前に要件を整理しておきます。

前提:"Hello".equels("Hello")がtrueになることを検証する

文字列"Hello"と比較対象の文字列 "Hello"が同じであるということは以下の2つのパターンがあります。

  1. どちらの参照の中身も同じ場合(同一のインスタンスを指している場合)
  2. 同一のインスタンスではないがすべての文字が同じである場合

 

文字列 が同じであるということの2つのパターン

文字列 が同じであるということの2つのパターン

 

そして2.のケースでは、以下の3つの基準をクリアすれば同じ文字列であるということが言えます。

①比較対象も文字列である
②比較対象文字と文字数が同じである(Helloの場合は5)
③先頭文字から一文字づつ比較していって最後まで同じ文字である

※①②がfalseであれば早急に同じ文字列でないとして時間短縮ができる

それをJavaのコードで表現しているのが以下のStringクラスのequalsメソッドです。

なお、valueは、インスタンスの文字列が格納されているcharの配列(のインスタンス変数)です。

合計4つのif文があり少々複雑ですが、上記の説明と対応させてコメントを入れました。

    public boolean equals(Object anObject) {
        if (this == anObject) { //1
            return true;
        }
        if (anObject instanceof String) { // 2-①
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {// 2-②
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i]) // 2-③
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

(n- -!= 0)という表現が見慣れないかと思います。

これは、以下と同じなのですが、上記の書き方で1行短くしているのです。

                while (n != 0) {
                    n--;
                    if (v1[i] != v2[i]) // 2-③
                        return false;
                    i++;
                }

Stringクラスのequalsメソッドは2つの変数が同じインスタンスを参照していなくても、同じ値(文字列)を持っていれば同じ、という意味だということを私達は学びました。

それは、このようなコードで実現されているわけです。

 

中古車でしたら全く同じ車体でないと同じ車と判断してはいけませんね。

しかし、新車でしたら同じ車と判断しても良い車がたくさんありますね。

では、Javaプログラムでは何をもって同じインスタンスと判断しましょうか?

それは、プログラムの目的によって変わりますね。

したがってクラスごとにequalメソッドの内容は書き換えることができるのです。

 

Stringクラスのequalメソッドの例で見たように、サブクラスでスーパークラスのメソッドを変更することができます。

これをオーバーライドと言います。

英語ではover「上に」ride「載せる」ということで優先させるという意味です。

オーバーロード(多重定義:引数の数や型が異なり名前が同じメソッドを宣言すること)と名前が似ているので注意してください。

また、スーパークラスのオーバーライドされたメソッドが上書き(overwrite)されてしまったわけではないので気をつけてください。

したがってスーパークラスのメソッドをサブクラスから呼び出すことも可能です。

その際は、superというキーワードを使いますがここではその詳細には触れません。

 

5.メソッドのオーバーライド(自作クラスの場合)

ここでオーバーライドが成立するための3つの条件を挙げます。

  1. メソッドのシグネチャーが、スーパークラスと同じである
  2. メソッドのアクセス制御がスーパークラスと同じか緩い
  3. 戻り値の型が、スーパークラスのメソッドと同じかサブクラス型である

ひとまず3、はこの後学ぶポリモーフィズムと関連することなので、今はおいておきます。

また、2、でアクセス制御を不用意に緩めるとセキュリティ問題が発生しうるのですが、本研修の範囲を超えますので割愛します。

興味のある方はご自身で上記リンクをたどってください。

 

「1.メソッドのシグネチャが、スーパークラスと同じである」については失敗することがあります。

スペルミスによりオーバーライドのつもりがそうなっていないことが起こり得るのですね。

そのために

@Override

というアノテーション(annotation)があります。

アノテーションとは注釈という意味でした。

アノテーションを挿入すると「オーバーライドのつもりがスペルミスによりオーバーライドになっていなかった」という間違いを防ぐ効果があります。

 

VehicleクラスのサブクラスとしてRacingCarクラスを作成し、runメソッドをオーバーライドしてみます。

package chap10;

public class RacingCar extends Vehicle {

    @Override
    void run() {
        System.out.println("I'm driving fast.");
    }
}
package chap10;

public class Example05 {

    public static void main(String[] args) {
        Vehicle v = new RacingCar();
        v.run();
    }
}

<結果>

I'm driving fast.

オーバーライドしたサブクラスのメソッドが実行されました。

では、このことがどのように役立つのでしょうか?

それは、乗り物(Vehicle)の変数ですべての種類の乗り物(車、飛行機、船)が扱えるということです。

次のようなプログラムが書けるということになります。

package chap10;

class PatrolCar extends Vehicle {

    void run() {
        System.out.println("I'm driving.");
    }
}

class Locket extends Vehicle {

    void run() {
        System.out.println("I'm flying.");
    }
}

class Ship extends Vehicle {

    void run() {
        System.out.println("I'm sailing.");
    }
}

public class Example06 {

    public static void main(String[] args) {
        Vehicle[] vs = {new PatrolCar(), new Locket(), new Ship(), new PatrolCar(), new Locket()};
        for (Vehicle v : vs) {
            v.run();
        }
    }
}

※本来は一つのクラスに一つのJavaファイルを用意すべきですが、読みにくくなるため上記のサンプルコードは、簡易的に一つのファイルに複数のクラスを書いています。その際には、publicで宣言できるのは一つのクラスだけになります。

<結果>

I'm driving.
I'm flying.
I'm sailing.
I'm driving.
I'm flying.

<イメージ図>

ポリモーフィズムのイメージ

ポリモーフィズムのイメージ

 

今回のように「v.run()」という同じ命令にもかかわらず、インスタンスごとに違った処理を実行しました。

このようなオブジェクト指向の仕組みをポリモーフィズム(多態性)といいます。

 

6.オリジナルなequalsメソッドの実装

次に、オリジナルなequalsメソッドの実装を試してみましょう。

自転車クラスを考えます。

この自転車クラスは防犯登録番号(registryNumber)が同じなら、同一であると判断するとします。

先ほどのStringクラスのequalsメソッドを参考にしてみました。

他の乗り物クラスでもequalsメソッドを持つことを想定し、汎用性を考えて仮引数を(Vehicle aVehicle)としてみました。

ちなみにequalsメソッドもIDEの機能を使えば簡単に挿入できるうえスペルミスを減らすことになります。

package chap10;

class Bicycle extends Vehicle {

    int registryNumber;

    public Bicycle(int registryNumber) {
        this.registryNumber = registryNumber;
    }

    public boolean equals(Vehicle aVehicle) {
        if (this == aVehicle) {
            return true;
        }
        if (aVehicle instanceof Bicycle) {
            Bicycle anotherBicycle = (Bicycle) aVehicle;
            return this.registryNumber == anotherBicycle.registryNumber;
        }
        return false;
    }

    @Override
    public int hashCode() {
        return this.registryNumber;
    }
}
package chap10;

public class Example07 {

    public static void main(String[] args) {

        Bicycle bi1 = new Bicycle(110);
        Bicycle bi2 = new Bicycle(110);
        Bicycle bi3 = new Bicycle(119);

        System.out.println(bi1.equals(bi2));
        System.out.println(bi2.equals(bi3));
    }
}

<結果>

true
false

<イメージ図>

equalsメソッドの独自実装

equalsメソッドの独自実装

 

ここで一見無関係に見えるhashCodeをオーバーライドしていることを不思議に思う人もいるかもしれません。

実は、equalsメソッドをオーバーライドしたときにはhashCodeメソッドもオーバーライドしないと、後に学ぶコレクションフレームワークのHashMap/HashSetなどのハッシュ表の要素としたときに深刻なバグを生む可能性があるのです。

ちなみにこのハッシュ値はごちゃまぜにするという意味でハッシュドビーフ(hashed beef)などと同じ語源でしたね。

ここでのハッシュ値は、いわゆる情報セキュリティなどで出てくるハッシュ値のように、一方向性や不可逆性といったことまでは要求されません。

ここでは単純に防犯登録番号を返す仕様としています。

 

実は数多くのインスタンスから目的のインスタンスをequalsメソッドで探すときには2段階の探索が行われるのです。

つまり、最初はhashCodeの一致を見ます。

しかし、前述の通りhashCodeが偶然同じで別のインスタンスということはありえます。

そこで絞り込んだ対象に対してequalsメソッドで等しいかどうかを見るのです。

なお、この例「equals(Vehicle aVehicle)」のようにメソッドの仮引数をスーパークラス型にすることで、サブクラス型の実引数を受け取れるようにしておくことをポリモーフィズムを使ったメソッドの引数設計と呼ぶことがあります。

大切な考え方ですので押さえてください。

 

たとえ話をしますと、「お母さんアレ取って」と言って理解しあえる夫婦のようなものです。

アレの中身は状況により変わってもいいわけですね。

この例のように私達は日常生活で抽象化の恩恵を受けていますが、ポリモーフィズムは抽象化だとお考えください。

ちなみに、このあと学ぶインターフェースを使うと更に抽象化の恩恵が受けられます。

 

7.ポリモーフィズムで保守性が高まる

ここで、ポリモーフィズムを使わずにコーディングしたとします。

何が不都合なのでしょうか?

ポリモーフィズムを使わない場合は、クラスの種類に応じて呼び出すインスタンスメソッドが変わります。

そのためif文などで処理を分岐させる必要があります。

クラスの種類に応じてというところは、instanceof演算子を使えばいいです。

こんな風になりますね。

<ポリモーフィズムを使わない場合>

package chap10;

class CampingCar {

    void run() {
        System.out.println("I'm driving.");
    }
}

class PropellerPlane {

    void run() {
        System.out.println("I'm flying.");
    }
}

public class Example08 {

    public static void main(String[] args) {
        Object[] vs = {new CampingCar(), new PropellerPlane(), new String()};
        for (Object v : vs) {
            if (v instanceof CampingCar) {
                CampingCar c = (CampingCar) v;
                c.run();
            } else if (v instanceof PropellerPlane) {
                PropellerPlane p = (PropellerPlane) v;
                p.run();
            } else {
                System.out.println("想定していないクラスです");
            }
        }
    }
}

<結果>

I'm driving.
I'm flying.
想定していないクラスです

<イメージ図>

ポリモーフィズムを使わない場合のイメージ

ポリモーフィズムを使わない場合のイメージ


しかし、ここで新たにSurfingクラスを作るとします。

class Surfing{

    void run() {
        System.out.println("I'm surfing.");
    }
}

すると、“想定していないクラス”なのでmainメソッドのif文を追加する必要が出てきてしまうのです。

しかし、以下のポリモーフィズムを使ったコードは乗り物の種類が増えてもmainメソッドの中身を(ほとんど)書き換えなくて済みます。

 

<ポリモーフィズムを使った場合> ※ PatrolCar, Locket, Shipの3クラスにもextends Vehicleを追記したが再掲は省略。

package chap10;

class Surfing  extends Vehicle{

    @Override
    void run() {
        System.out.println("I'm surfing.");
    }
}

public class Example09 {

    public static void main(String[] args) {
        Vehicle[] vs = {new PatrolCar(), new Locket(), new Ship(),new Surfing()};
        for (Vehicle v : vs) {
            v.run();
        }
    }
}

<結果>

I'm driving.
I'm flying.
I'm sailing.
I'm surfing.

<イメージ図>

ポリモーフィズムを使うと保守に強くなる

ポリモーフィズムを使うと保守に強くなる


今動いているプログラムをいじるというのはとても勇気のいることです。

もっと複雑なコードで、しかも、mainメソッドの処理が現役で使われている処理だとしましょう。

そうするとなかなか変更するのは大変です。

ポリモーフィズムの活用の一面として、新しくクラスを追加しても変更は最低限で済むということがあります。

もし、あなたのプログラムがinstanceof演算子を使ってクラスの種類を判定していて、今後も判定すべきクラスが増えそうであれば、そこに不吉なにおいを嗅ぎ取ってくださいね。

 

8.toStringメソッドで出力内容をコントロールする

toStringメソッドをオーバーライドすることでSystem.out.println()メソッドに渡したオブジェクトの表示内容を分かりやすくできます。

ここで初心に戻って以下のプログラムを実行してみてください。

package chap10;

public class Example10 {
    public static void main(String[] args) {
        Bicycle bi = new Bicycle(1234);
        System.out.println(bi);
    }
}

<結果>

chap10.Bicycle@a

@の前がクラス名、後ろがハッシュコードと呼ばれる16進数でした。

このとき、例えば、printlnメソッドを使い防犯登録番号を表示させることはできるのでしょうか?

上記の表示を以下のように変えたいとします。

私の防犯登録番号は、1234

toStrintoStringメソッドをオーバーライドすればできます。

 

では、IDEでprintlnメソッドの定義をさかのぼりましょう。

    public void println(Object x) {
        String s = String.valueOf(x);
        synchronized (this) {
            print(s);
            newLine();
        }
    }

クラスは、PrintStreamクラスです。

2行目でStringクラスのvalueOfというクラスメソッドを使っています。これは、xが参照する様々なオブジェクトを、String型に変換するメソッドです。

※ちなみに、printメソッドとnewLineメソッドも見えていますね。synchronized (this) という記述は排他制御のためです。このsynchronizedブロックで囲まれた2行は、1インスタンスごとに必ず1つのスレッドからしか実行されないため、同時アクセスによって矛盾した表示がされないことが保証されています。

では、IDEでvalueOfメソッドの定義をさかのぼりましょう。飛び先のクラスはStringです。

    public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }

obj == nullでnull判定をして、nullであれば文字列"null"を、そうでなければ、toStringメソッドを呼んでいます。

では、さらにIDEでtoStringメソッドの定義をさかのぼりましょう。

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

クラスはObjectクラスです。

ここでやっとご本尊(?)らしき記述と対面することができました。

では、このtoStringメソッドをオーバーライドしたらどうなるでしょうか?

オリジナルな文字表現で標準出力できそうですね。

やってみましょう!

package chap10;

public class Bicycle2 extends Vehicle {

    int registryNumber;

    public Bicycle2(int registryNumber) {
        this.registryNumber = registryNumber;
    }

    @Override
    public String toString() {
        return "私の防犯登録番号は、" + this.registryNumber;
    }
}
package chap10;

public class Example11 {

    public static void main(String[] args) {
        Bicycle2 bi = new Bicycle2(10);
        System.out.println(bi);
    }
}

<結果>

私の防犯登録番号は、10

なお、オリジナルなクラスを作成したらtoStringメソッドをオーバーライドするというのはJavaプログラマの半ば常識となっています。

そうすることでデバッグ時に表示が分かりやすくなるからです。

また、そのようなtoStringメソッドですから、IDEで簡単に挿入できるようになっているはずですので講師から説明いたします。

ただし、toStringメソッドで取得したデータを使いまわすようなプログラムは慎むべきです。

フィールドの値を取得する用途には、次回お話しするアクセサメソッドを使います。

 

ちなみに、ここまで来たらObjectクラスのhashCodeメソッドの中身も気になっている人もいるかもしれませんね。

試しにJavaのソースコードで定義を確認すると、

  public native int hashCode();

となっています。

中身のないメソッドです。

そしてnative修飾子がついています。

英語でも母国語を話せる人をnativeといいますね。

コンピュータの母国語はマシン語です。

C言語で書かれてマシン語にコンパイルされたコードです。

Javaではこれ以上、遡ることはできません。

 

9.スーパークラスの土台の上にサブクラスは作られる

もう少し継承の仕組みのお話にお付き合いください。

以下のサンプルコードを実行したらどうなるでしょうか?

package chap10;

class Parent {
  public Parent() {
    System.out.println("Hello from SuperClass");
  }
}
 
class Child extends Parent {
}
 
public class Example12 {
  public static void main(String[] args){
    Child c = new Child();
  }
}

<結果>

Hello from SuperClass

 

これは、コンストラクタが継承されたわけではありません。

そうではなく、見えないだけで本当は以下のようなコードなのです。

見えていないコードを書き入れるとこうなっています。

package chap10;

class Parent {
  public Parent() {
    System.out.println("Hello from SuperClass");
  }
}
 
class Child extends Parent {
    Child(){
        super();
    }
}
 
public class Example12 {
  public static void main(String[] args){
    Child c = new Child();
  }
}

<結果は同じ>

Subクラスのデフォルトコンストラクタの先頭行でスーパークラスのデフォルトコンストラクタを呼んでいたのです。

したがって3代に渡る継承関係を作るとこうなります。

package chap10;

class First {

    public First() {
        System.out.println("Hello from First");
    }
}

class Second extends First {

    Second() {
        super();
        System.out.println("Hello from Second");
    }
}

class Third extends Second {

    Third() {
        super();
        System.out.println("Hello from Third");
    }
}

public class Example13 {

    public static void main(String[] args) {
        Third t = new Third();
    }
}

<結果>

Hello from First
Hello from Second
Hello from Third

この順番をよくご記憶ください。

スーパークラスから順番にサブクラスがコンストラクトされていってます。

コンストラクターチェーンと呼ばれることもあります。

スーパークラスの土台の上にサブクラスは作られるのです。(一番下の土台はObjectクラス)

この点が理解できれば、スーパークラスの参照でサブクラスが扱えるということも理解しやすいのではないでしょうか?

スーパークラスの土台の上にサブクラスは作られる

スーパークラスの土台の上にサブクラスは作られる

 

10.継承よりも委譲を選ぶ

先にも述べたとおり、あるクラスのある機能を取り込みたいばかりに、継承を使うというのは悪い設計です。

継承を使うとスーパークラスの変更がサブクラスに及ぶため、クラス間に依存関係が生じてしまうのです。

なお、依存関係が強いことを密結合、依存関係が弱いことを疎結合と情報処理の世界では呼ぶことがあります。

 

継承を使わなくても他のクラスの機能を取り込むことができます。

その方法を「依存」や「委譲」といいます。

次のサンプルプログラムを見てください。

package chap10;

import java.util.Scanner;

public class Example14_1 {

    public static void main(String[] args) {
        DependencePc pc1 = new DependencePc();
        pc1.input();
    }
}

class DependencePc {

    void input() {
        Scanner scanner = new Scanner(System.in);
        System.out.print("入力してください > ");
        String input_text = scanner.nextLine();
        System.out.println(input_text + "が入力されました");
    }
}

<結果>※キーボードからHelloと入力した場合

入力してください > Hello
Helloが入力されました

ご覧いただきたいのはDependencePc クラスのローカル変数にScannerクラスのインスタンスを割り当てている部分です。

これが依存です。

useの関係ともいいます。

クラス図で書くと以下のようになります。

野球で例えるならピンチヒッターとして、一時的に起用された選手のような役割です。

このinputメソッドの中だけで活躍します。

クラス図ではこうなります。

依存

use関係

 

もしも、ScannerクラスがPCクラスの部品と考えられて、複数のメソッドにまたがって使いたいクラスであれば、フィールドにインスタンスメンバとして宣言するほうが良いでしょう。

野球の例えでいえばスタメンのようなものです。

以下のサンプルプログラムをご覧ください。

package chap10;

import java.util.Scanner;

public class Example14_2 {

    public static void main(String[] args) {
        DelegatePc pc1 = new DelegatePc();
        pc1.input();
    }
}

class DelegatePc {

    Scanner scanner = new Scanner(System.in);

    void input() {
        System.out.print("入力してください > ");
        String input_text = scanner.nextLine();
        System.out.println(input_text + "が入力されました");
    }
}

<結果は上記と同じ>

ご注目いただきたいのは、フィールドにScannerクラスのインスタンス変数を宣言している部分です。

そうしておいて、inputメソッドでScannerクラスのnextLineメソッドを呼び出しています。

このようなクラスの関係を”has-a”の関係といいます。

has-a関係

has-a関係

クラス図では上図のようになります。

白抜きの◇があるクラスが矢印のクラスを持っているという意味です。

継承のところで見たようにクラス図の矢印はそれがどのような形のものであっても“依存”関係を表現するのでした。
(ここでの依存はクラス図の依存とは別の一般的な意味で使用しています。)

PcクラスはScannerクラスを持っているということは、PcクラスはScannerクラスがないと困る、すなわち依存しているというわけですね。

両方が自作のクラスであれば、矢印の先にあるクラスから作り始めたほうが簡単です。

 

このときnextLineメソッドを使いたいがためだけにPcクラスがScannerクラスを継承するのは間違いです。
※そもそもScannerクラスはfinalで宣言されているため継承できませんが

樹形図になっているクラスの体系をぐちゃぐちゃにしてしまいます。

"is-a"関係には後ほどお話する多重継承の問題や、サブクラスにスーパークラスの不要なフィールドやメソッドまでをも組み込んでしまうという問題があります。

”has-a”関係にはそのような問題はありません。

継承か委譲か迷ったら委譲を使う方が良いのです

 

11.クラスの責務を考えてクラス設計をする

継承よりも移譲を選ぶという話題が出たついでにクラスの責務という考え方をご紹介します。

クラスの責務とは、このデータはどのオブジェクトが知っていて、どのオブジェクトが処理をすべきか、ということです。

具体例で説明します。

BicycleクラスとExample07のサンプルプログラムでは、registryNumberを知っているのも、equalsで同じかどうかを比較するのも、Bicycleクラスの責務と考えたということになります。

しかし、強引にequalsメソッドをExample07に持たせたとしたら以下のようなコードになります。

package chap10;

public class Example15 {

    public static void main(String[] args) {

        Bicycle bi1 = new Bicycle(110);
        Bicycle bi2 = new Bicycle(110);
        Bicycle bi3 = new Bicycle(119);

        System.out.println(bi1.equals(bi2));
        System.out.println(bi2.equals(bi3));
    }
    
    static boolean equals(Bicycle bi1, Bicycle bi2){     
        if (bi1 == bi2) {
            return true;
        }
        if (bi2 == null) {
            return false;
        }
        if (bi1.getClass() != bi2.getClass()) {
            return false;
        }
        if (bi1.registryNumber != bi2.registryNumber) {
            return false;
        }
        return true;
    }
}

<結果>

true
false

このとき、もしも、Bicycleクラスを利用しているクラスが、Example1~100まで100個あったとしたらどうでしょうか?

その全てでこのequalsメソッドを書かなければいけません。

また、このequalsメソッドに変更が生じたとしたら、100箇所で修正しなければいけないことになります。

しかし、equalsメソッドを正しくBicycleクラスの責務と考えられたなら1箇所の修正で済むわけです。

 

12.ClassCastException

ここでは、クラスのキャストについて見ていきます。

「サブクラスは一種のスーパークラス」でした。

しかし、逆に、「スーパークラスは一種のサブクラスである」とは言えません。

ですからスーパークラスのインスタンスをサブクラスの参照に代入することはできません。

以下のサンプルプログラムを見てください。

package chap10;

public class Example16 {

    public static void main(String[] args) {
        Object o = new Object();
        String s = new String();
        s = (String) o;
    }
}

<結果>

Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String

このとき送出されるのがClassCastExceptionという例外です。

「オブジェクトは一種の文字列である」とは言えませんね。

そのためこのようなキャストができないという例外が発生したのでした。

この例外もよく見かけますのでご記憶ください。

 

13.多重継承の禁止

なお、Javaでは、あるクラスが持てる直接のスーパークラスは1つのみです。

これを単一継承といいます。

単一継承であるということは、「どの子クラスにも親クラスは1つだけ」だということです。

しかし、親クラスには子クラスが複数いても良いことになります。

つまりはクラスの体系は樹形図になります。

例えば、Java8の標準APIで確認するとこんな感じです。

全てのクラスの階層ツリー

 

一方、サブクラスが直接の親として複数のクラスを持てる仕組みを多重継承といいます。

例えば、C++言語では多重継承を採用しています。

Javaではなぜ、多重継承を禁止したのでしょうか?

それはこういう訳です。

class A{

show();

}

class B{

show();

}

class C extends A,B{

}

このようなコードがあった場合に、メインメソッドの中で、

C obj1 = new C();

obj1.show();

としたらどうなるでしょうか?

このshowメソッドがクラスA由来のものかクラスB由来のものか判断がつきませんね。

そういう訳で、Javaでは多重継承が禁じられているのです。

ただし、後で学ぶインターフェースというものを使うと多重継承“的”な処理が可能になりますのでお楽しみに。

 

今回は継承(拡張)について見てきました。

スーパークラスの変数で扱えることやポリモーフィズムなど便利そうな仕組みがありましたね。

ただし、「継承は最後の手段」という言葉があります。

Javaは単一継承のため、継承を使ったクラス設計はめったに採用されないと思ってください。

特に新入社員の皆さんの最初のお仕事ならばなおさらです。

ただし、継承関係にあるクラスを利用することは多くありますからその仕組みを理解しておくことはとても重要です。

そもそも全てのクラスはObjectクラスの子孫なのですから。

なお、継承されないものとして以下の3つがあることは知っておくとなおよいでしょう。

①プライベート宣言されたメンバ

②コンストラクタ(メンバでないので)

③スタティックメンバ(インスタンスの中に含まれないので)

 

今回は継承(拡張)について見てきました。

次回のテーマはカプセル化です。

これも、オブジェクト指向特有のテーマです。

さらに、継承よりも身近に使われるテクニックです。

カプセル化を使ってより頑強なクラスを作っていきましょう。

 

<まとめ:隣の人に正しく説明できたらチェックを付けましょう>

□すでにあるクラスのフィールドやメソッドを新しいクラスが引き継ぐことを継承というが、その際にサブクラスは一種のスーパークラスであるといえることが重要である
 
オーバーライドとは、サブクラスでスーパークラスのメソッドを優先させることでポリモーフィズムが実現できる。ポリモーフィズムとは、同じメソッドに対して違った処理をさせることである
 
□メソッドの仮引数をスーパークラス型にすることで、サブクラス型の実引数を受け取れるようにしておくことをポリモーフィズムを使ったメソッドの引数設計と呼ぶ
 
□継承(is-a)か委譲(has-a)か迷ったら委譲を使う方が良い
 
□クラスの責務を考えてクラス設計をする。
 

 

まとめができたら、アウトプットとして演習問題にチャレンジしましょう。

問題10.継承(拡張)

継承に関する問題です。 目次1.クラス図の作成2.クラス図を実装3.toStringメソッドのオーバーライド4.equalsメソッドのオーバーライド5.Randomクラスの継承6.オリジナルク…

【今回の復習Youtube】

038-オブジェクトの拡張-継承とクラス

039-オブジェクトの拡張-toStringメソッド

040-オブジェクトの拡張-継承とオーバーライド

 

JavaSE8の解説に戻る