新人エンジニア研修で知っておきたい継承の使い方

なぜ、継承の理解が重要なのか、その理由。

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

前回はインスタンスの活用について解説しました。今回は継承(拡張)について解説します。

継承を使うことで異なるクラスを一定のグループとして扱えるようになります。乗り物を例に取れば、タクシーやバス、客船など色々な乗り物があります。これらはそれぞれ違ったクラスと考えることもできますが、大きく乗り物という一つのカテゴリに分類できますね。そうすると同じ性質や動作をもっていると言えます。それらの乗り物を同じグループとして扱えるのが継承です。

継承はとても巧妙な仕組みです。ただし、新人エンジニアの皆さんには少し直感的に分かりにくいところがあります。手続き指向のプログラムではソースコードをざっと読めばどのような処理をしているかが一目瞭然なのですが、オブジェクト指向では一見、自分のクラスが持っていないはずのメンバを使うことができたりするのです。そのあたりのところを今回はお話ししたいと思います。

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という英語には拡張するという意味があるのでしたね。

以下のEmployee,Engineer,Example01という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つのフィールドを持っているのです。しかし、ぱっと見はクラスの持っているフィールドやメソッドが分かりにくいですね。

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

新人エンジニア研修で継承のクラス図を説明
図10.1 継承のクラス図

クラス図ではクラスを四角形で表します。四角形を3つに区切って、一番上がクラス名、真ん中がフィールド、一番下がメソッドです。今回の2つのクラスにはメソッドがありませんので空欄になっています。

そして、2つのクラスの関連である継承関係を白抜きの実線矢印で表現します。クラス図の矢印の向きは重要です。なぜなら、依存関係を表現するからです。上記の例で言えば、親クラスが無いと子クラスが成立しません。一方、子クラスがなくても親クラスは成立します。これが依存関係です。

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

すべてのクラスが自動的にObjectクラスのサブクラスや孫クラスになることが保証されていますので、わざわざクラス図で表現しません。(さらに詳細説明は研修の目的に応じて講師からいたします)

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

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

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

1.サブクラスはスーパークラスの差分だけをプログラミングすればよい。
※これを差分プログラミングといいますが、現在ではこれをメリットと捉えない考え方も有力です。
2.サブクラスをスーパークラスの変数で扱える。
※これを応用してポリモーフィズムを実現します。これは明らかにメリットです。

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

3.継承関係で複数のクラスを仲間として扱える

サブクラス達をスーパークラスの参照で扱うことで複数のクラスを仲間として扱うことができます。

Javaでは、すべてのクラスのスーパークラスにはObjectクラスがあります。つまり、すべてのクラスにはObjectクラスのメソッドがあるということになります。書いてないけれど使える。それが継承(拡張)の仕組みでした。

では、Objectクラスを覗いてみることにしましょう。そろそろ慣れた頃かと思いますが、講師と一緒にJavaのソースコード・ツアーに出かけましょう。

そして148行目以降のequals()メソッドが定義されているところを探してください。

public boolean equals(Object obj) {
  return (this == obj);
}
Java17標準API

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

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

仮引数は Object obj となっています。これはつまり、“Object なら何でも”という意味です。サブクラスをスーパークラスの変数で扱えるからできることですね。

そして、

this == obj

thisというのはこのクラスがインスタンス化したときの自分自身(の参照)を意味しています。まだ、インスタンス化されていない状態でコードが書かれているため参照がなくthis(これ)としか表現しようがないわけです。

これは参照が同一かどうかを見ているわけですね。Objectの同一性は参照が同じであるということで判断しているということです。

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

例えば以下のVehicle,Car,Example02という3つのプログラムはサブクラス(Car)をスーパークラス(Vehicle)の参照で扱う例です。

package chap10;

public class Vehicle {

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

public class Car extends Vehicle {
}
package chap10;

public class Example02 {

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

<実行結果>

I'm running.

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

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

言えますね。

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

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

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

つまり、

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

ということになります。

サブクラスをスーパークラスの参照で扱うことで仲間として扱ったわけです。

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

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

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

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();
    }
}

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

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

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

冗談はさておき、スーパークラス型の変数でサブクラスを扱う時、サブクラス独自のメソッドをそのままでは呼び出せません。しかし、実態は飛行機なのです。ですから、いったんは乗り物型の変数に紐づいた飛行機も、飛ばすことは可能です。プリミティブ型でもみたキャストを使います。(より厳密にはインスタンスをキャストしてるのではなく、参照をキャストしている)

以下のExample04はインスタンスのキャストの例です。

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つのパターンがあります。(下図10.3)

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

文字列 が同じであるということの2つのパターンを新人エンジニア研修向けに解説
図10.3 文字列が同じであるということの2つのパターン

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

