00. 始めに
Javaの継承があまりにも分かっていなかった筆者です。
何となく分かっていた気になったつもりでしたが、何となく分かっていなかった訳ですね、はい。
本記事では、私個人が経験した「分かっている」ようで、「分かっていなかった」、Javaの継承について紹介したいと思います。
Javaの継承を理解するのに、少しでもお役に立てれば幸いです。
※ 注意
1.本記事は、Java Silver8 の資格取得に向けた学習の一環として書かれたものです。そのため、試験対象外の機能に関しては記載されていないことがあります。
2.本記事では、インターフェースは登場しません(今のところ、その予定です)。
3.やたら「分かっていない」という表現が出てきますが、あくまで試験問題を解く上での話です1。
01. 要約
coming soon
02. そもそも継承ってなんだっけ?
継承とは、**「既存のクラスを元に新たなクラスを作成(定義)する」**ことです。この時、
・継承元のクラス:スーパークラス or 基底クラス
・継承先のクラス:サブクラス or 派生クラス
と呼びます(この記事では、スーパークラス・サブクラス、という表現を使っています)。
下の例では、Mammals(哺乳類)クラスから、Cat(猫)クラスを作成しています。
// スーパークラス
class Mammals { }
// サブクラス
class Cat extends Mammals { }
継承のやり方は以上です。カンタンですね!
. . . そんな事は分かっているんです!ただ、継承をしたことで何が起きるか、意外と分かっていないもんです(特に私)。
次章からは個人的に注目している4つの観点
1.継承の基本ルール
2.継承したクラスのキャスト
3.継承とアクセッサ
4.継承したクラスのコンストラクタ
に分けて継承について紹介していきます。
その1:継承の基本ルール
1.1 継承できるスーパークラスは、1つだけ
例えば、↓のような宣言はコンパイルエラーになります。
// スーパークラス その1
class Mammals { }
// スーパークラス その2
class Reptiles { }
// サブクラス
class Cat extends Mammals, Reptiles { }
1.2 抽象メソッドは具象クラス内でオーバーライド必須
次のコードをご覧ください。
抽象クラスMammalsに抽象メソッドeact, walk, 具象メソッドsleepがあります。
またMammalsを継承したCatクラスでは、walk, sleepメソッドをオーバーライドしています。
public abstract class Mammals {
public abstract void eat();
public abstract void walk();
public void sleep() {}
}
public class Cat extends Mammals {
public void walk() {}
public void sleep() {}
}
さて、↑のコードですが、コンパイルするとエラーになります(Eclipse, VSCodeなどのエディタで見れば、真っ赤っ赤になっていること間違いなしです)。
理由はお察しの通り、抽象クラスの抽象メソッドを具象クラス内でオーバーライドしていないからです。
今回だと、Mammalsのeat()をCat内でオーバーロードしていないために、コンパイルエラーになります。抽象メソッドは全てオーバーライドしましょう2。
その2:継承したクラスのキャスト
2.1 継承関係にないクラスへのキャストは、コンパイルエラー
以下のコードでは、Mammals(哺乳類)クラスを継承して、Cat(猫)クラスとDogクラスを作成しています。
class Mammals { } // 哺乳類クラス
class Cat extends Mammals { } // 猫クラス
class Dog extends Mammals { } // 犬クラス
public class Main {
public static void main(String[] args) {
Cat catA = new Cat();
Dog dogA = new Dog();
// Cat型のインスタンスをMammals型にキャスト
Mammals mammals = (Mammals) catA; // OK
// Cat型のインスタンスをDog型にキャスト
Dog dogB = (Dog) catA; // NG コンパイルエラー
}
}
Javaはコンパイル時にキャスト可能かどうか(= 継承関係のあるクラスにキャストしているか)もチェックしています。そのため、実行時エラーではなくコンパイルエラーなんですね!
継承に限らず、一般にキャストできないようなコードはコンパイルエラーになります。
その3:継承とアクセッサ
3.1 継承したクラスのメソッドは、公開範囲を厳しく出来ない
次のコードは、コンパイルエラーになります。
Mammalsクラスを継承して、Catクラスでeatメソッドをオーバーライドする時、元のeatメソッドより公開範囲を厳しくしたことが原因です。
class Mammals {
void eat() {}
}
class Cat extends Mammals {
private void eat() {}
}
アクセッサについて<参考1 p175より一部記載を変えて引用>
メソッドのアクセッサ(アクセス修飾子)は、主に以下が良く使われます。
// 厳しい ← 公開範囲 → 緩い
private protected /*(アクセッサなし)*/ public
アクセッサ | 説明 |
---|---|
private | 同一クラス内からのみ利用可能 |
protected | 同一パッケージのクラス or 対象のクラスを継承したクラスから利用可能 |
(アクセッサなし) | 同一パッケージのクラスからのみ利用可能 |
public | どのクラスからでも利用可能 |
その4:継承クラスのコンストラクタ(意外と手ごわい)
4.1 継承でスーパークラスから引き継げないもの
継承しても引き継げないのは、
privateなフィールド、メソッド(コンストラクタも)
です!
4.2 コンストラクタの実行順番に注意
上の方で、「継承してもコンストラクタは引き継げない」と言いましたが、注意すべきことがあります。それは、サブクラスのコンストラクタが実行される前に、スーパークラスのコンストラクタが実行されることです。
次のコードをご覧ください。
A, B, Cというクラスがあり、CはBを、BはAを継承しています。そしてそれぞれのコンストラクタでは、クラス名をコンソールに出力するようになっています(コードは参考2より、一部形式を変えて引用)。
class A {
public A() {
System.out.print("A");
}
}
class B extends A {
public B() {
System.out.print("B");
}
}
class C extends B {
public C() {
System.out.print("C");
}
}
public class Main {
public static void main(String[] args) {
new C(); // ABC
}
}
そして、このmain()を実行するとどうなるか、おわかりいただけただろうか . . .
そう、A, B, Cのコンストラクタが全て実行されているのです!!
. . . すみません、大げさに言いました3。
一体全体、どうしてそうなるかの説明ですが、参考3 p.429から引用したいと思います。
実は、Javaでは、「すべてのコンストラクタは、その先頭で必ず内部インスタンス部( = 親クラス)のコンストラクタを呼び出さなければならない」というルールになっています。
4
つまり、さっきのコードの、本当の姿と実行順番を考えると、
class A {
public A() {
System.out.print("A"); // 3
}
}
class B extends A {
public B() {
super(); // Aのコンストラクタを呼び出す 2
System.out.print("B"); // 4
}
}
class C extends B {
public C() {
super(); // Bのコンストラクタを呼び出す 1
System.out.print("C"); // 5
}
}
public class Main {
public static void main(String[] args) {
new C(); // C をインスタンス化 0
}
}
C()が呼ばれた時に、B()が実行されて、B()の内部でA()を呼んで . . . という風に、継承元のクラスが別のクラスを継承していたら、その継承先のクラスのコンストラクタを先に実行しようとします。
また、サブクラスの内部で、スーパークラスクラスのコンストラクタを呼び出していなかったときは、コンパイラが自動的にsuper()
を追加してくれます56!
ただし、
・明示的にスーパークラスのコンストラクタを呼び出す(super()
とか、super(str)
とか)
・オーバーロードしたコンストラクタを呼び出す(this()
とかthis(str)
とか)
のどっちかやっていれば、コンパイラーは勝手にsuper()
って追加しません。
サブクラスのコンストラクタで、スーパークラスのコンストラクタが呼ばれる . . .
そのことを知っていても、こう、いくつもいくつも継承されたクラスのコンストラクタを呼ばれたんじゃ、面食らっちゃいますよねw
4.3 こんなコンストラクタ呼び出しはダメだ
「02. コンストラクタの実行順番に注意」に関連して、こんなひっかけも登場します。それは、
スーパークラスのコンストラクタ2重呼び出しするようなコードは、コンパイルエラーになる
ということです。
まずは、次のコードをご覧ください!(参考2 p.362-363 より、形式を一部変更して引用)
public class Mammals {
String name;
int age;
public Mammals(String name, int age) {
this.name = name;
this.age = age;
}
}
class Cat extends Mammals {
String color
publip Cat(String color) {
this.color = color;
}
public Cat(String name, int num, String color) {
super(name, num);
this(color);
}
}
public class Main {
public static void main(String[] args) {
Cat cat1 = new Cat("white");
Cat cat2 = new Cat("mike", 2, "Black");
System.out.println(cat1.name + ", " + "" + "")
}
}
さあ、このコード、コンパイラしたらどうなると思います?
正解は、猫クラス
の3, 8行目でコンパイルエラーになります!
その理由を考えるため、このコードが実行されたと想定して、生成されるインスタンス毎に処理を追って行きます!
①cat1
インスタンスが生成される処理
Cat cat1 = new Cat("white");
のように引数が1つなので、publip Cat(String color)
が実行。
. . . の前に、CatはMammalsを継承しているため、Mammalsのコンストラクタが実行されます。
String color
publip Cat(String color) {
super(); // コンパイル時に自動追加
this.color = color;
}
しかし、Mammalsクラスには、引数なしのコンストラクタpublic Mammals();
がありません!
存在しないメソッドを実行しようとしているとして、publip Cat(String color)
を記載した行でエラーになります7。
②cat2
インスタンスが生成される処理
今度は引数を3つ与えてインスタンスを生成しています。
よって実行されるのは、対応する下記のコンストラクタ!
public Cat(String name, int num, String color) {
super(name, num);
this(color);
}
処理としては、まずスーパークラスのコンストラクタpublip Cat(String color)
を実行しています。これは存在するので特に問題ないです。
しかし次の行のコード、今度はCatクラスのコンストラクタpublip Cat(String color)
を実行してます。
①の時と同じエラーになるかと思いきや、別種のコンパイルエラーに引っかかります。
理由は、スーパークラスのコンストラクタを2回呼び出すことになるからです。
Javaでは、スーパークラスのコンストラクタを呼び出し後に、オーバーロードしたコンストラクタを呼び出すことが出来ないようになっています。
8
今回は、publip Cat(String color)
がそのオーバーロードしたコンストラクタです。super();
の後にこれが実行されたため、エラーになってしまいます。
実際の試験においては、コンストラクタ内部で、
super();
this();
ってコードをみたら、this();
の部分でコンパイルエラーになる!って覚えておけば良さそうですね!
05. 最後に
最後まで読んで下さり、ありがとうございました。
06. 参考
1.山本道子 (2015) 「Java プログラマ Silver SE 7(第5刷発行)」 ㈱翔泳社 発行
2.志賀澄人 (2019) 「徹底攻略 Java SE 8 Silver 問題集 [1Z0-808] 対応」 ㈱インプレス 発行
3.中山清喬/国本大吾 (2018) 「スッキリわかるJava入門 第2版」 ㈱インプレス 発行
-
普段はエディタやコンパイラが教えてくれるから、例え分かっていなくても問題ないけど、自力で気づかないといけないとなると、そうも言ってられないよね!って意味です。 ↩
-
余談ですが、抽象メソッドは抽象クラスにしか定義できません。もしCatクラスでメソッドを実装しないのなら、Catクラスもabctract付けておくのが逃げ道です。 ↩
-
大げさに書きました。 ↩
-
参考3 p.429より ↩
-
勝手にコードが現れるとかではないです。あくまでsuper()で書かれている体でコードがコンパイラされるだけです。 ↩
-
そもそもなせ、スーパークラスのコンストラクタを実行しなければならないかの解釈ですが、コンストラクタがそのクラスを生成するためメソッドだとすれば、サブクラスを生成する前に、そのベースとなっているクラス(= スーパークラス)から用意しないといけない、ってところでしょうか。 ↩
-
Javaコンパイラは、実行しようとしているメソッドが存在するかもチェックしているため、コンパイルエラー扱いなんですね! ↩
-
参考2 p.416より ↩