第22講・STEP 03

継承って何?

今回は、あるクラスの機能を引き継いで新しいクラスを作る「継承」です。人間とプログラマーの関係を例に、継承の書き方と、クラスをどう結び付けるべきかを考えてみましょう。

人間を継承したプログラマー

 例えばこんなHumanクラスがあるとしましょう。

今回は継承の仕組みに注目するため、前回のHumanクラスを簡略化した別の例を使います。各ファイルは同じパッケージに置き、メソッドの処理内容は空にしています。

Human.java

class Human{
    String name;
    public void eat() {}
    public void sleep() {}
}

 このHumanクラスを継承して、Programmerクラスを作ります。
 あるクラスを継承したクラスを作るには、クラス名の後ろに
extends クラス名
を付けるだけです。

Programmer.java

class Programmer extends Human{
    public void programming() {}
}

 これでHumanクラスを継承したProgrammerクラスが出来ました。

 継承元であるHumanクラスのことを親クラス、スーパークラス、基底クラスなんていう風に呼びます。継承した側のProgrammerクラスは子クラス、サブクラスと呼びます。
 
 Programmerクラスで定義されているのはご覧のようにprogramming()メソッドのみですが、プログラマーはプログラミングしかできないのではありません。Humanクラスを継承してるので、食べることも寝ることも出来るし、名前も付けられます。

 もしこのProgrammerクラスを継承なしで書くなら、こういうことです。↓

Programmer.java

class Programmer {
    String name;
    public void eat() {}
    public void sleep() {}
    
    public void programming() {}
}

 Humanクラスのメンバをコピペしてから、programming()メソッドを追加した状態です。

 継承することで、親クラスから引き継げるメンバ(フィールドやメソッド)は、子クラスに書き直さずに使えます。追加したいメンバだけ書き足せばいいので楽ちんです。

ただし、privateなメンバは子クラスに継承されません。また、コンストラクタも継承されません。Programmerクラスにはコンストラクタがないのでデフォルトコンストラクタが備わっていることになります。

 この例では、継承してもコピーしても同じ名前のフィールドやメソッドを使えます。しかし、クラス同士の関係は同じではありません。コピーしただけのProgrammerクラスはHumanクラスとは別の型ですし、Humanクラスを書き変えてもProgrammerクラスには反映されません。

 なので、もしHumanクラスにwalk()メソッドもあった方がいいな。なんてことになって後から付け足した場合、Programmerクラスにも付け足さないといけないことになります。もしProgrammerクラスだけでなく、DoctorクラスやTeacherクラスなどの人間的なクラスをたくさん書いていたら、それら全部のクラスにwalk()メソッドを付け足す必要に迫られます。

 コピペをするのではなく初めから継承を使っていたら、親クラスであるHumanクラスにwalk()メソッドを書き足すだけで済むので楽ですね。

継承の本質

 とは言え、継承とは単にコピペするのが面倒だから使うものではありません。継承の本質はそこじゃありません。重要なのは

プログラマーは人間である

と言うことです。あ、人権うんぬんの話じゃないですよ。(笑)

 これは継承の最も重要なポイントです。

class Child extends Super
とした場合、
Child is Super
の関係が成り立ちます。

プログラマー is 人間です。

 人間クラスを継承することで、ある特殊な人間(プログラマー)を作るわけです。そしてどんな特殊な人間も人間であることには変わりはありません。人間クラスを継承してるクラスは全て人間です。

 なので、Programmerクラスのインスタンスを、Human型の変数に代入して扱うことが可能になります。以下はHumanTest.javaのmainメソッド内に書く例です。

HumanTest.java

Human programmer = new Programmer();

 プログラマーは人間として扱っていいんです。
 プログラマーだけでなく人間クラスを継承しているクラスなら医者も先生もプログラム上、人間として扱えるようになります。

 DoctorクラスとTeacherクラスも、Humanクラスを継承して用意しておきましょう。

Doctor.java

class Doctor extends Human {
}

Teacher.java

class Teacher extends Human {
}

 すると、HumanTest.javaのmainメソッド内で、こんな配列を作れます。

HumanTest.java

Human[] humans;
humans = new Human[] { new Programmer(), new Doctor(), new Teacher() };

 Human配列の要素として、子クラスのインスタンスを格納しています。こういうことが出来るのは継承という機能のおかげです。

宣言とは別の行で配列を代入しているので、new Human[]を省略できません。また、Human型の変数から呼べるのはHuman型で使えるメソッドです。Programmerだけが持つprogramming()を、そのまま呼ぶことはできません。

 大事なことなのでもう一度言います。
Child is Super
これが継承の本質です。

継承の間違った使い方

 逆に言えば、クラスを設計する際にChild is Superが成り立たないような継承はするべきではありません。

 例えば、Dogクラスを書こうと思って、「bow()とeat()とwalk()とsleep()か……。書くの面倒くせーな。お、Humanクラスのメンバを継承しちゃえばいいじゃん♪」ってことで、

Dog.java

// 動作しても、今回のモデルでは不適切な継承の例
class Dog extends Human{
    public void bow() {}
}