①比較対象も文字列である

②比較対象文字と文字数が同じである(Helloの場合は5)

③先頭文字から一文字づつ比較していって最後まで同じ文字である

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

それをJavaのコードで表現しているのがStringクラスのequals()メソッドです。(注:以下はJava8の例です。以降のバージョンでは高速化のためコードが変わっています)

なお、このコードで使われている変数valueは、インスタンスの文字列(今回の例では"Hello")が一文字ずつ格納されている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;
  }

Java8 標準API

(n- -!= 0)という表現が見慣れないかと思います。これは、以下と同じなのですが、上記の書き方で1行短くしているのです。

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

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

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


中古車でしたら全く同じ車体でないと同じ車と判断してはいけませんね。しかし、新車でしたら同じ車と判断しても良い車がたくさんありますね。

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

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

したがってクラスごとにequal()メソッドの内容は書き換えることができるのです。Stringクラスのequal()メソッドの例で見たように、サブクラスでスーパークラスのメソッドを変更することができます。これをオーバーライド(override)と言います。英語では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 running fast.");
    }
}
package chap10;

public class Example05 {

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

<実行結果>

I'm running fast.

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

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

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

次のExample06はそのことを説明するプログラムです。

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

package chap10;

class PatrolCar extends Vehicle {

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

class Rocket extends Vehicle {

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

class Ship extends Vehicle {

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

public class Example06 {

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

<実行結果>

I'm running.
I'm flying.
I'm sailing.
I'm running.
I'm flying.

<イメージ図>

新人エンジニア研修
図10.4 ポリモーフィズムのイメージ

今回のように「v.run()」という同じ命令にもかかわらず、インスタンスごとに違った処理を実行しました。このようなオブジェクト指向の仕組みをポリモーフィズム(多態性)といいます。

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

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

自転車クラスを考えます。この自転車クラスは防犯登録番号(registryNumber)が同じなら、同一であると判断するとします。先ほどのStringクラスのequals)メソッドを参考にして作成してみました。他の乗り物クラスでもequals()メソッドを持つことを想定し、汎用性を考えて仮引数を(Vehicle aVehicle)としました。(ちなみにequals()メソッドもIDEの機能を使えば簡単に挿入できるうえスペルミスを減らすことになります)

package chap10;

public class Bicycle extends Vehicle {

    int registryNumber;

    public Bicycle(int registryNumber) {
        this.registryNumber = registryNumber;
    }
    
    @Override
    public int hashCode() {
        int hash = 5;
        hash = 47 * hash + this.registryNumber;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Bicycle other = (Bicycle) obj;
        if (this.registryNumber != other.registryNumber) {
            return false;
        }
        return true;
    }
}
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メソッドの独自実装を新人エンジニア研修向けに解説
図10.5 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 running slowly.");
    }
}

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 running slowly.
I'm flying.
想定していないクラスです

<イメージ図>

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

例えば、ここで新たにSurfingクラスを作るとします。すると、“想定していないクラス”なのでmain()メソッドのif文を追加する必要が出てきてしまうのです。

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

<ポリモーフィズムを使った場合> 

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 Rocket(), new Ship(), new Surfing()};
        for (Vehicle v : vs) {
            v.run();
        }
    }
}

<実行結果>

I'm running.
I'm flying.
I'm sailing.
I'm surfing.

<イメージ図>

新人エンジニア研修
図10.7 ポリモーフィズムを使うと保守に強くなる

今動いているプログラムをいじるというのはとても勇気のいることです。もっと複雑なコードで、しかも、この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@5bd

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

このとき、例えば、println()メソッドを使い防犯登録番号を表示させることはできるのでしょうか?例えば、上記の表示を以下のように変えたいとします。

私の防犯登録番号は、1234

toString()メソッドをオーバーライドすればできそうですね。

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

PrintStreamクラスの1046行目以降です。

    public void println(Object x) {
        String s = String.valueOf(x);
        if (getClass() == PrintStream.class) {
            // need to apply String.valueOf again since first invocation
            // might return null
            writeln(String.valueOf(s));
        } else {
            synchronized (this) {
                print(s);
                newLine();
            }
        }
    }
Java17 標準API

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

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

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

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

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

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

クラスはObjectクラスの255行目以降です。

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

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

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

オリジナルな文字表現で標準出力できそうですね。やってみましょう!

以下のExample11は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のソースコードで定義を確認するとObjectクラスの100行目で、

public native int hashCode();
Java17標準API

と書かれています。中身のないメソッドです。そしてnative修飾子がついています。コンピュータの母国語はマシン語です。C言語で書かれてマシン語にコンパイルされたコードです。Javaではこれ以上、遡ることはできません。(英語でも母国語を話せる人をnativeといいますね)

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

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

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

