本章ではオブジェクト指向でプログラムするために重要な機能である、インヘリタンス(継承)に関して学習します。
本章で学習する内容を動画としてまとめたものです。最初に一通り見終わった後で、学習に入るようにしてください。
オブジェクト指向言語は、似たようなプログラムコードをできるだけ使いまわそうという考え方を持っています。
プログラムの1単位であるクラスも、同様の処理を持つクラスをいくつか記述する必要がある場合、一つのクラスのプログラムソースを使い回す技術があります。
この技術の一つがこれから学習するインヘリタンス(継承)です。
継承は、新しくクラスを作成するときに、すでに存在する他のクラスのフィールドやメソッドをそのまま受け継ぐことができる手法です。
例えば、以下の図のように、クラスMySuperにフィールドxとメソッドgetXが定義してあるとき、このインヘリタンスの手法を用いることで、フィールドxとメソッドgetXをクラスMySubがそのまま引き継ぐことができます。
そのため、クラスMySubは、クラスMySuperのフィールドとメソッドを特に記述していなくても、定義してあることになります。
継承の元となるクラスを親クラス(スーパークラス)と呼び、それを継承するクラスを子クラス(サブクラス)と呼びます。
以下がインヘリタンス(継承)の書式です。子クラス(サブクラス)を定義するときに、「extends」を使用し、親クラス(スーパークラス)を継承します。
public class 子クラス extends 親クラス {
// (省略)
}
「MySuper」クラスを継承した「MySub」クラスは以下のようになります。
まずは、親クラス(スーパークラス)の「MySuper」クラスの定義です。
public class MySuper {
// フィールドxの定義
int x = 10;
// メソッドgetXの定義
int getX() {
return x;
}
}
次に子クラス(サブクラス)の「MySub」クラスの定義です。
// MySuperクラスを継承
public class MySub extends MySuper {
// 親クラス「MySuper」クラスのフィールドxとメソッドgetXは、
// 記述がなくても、定義されています
// フィールドyの定義
int y = 20;
// メソッドgetYの定義
int getY(){
return y;
}
}
子クラス(サブクラス)である「MySub」は、「MySuper」を継承することによって、メンバ(フィールドやメソッド)を何も記述しなくても、親クラス(スーパークラス)である「MySuper」のフィールドやメソッドが定義されていることになります。
また、上記の図のように、子クラス(サブクラス)は、自分独自のフィールドやメソッドを定義することにより、継承したメンバと追加したメンバの両方が定義されていることになります。
なお、コンストラクタは引き継がれませんので注意しましょう。
それでは、子クラス(サブクラス)「MySub」に、利用するクラス「MyClass」からアクセスし、動作を確認してみましょう。継承したフィールドやメソッドも記述されていなくても、同様に利用できることがわかります。
public class MyClass {
public static void main(String[] args) {
// MySubクラスをインスタンス化
MySub mysub = new MySub();
// MySubクラスが継承したフィールドxにアクセス
System.out.println(mysub.x);
// MySubクラスが継承したメソッドgetXにアクセス
System.out.println(mysub.getX());
// MySubクラスで定義したフィールドyにアクセス
System.out.println(mysub.y);
// MySubクラスで定義したメソッドgetYにアクセス
System.out.println(mysub.getY());
}
}
実行結果は、以下のとおりとなります。
10
10
20
20
インヘリタンス(継承)を利用することにより、重複するフィールドやメソッドの記述を省略することができるため、プログラミングの効率が良くなります。
また、何か修正があった場合に、親クラスのみを修正するだけで済むなどメンテナンス性が向上したりするメリットがあります。
継承は、プログラムを効率良く作成するには、便利な手法ですが、使用には気をつけないと大変わかりにくいことになりますので、注意しましょう。
java.lang.Objectクラスは、全てのクラスの親クラスとして位置しています。
一見何も継承していないように見えるクラスでも、java.lang.Objectクラスの子クラスになります。
これはJava言語の決まりで「全てのクラスはjava.lang.Objectが自動的に継承される」ことになっているためです。
つまり、以下の2つのクラスは同じような意味を表します。
public class MyClass {
}
public class MyClass extends java.lang.Object {
}
ただしこの2つの宣言は、完全に同じではありません。
Java言語は、既に説明したとおり複数の親クラスを保持できないので、一度extendsでObjectクラスを継承してしまうと、他のクラスを継承できないことになります。
そのため、最初のMyClassでは、extendsを追加して他のクラスを継承できますが、java.lang.Objectを明示的にextendsした場合は、別のクラスの継承を追加できません。
しかし、どちらもjava.lang.Objectを継承しているという意味では同じとなります。
全てのクラスは、Objectクラスのメソッドを使うことができます。
よく使うメソッドに、toStringメソッドがあります。このメソッドは、オブジェクトの文字列表現を戻すメソッドです。
それでは、以下のサンプルソースを見てみましょう。
public class MyClass {
}
public class UseClass {
public static void main(String[] args) {
MyClass myclass = new MyClass();
String s = myclass.toString();
}
}
MyClassには何もメソッドが定義されていませんが、上記のようにjava.lang.Objectクラスで定義されているtoStringメソッドを呼び出すことができます。
つまり、MyClassが自動的にjava.lang.Objectの子クラスになっているということになります。
インスタンスメソッドでは、thisやsuperという特殊な参照型変数があります。
このthisやsuperを使い、フィールド、メソッド、コンストラクタを呼び出すことが可能です。
これから、これらの使い方を学習していきましょう。
「this」は自分自身のインスタンスの参照値を変数として持っています。thisはインスタンスメソッド内でのみ使用できます。
「this」変数は、this変数が出現するオブジェクト自身を指す特別な変数で、よく利用するケースとしては、メソッド内部で定義したローカル変数名とそのメソッド自身が属しているクラスのインスタンス変数名がバッティングしたときです。
メソッド内で使用するインスタンス変数(フィールド)とローカル変数が同じ名前の場合、ローカル変数名が優先して使用されるため、インスタンス変数(フィールド)であることを明示するために、「this」を使用します。
以下がサンプルソースです。ローカル変数xとフィールドxは、変数名が同じであるため、ローカル変数が属しているメソッドの中でフィールドを使用する場合には、「this.x」というように記述し、「このオブジェクトに定義されているフィールドx」であることを明示します。
public class MyClass2 {
public static void main(String[] args) {
// MyThis2クラスをインスタンス化
MyThis2 mythis = new MyThis2();
// MyThis2クラスのprintメソッド呼び出し
mythis.print(100);
}
}
public class MyThis2 {
// フィールドxを定義
int x = 1;
// ローカル変数xを定義
void print(int x) {
// フィールドxの値1を出力
System.out.println(this.x);
// ローカル変数xの値100を出力
System.out.println(x);
// フィールドの値1とローカル変数の値100を加算し、
// フィールドに代入
this.x = this.x + x;
// フィールドの値101を出力
System.out.println(this.x);
}
}
実行結果は、以下のとおりとなります。
1
100
101
「super」は親クラスのインスタンス参照値を変数として持っています。superもインスタンスメソッド内で使用することができます。
「super」変数は、親クラスのフィールドやメソッドを明示的に示す場合に使用します。例えば、親クラスと子クラスで同じ名前のフィールドやメソッドが定義されている場合、子クラスのオブジェクトでは、自身のフィールドやメソッドが優先的に使用されます。
子クラスから、親クラスのフィールドやメソッドに明示的にアクセスする場合には、superを使用します。
以下がサンプルソースです。
// 親クラスMySuper1を定義
public class MySuper1 {
// フィールドxを定義
int x = 100;
void print() {
System.out.println("superの" + x);
}
}
// 子クラスMySub1を定義:MySuper1を継承
public class MySub1 extends MySuper1 {
// フィールドxを定義
int x = 1;
void print() {
// 親クラスのフィールドxの値100を出力
System.out.println(super.x);
// 自身のフィールドxの値1を出力
System.out.println(x);
// 親クラスのprintメソッド呼び出し
super.print();
}
}
public class MyClass3 {
public static void main(String[] args) {
// MySub1クラスをインスタンス化
MySub1 mysub = new MySub1();
// MySub1クラスのprintメソッド呼び出し
mysub.print();
}
}
実行結果は、以下のとおりとなります。
100
1
superの100
thisメソッド「this()」は自分のクラスのコンストラクタを呼び出すことができます。
この方法を利用して、コンストラクタの中で同じクラスの他のコンストラクタを呼び出すことができ、引数の引渡しも可能です。
superメソッド「super()」は親クラスのコンストラクタを呼び出すことができます。
この方法を利用して、コンストラクタの中で親クラスのコンストラクタを呼び出すことができ、引数の引渡しも可能になります。
ただし、thisメソッド、superメソッドともに、使えるのはコンストラクタの一番最初の行だけとなります。
他の場所で使うとコンパイルエラーになりますので注意しましょう。
superメソッドの仕組みとして、クラスのインスタンスが生成されると、その親クラスのコンストラクタも呼び出されるので、コンストラクタの呼び出しが連鎖的に行なわれます。
これは、コンストラクタの中には暗黙的にsuperメソッドの「super()」という記述がコンストラクタの一番最初の行に自動的に挿入されているためです。
ただし、「super()」という処理は全てのコンストラクタに挿入されるわけではなく、以下の2つの条件に当てはまる場合のみが該当します。
つまり、明示的に自分のクラスまたは親クラスのコンストラクタの呼び出しが記述されていない場合のみ、「super()」という処理が自動的に挿入されます。
なお、継承した親クラスに引数つきのコンストラクタしかない場合には、「super()」に対応するコンストラクタが存在しないため、親クラスの引数つきコンストラクタを明示的に呼び出す必要があります。
親クラスの引数つきコンストラクタを呼ぶには、「super()」の括弧部分に対応するデータ型の引数を与えることで呼び出すことができます。
以下のサンプルソースのように明示的に親クラスのコンストラクタを呼び出すことで、連鎖的な呼び出しを保つことができます。
public class MyTest1 {
MyTest1() {
System.out.println("MyTest1のコンストラクタ");
}
}
public class MyTest2 extends MyTest1 {
MyTest2(int x){
// 暗黙的にsuper()が挿入され、MyTest1の引数なしコンストラクタが呼び出される
// super();
System.out.println("MyTest2の引数つきコンストラクタ");
}
}
public class MyTest3 extends MyTest2 {
MyTest3() {
// MyTest2クラスの引数つきコンストラクタの明示的な呼び出し
// MyTest2クラス(親クラス)に引数つきコンストラクタしかないため、この行を記述しないと、エラーになります。
super(3);
System.out.println("MyTest3のコンストラクタ");
}
}
public class MyClass4 {
public static void main(String[] args) {
MyTest3 myTest3 = new MyTest3(); // クラス MyTest3 をインスタンス化
}
}
実行結果は、以下のとおりとなります。
MyTest1のコンストラクタ
MyTest2の引数つきコンストラクタ
MyTest3のコンストラクタ
次に、「this()」を使ったサンプルソースです。
thisは自分自身の参照を表し、「this()」は自分のコンストラクタを呼び出せます。
このサンプルではmainメソッドで呼び出されているのは、MyTest5のコンストラクタのみです。
このコンストラクタの中では「this(3)」を使って、もうひとつの引数つきのコンストラクタを呼んでいます。
public class MyTest4 {
void myMsg() {
System.out.println("MyTest4のmyMsgメソッド");
}
}
public class MyTest5 extends MyTest4 {
MyTest5() {
this(3); // MyTest5 の引数つきコンストラクタの呼びだし
super.myMsg(); // MyTest4 の myMsg メソッドの呼びだし
this.myMsg(); // MyTest5 の myMsg メソッドの呼びだし
}
MyTest5(int x) {
System.out.println("MyTest5の引数つきコンストラクタ");
}
void myMsg() {
System.out.println("MyTest5のmyMsgメソッド");
}
}
public class MyClass5 {
public static void main(String[] args) {
// クラス MyTest5 をインスタンス化
MyTest5 myTest5 = new MyTest5();
}
}
実行結果は、以下のとおりとなります。
MyTest5の引数つきコンストラクタ
MyTest4のmyMsgメソッド
MyTest5のmyMsgメソッド
このように、クラスはシグネチャーの違うコンストラクタを複数定義できます(このことをオーバーロードといいます。詳しくは「ポリモフィズム」の章で説明します)。
最後に、final修飾子について説明します。
final修飾子は、継承されたくないクラスやフィールド、メソッドの前に付加することにより、継承をできなくすることができます。
また、「ポリモフィズム」の章で学習するオーバーライドもできなくなります。そのため、これ以上機能拡張や変更がされたくない場合に使います。
例えば、final修飾子がついたクラスは、親クラスになることができません。
以下のサンプルソースのように、final修飾子が付加されたMyExtendsクラスを継承しようとするとコンパイルエラーになります。
// final修飾子を付加したクラス
final public class MyExtends {
}
// final修飾子を付加したクラスは継承できないのでエラー
public class SubClass extends MyExtends {
}
親クラスでfinal修飾子がついたメソッドは、子クラスで同じメソッドを定義することはできません。
また、final修飾子がついたフィールドは、子クラスで値を変更することはできません。
public class MyExtends {
// finalフィールド
final int x = 1;
// final メソッド
final void myTemp() {
}
}
public class SubClass extends MyExtends {
void change() {
// 親クラスで finalフィールド定義されているのでエラー
x = 10;
}
// 親クラスで finalメソッド定義されているのでエラー
void myTemp(){
}
}
それでは、練習問題をやってみましょう。
ここでは、「カプセル化」の章の練習問題で作成したCatクラスとDogクラスを使用していきます。
CatクラスとDogクラスに共通に定義されているフィールドとメソッドを抜き出した親クラスとして、Petクラスを以下のとおりに作成してください。
パッケージ名 | petpac | |
---|---|---|
クラス名 | Pet | |
フィールド | private String syurui | ペットの種類 |
private String seibetsu | ペットの性別 | |
private int toshi | ペットの年 | |
コンストラクタ | public Pet() |
下記内容で各フィールドを初期化する
|
public Pet(String syurui, String seibetsu, int toshi) | 各引数でフィールドを初期化する | |
メソッド | public String getSyurui() | ペットの種類を返す |
public String getSeibetsu() | ペットの性別を返す | |
public int getToshi() | ペットの年を返す |
package petpac;
public class Pet {
// フィールド定義
private String syurui; // 生物の種類
private String seibetsu; // 生物の性別
private int toshi; // 生物の年齢
// 引数なしコンストラクタ定義(各フィールドを指定した値で初期化)
public Pet() {
syurui = "なぞの生物";
seibetsu = "性別不明";
toshi = 0;
}
// 引数ありコンストラクタ定義(各フィールドを引数で初期化)
public Pet(String syurui, String seibetsu, int toshi) {
this.syurui = syurui;
this.seibetsu = seibetsu;
this.toshi = toshi;
}
// フィールドsyuruiの値を戻すメソッド
public String getSyurui() {
return syurui;
}
// フィールドseibetsuの値を戻すメソッド
public String getSeibetsu() {
return seibetsu;
}
// フィールドtoshiの値を戻すメソッド
public int getToshi() {
return toshi;
}
}
フィールド、コンストラクタ、メソッドの定義は、各構文の形式通りに記述します。
問題1で作成したPetクラスを親クラスとして継承するように、CatクラスとDogクラスを以下のように修正してください。
なお、Petクラスは修正しないでください。
パッケージ名 | catpac | |
---|---|---|
クラス名 | Cat | |
親クラス名 | Pet | |
フィールド | Petクラスから継承するため記述は不要 | |
コンストラクタ | public Cat() |
下記内容で各フィールドを初期化する
|
public Cat(String syurui, String seibetsu, int toshi) | 親クラスPetのフィールドを各引数で初期化します。 | |
メソッド | Petクラスから継承するため記述は不要 |
パッケージ名 | dogpac | |
---|---|---|
クラス名 | Dog | |
親クラス名 | Pet | |
フィールド | Petクラスから継承するため記述は不要 | |
コンストラクタ | public Dog() |
下記内容で各フィールドを初期化する
|
public Dog(String syurui, String seibetsu, int toshi) | 親クラスPetのフィールドを各引数で初期化します。 | |
メソッド | Petクラスから継承するため記述は不要 |
CatクラスとDogクラスの修正後、shopパッケージに以下のPetShopクラスを新規で作成し、プログラムをそのまま記述して、実行結果と同様の出力結果が表示されるようにしてください。
package shop;
import catpac.Cat;
import dogpac.Dog;
import petpac.Pet;
public class PetShop {
public static void main(String[] args) {
Pet myPet = new Pet();
PetShop.buyPet(myPet);
System.out.println("---");
Cat myCat = new Cat();
PetShop.buyPet(myCat);
myCat = new Cat("アメリカンショートヘアー","オス", 0);
PetShop.buyPet(myCat);
System.out.println("---");
Dog myDog = new Dog();
PetShop.buyPet(myDog);
myDog = new Dog("ゴールデンリトリバー","オス", 0);
PetShop.buyPet(myDog);
}
private static void buyPet(Pet tPet){
System.out.println(tPet.getSyurui()
+ "の"
+ tPet.getSeibetsu()
+ "、"
+ tPet.getToshi() + "歳を買いました。");
}
}
なぞの生物の性別不明、0歳を買いました。
---
スコティッシュフォールドのメス、0歳を買いました。
アメリカンショートヘアーのオス、0歳を買いました。
---
トイプードルのメス、0歳を買いました。
ゴールデンリトリバーのオス、0歳を買いました。
※この問題の解答は掲載しておりません。Tech Fun ITスクールのJava研修では、講師が丁寧に解説しています。
使い方に不安がある場合は、教材のサンプルをもう一度動かしてみるなどして復習しておくと良いでしょう。
インヘリタンスについての説明は、以上です。