としてしまった場合、
Dog is Human
の関係が成り立つことになってしまいます。プログラム上、犬は人間の一種だということになってしまいます。おかしいですよね。

 別に犬が人間であろうと動物であろうと植物であろうとそんなの関係ない。必要なメソッドを持ってるクラスを継承して何が悪いんだ。とは思わないで下さい。その時は何の問題もなかったとしても、もし後からHumanクラスにwork()メソッドを付けたすと、人間向けの仕事の処理までDogクラスに引き継がれてしまいます。

 別にDogクラスにwork()メソッドがあろうとstudy()メソッドがあろうと、要らないメソッドは使わなかったらいいだけじゃないか。とは思わないで下さい。人間クラスは人間らしいクラスにし、犬クラスは犬らしいクラスにしないと、クラス設計が無茶苦茶になってきます。ちょっとぐらい大丈夫だと思って辻褄の合わないクラスの設計をし始めてしまうと、そのうち自分でもワケが分からなくなるのがオチです。このDogクラスって犬のことじゃないのか?なんてことになったら、もはやオブジェクト指向の意味がありません。他の人が読んだ時にも、クラス名から役割を理解しにくくなってしまいます。

継承の正しい使い方

 Child is Superというのは、言い方を変えると子クラス(Child)よりも親クラス(Super)の方が抽象度が高いということです。

 例えばこんな継承関係があったとします。↓

プログラマー → 人間 → 動物 → 生物

 ちなみに、この図やUMLのクラス図で継承を表す時は、このように子クラスから親クラスへの矢印で表現します。なんとなく親から子に矢印を指したくなる気持ちも分かりますが、反対ですのでご注意を。

 親に行けば行くほど抽象的なモノになっているのが感覚的に分かると思います。これが正しい継承の仕方です。Child is Superの原則が崩れないように継承していけば、クラスの役割を整理しやすくなります。名前の分類だけでなく、子クラスを親クラスとして使っても、期待される動作が成り立つことが大切です。

犬 → 人間 → 動物 → 生物
 ↑これではおかしいというのが感覚的に分かりますよね。

 犬クラスを書くとしたら、動物クラスを継承して書けば自然なクラス設計と言えます。
犬 → 動物 →生物

 つまりこの場合、人間クラスと犬クラスが動物クラスを継承している状態にするのが正解と言えます。
プログラマーは人間を継承し、人間と犬は動物を継承する。動物は生物を継承する関係図

全てのクラスの先祖 Objectクラス

 JavaのAPIにはStringクラスやIntegerクラスなど、いろいろなクラスが用意されています。Objectクラス自身を除くすべてのクラスは、直接または間接的にjava.lang.Objectクラスを継承しています。言い方を変えると、Objectクラスがクラス階層のルートです。

 自分で書くHumanクラスも同じです。今回のようにextendsを書かない通常のクラスは、Objectクラスを直接継承します。

 なので、HumanやProgrammerのインスタンスにも、Objectクラス由来のメソッドがあります。例えばequals()やtoString()は、もともとObjectクラスで定義されているpublicなメソッドです。

参考:Object(Java API)

 Objectクラスのpublicなインスタンスメソッドは、自分で作ったクラスのインスタンスからも呼べます。ただし、すべてのメソッドがpublicなわけではなく、呼び出せる範囲はアクセス修飾子によって異なります。

親クラスは1つだけ

 Javaでは、直接の親として同時に複数のクラスを指定することはできません。

 蝶のように舞い、蜂のように刺すボクサーを作りたいからと言って、

Boxer.java

// コンパイルエラーになる例:親クラスを2つ指定することはできません
class Boxer extends 蝶, 蜂{
    public void punch() {}
}

 ↑こんな書き方はできません。継承元として選べるのは1つだけです。

 もし複数のクラスの機能を継承できたら、それはそれで便利だと思います。ですが、Javaではそれは出来ない仕様になっています。

 複数の親を継承したクラスが作れてしまうと、別の親クラスに同じ名前のメンバが存在してしまったり、扱いが複雑になることがあります。またそれだけでなく、親を辿ればどんどん抽象化されて一つのクラス(Object)に収束していくという美しい構造を実現するために、Javaではこのルールがあるんだと思います。なお、後の講で学ぶ「インターフェース」は、複数実装することができます。

 難しいことはさて置き、親クラスは1つだけというのは大事なルールなので覚えておいて下さい。

 継承には細かくてややこしいルールもいろいろありますが、まずは概念だけでもしっかり理解しておきましょう。次回は、親クラスから引き継いだメソッドを子クラスで定義し直す「オーバーライド」を学びます。

目次へ

質問・コメント

読み込み中

この記事についての質問や、学習して気づいたことをお寄せください。

コメントを書く

投稿フォームを準備しています。

コメントと一緒に公開される名前です。
3,000文字以内。パスワードなどの個人情報は書き込まないでください。

コメントは送信後すぐに公開されます。

この記事のコメント

旧サイトに寄せられたコメントも当時の投稿内容のまま掲載しています。現在の記事では変更している内容や、旧環境についてのやり取りも含まれます。

コメントを読み込んでいます。