この間、Java SE 11 Programmer I (1Z0-815-JPN) 試験を受けて合格しました。
試験範囲でややこしい部分があったので、どういう場合にどのメンバを呼び出すのかまとめました。
クラスの継承の基本
まず、クラスの継承の基本です。以下のようにextends
をつけるとクラスが継承できます。
// 親クラス
class Parent {}
// 子クラス。Parentクラスを継承したChildクラス
class Child extends Parent {}
基本的には子クラスには親クラスのメンバが引き継がれます。
(private修飾子がついているメンバは引き継がれません。
また、修飾子なし(デフォルト)の場合、パッケージが異なると引き継がれません。)
オブジェクト生成の時の挙動
子クラスのオブジェクトは、親クラスとその差分のオブジェクト両方を合わせて子クラスのオブジェクトとしています。
子クラスのオブジェクトを生成するとき、親クラスのオブジェクトを生成してから差分のオブジェクトが生成されます。つまり、親クラスのコンストラクタを呼び出した後に、子クラスのコンストラクタが実行されます。
//親クラス
class Parent {
// 親クラスのコンストラクタ
public Parent() {
System.out.println("Parent Constructor");
}
}
// 子クラス。Parentクラスを継承したChildクラス
class Child extends Parent {
// 子クラスのコンストラクタ
public Child() {
System.out.println("Child Constructor");
}
}
// 実行クラス
public class Main {
public static void main(String[] args) {
// 子クラスのオブジェクト生成
Child hild = new Child();
}
}
実行結果
Parent Constructor
Child Constructor
コンストラクタ以外は宣言した変数の型で呼び出すメンバが決まる
子クラスのオブジェクトは親クラスの変数に入れることができます。
public class Main {
public static void main(String[] args) {
// 子クラスのオブジェクトを親クラスの型の変数に代入
Parent obj = new Child();
}
}
この時、オブジェクト自体は子クラスのオブジェクトが生成されるので、先ほどと同じ実行結果になります。
しかし、オブジェクトを代入している変数の型は親クラスの型なので、呼び出されるメンバは親クラスに定義したメンバになります。
//親クラス
class Parent {
// 親クラスのコンストラクタ
public Parent() {
System.out.println("Parent Constructor");
}
public void methodA() {
System.out.println("methodA");
}
}
// 子クラス。Parentクラスを継承したChildクラス
class Child extends Parent {
// 子クラスのコンストラクタ
public Child() {
System.out.println("Child Constructor");
}
public void methodB() {
System.out.println("methodB");
}
}
// 実行クラス
public class Main {
public static void main(String[] args) {
// 子クラスのオブジェクトを親クラスの型の変数に代入
Parent obj = new Child();
System.out.println();
obj.methodA();
// obj.methodB(); コンパイルエラーになる
}
}
実行結果
Parent Constructor
Child Constructor
methodA
methodBは子クラスに定義しているメソッドなので、親クラスの変数に対してmethodBを呼ぶとコンパイルエラーになります。
変数の型を子クラスの方にするとmethodBも呼び出すことができます。
public class Main {
public static void main(String[] args) {
// 子クラスのオブジェクトを子クラスの型の変数に代入
Child obj = new Child();
System.out.println();
obj.methodA();
obj.methodB();
}
}
実行結果
Parent Constructor
Child Constructor
methodA
methodB
オーバーライドした場合
上記まではまだ分かりやすいと思いますが、ややこしいのはオーバーライドしている場合です。
上記のコードのmethodAを子クラスでオーバーライドしてみます。
インスタンスメソッドのオーバーライド
//親クラス
class Parent {
// 親クラスのコンストラクタ
public Parent() {
System.out.println("Parent Constructor");
}
public void methodA() {
System.out.println("methodA");
}
}
// 子クラス。Parentクラスを継承したChildクラス
class Child extends Parent {
// 子クラスのコンストラクタ
public Child() {
System.out.println("Child Constructor");
}
// methodAをオーバーライド
public void methodA() {
System.out.println("methodB");
}
}
// 実行クラス
public class Main {
public static void main(String[] args) {
// 子クラスのオブジェクトを子クラスの型の変数に代入
Child obj1 = new Child();
// 子クラスのオブジェクトを親クラスの型の変数に代入
Parent obj2 = new Child();
System.out.println();
System.out.println("obj1に対してmethodAの呼び出し");
obj1.methodA();
System.out.println("obj2に対してmethodAの呼び出し");
obj2.methodA();
}
}
実行結果
Parent Constructor
Child Constructor
Parent Constructor
Child Constructor
obj1に対してmethodAの呼び出し
methodB
obj2に対してmethodAの呼び出し
methodB
メソッドがオーバーライドされている場合は、変数の型が親クラスでも子クラスでオーバーライドしたメソッドが呼び出されます。
ただし、これはオーバーライドしている(されている)メソッドがインスタンスメソッドの場合のみです。
クラスメソッドがオーバーライドされたパターンを見てみましょう。
クラスメソッドのオーバーライド
//親クラス
class Parent {
// 親クラスのコンストラクタ
public Parent() {
System.out.println("Parent Constructor");
}
static public void methodA() {
System.out.println("methodA");
}
}
// 子クラス。Parentクラスを継承したChildクラス
class Child extends Parent {
// 子クラスのコンストラクタ
public Child() {
System.out.println("Child Constructor");
}
// methodAをオーバーライド
static public void methodA() {
System.out.println("methodB");
}
}
// 実行クラス
public class Main {
public static void main(String[] args) {
// 子クラスのオブジェクトを子クラスの型の変数に代入
Child obj1 = new Child();
// 子クラスのオブジェクトを親クラスの型の変数に代入
Parent obj2 = new Child();
System.out.println();
System.out.println("obj1に対してmethodAの呼び出し");
obj1.methodA();
System.out.println("obj2に対してmethodAの呼び出し");
obj2.methodA();
}
}
実行結果
Parent Constructor
Child Constructor
Parent Constructor
Child Constructor
obj1に対してmethodAの呼び出し
methodB
obj2に対してmethodAの呼び出し
methodA
上記のようにクラスメソッドがオーバーライドされた場合は、宣言した変数の型で定義した方が呼び出されます。
ちなみに、変数のパターンも把握しておきましょう。
子クラスに同じ名前でフィールド変数を命名した場合
//親クラス
class Parent {
static String class_var = "親クラスのクラス変数";
String instance_var = "親クラスのインスタンス変数";
}
// 子クラス。Parentクラスを継承したChildクラス
class Child extends Parent {
static String class_var = "子クラスのクラス変数";
String instance_var = "子クラスのインスタンス変数";
}
// 実行クラス
public class Main {
public static void main(String[] args) {
// 子クラスのオブジェクトを子クラスの型の変数に代入
Child obj1 = new Child();
// 子クラスのオブジェクトを親クラスの型の変数に代入
Parent obj2 = new Child();
// 各変数の呼び出し
System.out.println(obj1.class_var);
System.out.println(obj1.instance_var);
System.out.println(obj2.class_var);
System.out.println(obj2.instance_var);
}
}
実行結果
子クラスのクラス変数
子クラスのインスタンス変数
親クラスのクラス変数
親クラスのインスタンス変数
子クラスで親クラスと同じ名前のフィールド変数を宣言することは可能です。
また、冒頭に書いたように子クラスのオブジェクトは親クラスのオブジェクトとその差分のオブジェクトで構成されているため、2つのオブジェクトに同じ名前で異なるフィールドが存在できます。
呼び出される変数はクラス変数、インスタンス変数問わず宣言した変数の型に定義されている方が呼び出されます。
まとめ
平たくまとめると以下のようになります。
- 子クラスのオブジェクトは親クラス型の変数に代入することができる
- 呼び出されるメンバは変数の型に定義されたメンバが呼び出される
- ただし、インスタンスメソッドがオーバーライドされていれば、親クラス型の変数に代入されていても子クラスのオーバーライドしたメソッドが呼び出される
試験対策の一番は黒本などで問題を繰り返し解くことですが、問題の解説だけでは少し整理しにくい部分をまとめてみました。
今回はアクセス修飾子には触れていませんが、アクセス修飾子によっても呼び出される(アクセスできる)メンバは変わります。
コードは色々なパターンの組み合わせですので、色んなことを考慮に入れることが必要がありますが試験対策の一つとして役立ったら幸いです。