【Java初心者用】これで絶対理解できる!!『クラスの継承』
Javaの学習を始めて「クラスの継承」でつまずく人が多いと思います。スーパークラスのメソッドがサブクラスでも使える、コンストラクタは継承できない、色々な疑問があるはずです。今回は継承の仕組みを図解を用いてじっくりと説明します!! これでクラスの継承について理解できます!!
まず、この記事は以下のような人を対象としています。
対象者・クラスの継承の役割について知りたい人
・メソッドのオーバーライドについて知りたい人
・コンストラクタについて知りたい人
・クラスのキャストについて知りたい人
この記事を読むと、次のようなことが理解できるようになります。
この記事を読むとできること・クラスを継承する理由を知ることができる
・オーバーライドされたメソッドの実行方法を知ることができる
・コンストラクタが継承できない理由を知ることができる
・サブクラスをスーパークラスでキャストするとどうなるかを理解することができる
・差分プログラムについて知ることができる
クラスの継承を勉強しているのですがイマイチ仕組みが理解できません。
継承とは何か、継承する利点をまず理解する必要があります。
オーバーライドやコンストラクタといった用語も多いので混乱するかも知れませんね。
色々と覚える言葉が多く、大変です。
言葉だけ覚えても、何だか理解できていないような・・・
今回は図解をまじえてじっくりと説明していきます。
きっと、読み終える頃には継承について理解できると思います!!
クラスとは
Javaでは全てのプログラムは「クラス」です。if文やfor文などの制御構文の勉強をするために作成するサンプルプログラムも実際はクラスを作成しています。Javaの基本的な使い方を勉強している時は気にしていませんが、作成しているのは全てクラスです。
本来Javaでプログラムを作成する場合、まずその前にシステムエンジニアなどがクラス図(複数のクラスの相関図)を作成しておきます。開発するシステムにはどのようなクラスが必要で、それらがどのように関連しあっているのかきちんと決めておかなければなりません。クラス図が完成したら、プログラマにクラスを作成する仕事が割り振られます。したがってプログラマが自分でゼロからクラスを作ることは、まれです。つまり、プログラマはクラスを設計できなくても大きな問題ではありません。
しかし、いずれプログラマからシステムエンジニアにキャリアアップするためにはクラスがどのようなものか、どのように関係しているのか、といったことは理解しておく必要があります。
汎化と特化
クラスの継承を説明する場合、「あるクラスを作成し、それが他のクラスでも利用されるので、スーパークラスとして継承して・・・・」と解説している例があります。スーパークラスから説明が始まることが多々ありますが、その考え方ではクラスの継承を理解することでできません。まず、スーパークラスを作成する理由を考える必要があります。
Javaでシステムを開発する場合、多数のクラスを設計します。クラスはプログラムで表現したい事象や業務です。それぞれのクラスを設計していくうちに、共通処理が存在するクラスが複数設計される場合があります。共通部分をそれぞれのクラス内に別々に記述しても構いませんが、共通部分の処理内容を変更したい場合、複数個所修正しなければなりません。しかし、共通部分を1つにまとめておけば、修正する個所は1か所で済みます。修正作業を人間がおこなう際、直す数が多くなればなるほとヒューマンエラー(ミス)が発生する可能性が高まります。なるべく修正する個所が少なくなるように設計しなければなりません。
上の図のようにクラスA、クラスB、クラスCに同じ処理(メソッド)が定義されていて、そのメソッドの一部を修正しなければならない場合、全てのクラスのメソッドを修正する必要があります。一方、共通部分(メソッド)を他のクラスDにまとめて定義した場合、修正する個所はクラスD内のメソッドだけになります。
このように複数のクラスに共通部分が存在している場合、その部分だけを取り出して別のクラスとして定義します。共通部分を取り出すことを「汎化」と呼びます。一方、共通部分を取り除いたクラスは、そのクラス独自の処理のみが残っています。独自の処理のみ定義することを「特化」と呼びます。スーパークラスを上にたどればたどるほど「汎化」が進み、サブクラスを下にたどればたどるほと「特化」が進みます。
継承
特化されたクラスは複数の別のクラスの共通部分が取り出されてまとめられています。つまり、他のクラスには共通部分が取り出されて何も残っていません。しかし、この共通部分はいらない訳ではなく、そのクラスでも利用できるようにする必要があります。他のクラス内で定義されている処理内容を自分のクラス内でも利用できるようにする仕組みが「クラスの継承」です。
別のクラスを継承することで、別のクラス内に定義されている処理を自分のクラスで利用することができるようになります。この際、継承するだけで利用したい処理内容を自分のクラス内で記述する必要はありません。継承するだけで、その処理を直ぐに利用できるようになります。
上の図では、クラスBはクラスAを継承しています。したがって、クラスBは自分自身のクラス内に処理Aを記述していませんが、処理Aをいきなり利用することができます。また、クラスCはクラスBを継承しています。クラスBはクラスAを継承しているので、クラスC内には何も記述していませんが、継承元に定義されている処理Aと処理Bをそのまま利用することができます。
なお、継承されるクラスを「スーパー(親)クラス」、別のクラスを継承しているクラスを「サブ(子)クラス」と呼びます。上の図では、クラスAがクラスBから見たスーパークラスとなり、クラスBはクラスAから見るとサブクラスとなります。またクラスBはクラスCから見るとスーパークラスとなります。このようにサブクラスであっても、別のクラスから見るとスーパークラスとなる場合があります。
差分プログラミング
クラスを汎化によって分けて定義し、継承を利用するとサブクラスではスーパークラスで定義されている処理を何も記述せず利用できることを説明してきました。これが継承の利点となります。
また、サブクラスでは自分に特化した内容を定義していきます。このようにスーパークラスで定義されていない自分に特化した処理だけを記述していく方法を「差分プログラミング」と呼びます。差分プログラミングではスーパークラスでは足りないものをサブクラスで追記していきます。継承を利用することでサブクラスで定義する内容は足りないものだけなので、コード量を減らすことができます。
差分プログラミングによって入力するコード量を減らすことができるのも継承を利用する際の利点となります。
クラスA(スーパークラス)には汎化によってクラスBとクラスCの共通部分の処理Aが定義されています。このクラスAを継承しているクラスB(サブクラス)では、クラスB独自の処理として処理Bを定義しています。スーパークラスの処理Aに足りない処理Bを追加しています。これが差分プログラミングです。同様にクラスCもスーパークラスの処理Aに足りない処理Cを差分として追加しています。
オーバーライド
スーパークラスに定義されている処理内容はサブクラスではそのまま利用することができます。しかし、サブクラス側で内容を若干変更したい場合があります。この様にサブクラス側でスーパークラスに定義されている処理を改めて定義しなおすことを「オーバーライド」と呼びます。オーバーライドはメソッド名を変更したくないが、内容は変更したい時に有効です。
なお、オーバーライドされたスーパークラス側の処理自体が上書き(変更)される訳ではありません。上書きと説明されるとスーパークラス側の内容が変更されるように誤解されますが、スーパークラス側の処理がそのまま残り、サブクラス側に同名の処理が追加される形となります。
プログラムを実行した時のイメージ
ここまでの用語の説明については、初心者も大体理解できると思います。クラスの継承の仕組みが理解できない理由は、実際にプログラムを実行した時のコンピュータ内部の状態を把握できていないためです。継承しているプログラムを実行するとスーパークラスやサブクラスが実際どうなっているのかが理解できれば、継承に関する様々な疑問点は解消されるはずです。
作成したプログラム(ソースファイル、拡張子「*.java」)をコンパイルすると中間コード(クラスファイル、拡張子「*.class」)がハードディスクなどに保存されます。プログラムを実行すると、この中間コードがメインメモリにロードされます。プログラムを実行するためには、その元となる中間コードをメインメモリにロードし、プロセスを作成する必要があります。
プログラムをロードする仕組みについては、以下の記事を参照してください。
以下のサンプルで具体的に説明します。
- 「SuperClass」クラスにはメソッド「showMessage()」が定義されている
- 「SubClass」クラスは「SuperClass」クラスを継承している
- 「SubClass」クラスにはメソッド「showWarning()」が定義されている
- 「SubClass」クラスと「SubClass」クラスにはエントリポイント(main()メソッド)がないので単独で実行することはできない
- 「ClassMain」クラスが「SubClass」クラスをnewでインスタンスを生成し利用する
「ClassMain」クラスの具体的な処理内容(ソースコード)は次の通りです。
このプログラムを実行すると、コンピュータ内部の状態は以下のようになります。順番に解説します。
プログラム「ClassMain」を実行すると、HDDなどに保存されているコンパイル済みの中間コード(ClassMain.class)ファイルがメインメモリにロードされます(①)。メインメモリにロードされると、専用の作業場所としてプロセスが作成されいます。その中でエントリポイントであるmain()メソッドが自動的に実行されます。
8行目のプログラムが実行されると、new演算子によってSubClassクラスのインスタンスが生成されます(②)。この時、HDDなどに保存されている中間コード(SubClass.class)ファイルがメインメモリーにロードされます(②)。ロードされたSubClassクラスのインスタンスは専用の作業場所としてプロセスが生成され、その領域のスタート地点を示す位置情報(番地)が変数sbに代入されます(④)。これで、変数sbを使えば、いつでもロードされたSubClassインスタンスのインスタンスメンバ(変数やメソッド)にアクセスできます。
このようにプログラムを実行すると、対象となる中間コード (クラスファイル)がHDDなどからメインメモリにロードされます。Javaのプログラムを理解するためには難しいですが、この流れを意識しましょう。
しかし、このイメージではスーパークラスに定義されている「showMessage()」メソッドはどのようにして実行されているのでしょうか? 「SubClass」クラス用のプロセス内には「showMessage()」メソッドがありません。継承するとSuperClassクラス内のメソッドがSubClass内にコピーされているのでしょうか?
実は上で説明した図は正しいものではありません。SubClassクラスが何も継承していなければ、この図で正しいのですが、他のクラスを継承している場合、実行時のメモリの状態は上の図のものとは異なります。正確なメモリの状態を理解できるとクラスの継承も理解できるようになります。
では実際、プログラムを実行してからのメモリの状態を詳しく説明します。
①と②までは前の図の説明と変わりません。サブクラスであるSubClassクラスをnew演算子でインスタンスを作るところから処理内容が変わってきます。SubClassクラスのインスタンスを生成しようとするのですが、このSubClassクラスはSuperClassクラスを継承しています。他のクラスを継承している場合、まず最初にスーパークラスのインスタンスが生成(ロード)されます。したがって、SubClassクラスのインスタンスを生成する前に、SuperClassクラスの中間コードがロードされ(③)、メインメモリ内に専用のプロセスが作成されます。その後、SubClassクラスの中間コードがロードされ(④)、SubClassクラス用のプロセスが作成されます。つまり、SubClassクラスのインスタンスを生成しているのですが、同時にSuperClassクラスのインスタンスも生成されているのです。同時に作成されたSuperClassクラスのインスタンスはSubClassクラスのインスタンスの内部に格納されているイメージとなります。全てのクラスのロードが完了すると、new演算子で作成したインスタンスのプロセスの位置情報(番地)が変数sbに代入されます。
このように他のクラスを継承しているクラスのインスタンスを作成すると、そのインスタンスの内部にスーパークラスのインスタンスも同時に作成される、というイメージを持つことが大切です。
変数sbが参照するSubClassクラス用のプロセス(領域)内にはSuperClassクラスのインスタンスがあるので、SuperClassクラスで定義されている「ShowMessage()」メソッドが問題なく実行できます。
スーパークラスのメソッドは継承される
クラスの継承を勉強すると、スーパークラスから何がサブクラスに継承されるのか、覚えなければなりません。しかし、今回説明した実行時のイメージを正しく理解していれば問題なく理解できます。
継承されるということはサブクラスのインスタンスでも利用できる、ということです。利用できるかどうかは、スーパークラスのインスタンスがあるかないかになります。サブクラスのインスタンスを作成すると、スーパークラスのインスタンスも自動的に作成される、ということを理解していればスーパークラスのメソッドが継承(利用)できることが分かるはずです。
プログラムの処理が実行できるかどうかは、そのクラスファイルがメインメモリにロードされているかどうか、が重要です。HDDなどの記憶装置に保存されている単なるデータファイルの状態では中に記述されている処理は実行できません。必ず実行させるためには、メモリにロードする必要があります。
スーパークラスのコンストラクタは継承されない
クラスの継承を勉強すると、スーパークラスのコンストラクタはサブクラスに継承されない、という説明を見ると思います。コンストラクタがどのような働きをするものなのかを理解すれば、継承されない利用も理解できます。
まず、大前提としてコンストラクタはメソッドではありません。そのクラスのインスタンスを作成する時に自動的に1度だけ実行される特別な処理の集まりです。ここでのポイントは「そのクラスのインスタンスを作成する」となります。つまり、コンストラクタはコンストラクタが定義されているクラス専用の処理で、その他のクラスでは全く使い物にならない、ということです。
したがって、コンストラクタをもしサブクラスに継承できたとしても、そのコンストラクタはスーパークラスだけでしが利用できないので、何の役にも立ちません。継承しても役に立たないのであばれ、はじめから継承できないようにしておきましょう!! ということです。
サブクラスをスーパークラスでキャストする
クラスの継承を利用している場合、次のようなプログラムを見ることがあります。
注目は8行目です。サブクラスであるSubClassクラスのインスタンスをnewで作成し、その位置情報を保存する変数をSubClass型ではなく、SuperClass型で定義しています。この宣言自体は全く問題ないので、コンパイルエラーにはなりません。しかし、14行目でSubClassクラスのインスタンス内に存在するshowWarning()メソッドを実行しようとするとコンパイルエラーとなります。
コンパイルエラーのメッセージは「メソッドshowWarning()は型SuperClassで未定義です」というものです。メモリにロードされたインスタンスのプロセスイメージは以下のような状態で、きちんとshowWarning()メソッドが存在しています。一体何故、このようなコンパイルエラーが発生するのでしょうか?
変数のデータ型を宣言する理由は、どのようなデータを格納するか、そのデータの種類を事前に通知することです。しかし、この考え方だけでは上のコンパイルエラーの理由は理解できません。
変数のデータ型の宣言は、「インスタンスのどの部分を見るか」と置き換えれば理解しやすくなります。下の図を見てもらうと、一目瞭然だと思います。
左側はSubClassクラスのインスタンスをSubClass型で見ているので、インスタンス全体が見えます。一方、右側はSubClassクラスのインスタンスをSuperClass型で見ているので、見える範囲はSuperClassクラスのインスタンス部分のみとなります。つまり、SubClassクラスのインスタンスの一部分しか見えていないのです。プログラムで実行しようとしているshowMessage()メソッドはメモリ上には存在しているのですが、変数scからは見えない範囲に存在しています。したがって、実行しようとするとコンパイルエラーとなるのです。
なお、変数scが見える範囲内にはshowMessage()メソッドが存在しているので、このメソッドを実行する11行目はコンパイルエラーにはなりません。
このように、クラスを継承している場合、どの範囲を見ようとしているのか、変数のデータ型に注目してください。
メソッドのオーバーライド
スーパークラスのメソッドをサブクラスでも再定義(オーバーライド)した場合、どちらのメソッドが実行されるでしょうか? オーバーライドされたメソッドはサブクラス側の内容が実行されます。
メモリ上には同じ名前で処理内容が異なるメソッドが2つ同時に存在しているのですが、優先されるのはサブクラスでオーバーライドしたメソッドとなります。
では、次の場合はどうなるのでしょうか?
SubClassクラスのインスタンスをSuperClass型で参照しています。既に説明していますが、この場合、見える範囲をSuperClassクラス部分だけに限定しています。showMessage()メソッドが2つ存在していますが、見える範囲内にあるのはSuperClassクラスで定義しているメソッドです。では、SuperClassクラス側のshowMessage()メソッドが実行されるのでしょうか? 実は、この場合も実行されるのはサブクラス側に存在しているshowMessage()メソッドとなります。
オーバーライドしているメソッドは見える範囲が限定されていても、サブクラス側で再定義されたものが必ず実行されます。先ほどは「サブクラス側が優先される」と書きましたが、実際は「必ずサブクラス側が実行される」のです。
まとめ
今回は、クラスの継承について説明しました。クラスの継承はいきなり考えるわけではなく、複数のクラスを設計していくうちに共通部分が出現し、それを分割する必要性が発生した時に考えます。多くのJavaの書籍では、いきなりスーパークラスを作って利用する説明ばかりですが、実際はそうではありません。スーパークラスを作る利点などをきちんと理解しておく必要があります。
また、サブクラスのインスタンスをサブクラス型とスーパークラス型で参照する場合の違いについても説明しました。これは非常に難しいのですが、非常に重要な考え方です。今回図を使って説明しているので、是非何度も見直して理解してください。
・クラスの継承は「汎化」と「特化」を意識して設計する
・コンストラクタはそのクラスのインスタンスを作るためだけに利用され、他のクラスでは使えない
・プログラムを実行した時にメモリのイメージを意識することが重要
・サブクラス型、スーパークラス型の違いは「見える範囲」
今回はクラスの継承に関する重要な内容について説明しました。
いかがでしたか?
用語だけで覚えようとすると大変ですが、図を使って説明されると分かりやすいです。ただ、結構難しい内容でした。
いきなり理解するのは大変だと思います。
ゆっくり何ででも見直してください!!
継承は使っていませんが、クラスの作り方として『銀行口座』や『Lotoくじ』を題材に説明して言える記事があります。ぜひご覧ください。