package chap10;

class Parent {
  public Parent() {
    System.out.println("Hello from SuperClass");
  }
}
 
class Child extends Parent {
    Child(){
    }
}
 
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 Example13 {
  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 Example14 {

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

<実行結果>

Hello from First
Hello from Second
Hello from Third

この順番をよくご記憶ください。スーパークラスから順番にサブクラスがコンストラクトされていってます(コンストラクターチェーンと呼ばれることもあります)。スーパークラスの土台の上にサブクラスは作られるのです。(一番下の土台はObjectクラス)

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

新人エンジニア研修
図10.8 スーパークラスの土台の上にサブクラスは作られる

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

先にも述べたとおり、あるクラスのある機能を取り込みたいばかりに、継承を使うというのは悪い設計です。継承を使うとスーパークラスの変更がサブクラスに及ぶため、クラス間に依存関係が生じてしまうのです。

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

継承を使わなくても他のクラスの機能を取り込むことができます。その方法を「依存」や「委譲」といいます。

まずは依存を使ってみます。次のサンプルプログラムは依存を使った例です。

package chap10;

import java.util.Scanner;

class DependencePc {

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

public class Example15 {

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

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

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

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

これが依存です。useの関係ともいいます。

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

野球で例えるならピンチヒッターとして、一時的に起用された選手のような役割です。このinput()メソッドの中だけで活躍します。

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

依存
図10.9 use関係

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

以下のExample16は委譲を使った例です。

package chap10;

import java.util.Scanner;

class DelegatePc {

    Scanner scanner = new Scanner(System.in);

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

public class Example16 {

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

<結果は上記と同じ>

ご注目いただきたいのは、フィールドにScannerクラスのインスタンス変数を宣言している部分です。そうしておいて、input()メソッドでScannerクラスのnextLine()メソッドを呼び出しています。

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

has-a関係
図10.10 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 Example17 {

    public static void main(String[] args) {

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

        System.out.println(Example17.equals(bi1, bi2));
        System.out.println(Example17.equals(bi2, 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 Example18 {

    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つだけ」だということです。しかし、親クラスには子クラスが複数いても良いことになります。つまりはクラスの体系は樹形図になります。例えば、Javaの標準APIで確認するとこんな感じです。

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

一方、サブクラスが直接の親として複数のクラスを持てる仕組みを多重継承といいます。例えば、C++言語では多重継承を採用しています。

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

それはこういう訳です。

class A{  
 show();
}
class B{
 show();
}
class C extends A,B{
}

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

C obj1 = new C();  
obj1.show();

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

このshow()メソッドがクラスA由来のものかクラスB由来のものか判断がつきませんね。そういう訳で、Javaでは多重継承が禁じられているのです。ただし、後で学ぶインタフェースというものを使うと多重継承“的”な仕組みが実現可能になりますのでお楽しみに。

今回は継承(拡張)について見てきました。スーパークラスの変数でサブクラスが扱えることや同じメソッドで違った動作をさせるポリモーフィズムなど便利そうな仕組みがありましたね。

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

Javaは単一継承のため、継承を使ったクラス設計はめったに採用されないと思ってください。特に新入社員の皆さんの最初のお仕事ならばなおさらです。それでもなお継承関係にあるクラスを利用することは多くありますからその仕組みを理解しておくことはとても重要です。そもそも全てのクラスはObjectクラスの子孫なのですから。また、実用的な継承の例としてはWebアプリケーションのデータベースを扱うところでお話する予定です。

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

  • プライベート宣言されたメンバ
  • コンストラクタ
  • スタティックメンバ(インスタンスの中に含まれないので)

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

□ すでにあるクラスのフィールドやメソッドを新しいクラスが引き継ぐことを継承というが、その際にサブクラスは一種のスーパークラスであるといえることが重要である

□ オーバーライドとは、サブクラスでスーパークラスのメソッドを優先させることでポリモーフィズムが実現できる。ポリモーフィズムとは、名前の同じメソッドに対して違った処理をさせることである

□ オリジナルなクラスを作成したらtoString()メソッドをオーバーライドする

□ メソッドの仮引数をスーパークラス型にすることで、サブクラス型の実引数を受け取れるようにしておくことをポリモーフィズムを使ったメソッドの引数設計と呼ぶ

□ 継承(is-a)か委譲(has-a)か迷ったら委譲を使う方が良い

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

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

以上、今回は「継承を使ってクラスをグループ化する」方法について見てきました。

次回は、「カプセル化と情報隠蔽で部品の完成度を高める」です。

継承を使ってクラスをグループ化する 最後までお読みいただきありがとうございます。