WisdomSoft - for your serial experiences.

5.4 隠蔽とオーバーライド

継承したサブクラスが、スーパークラスと同じ名前のフィールドやメソッドを宣言したときの動作について説明します。

5.4.1 フィールドの隠蔽

クラスを継承すると、スーパークラスのメンバとサブクラスのメンバの識別子が衝突することがしばしば考えられます。継承関係にあるクラス間でメンバの識別子が衝突した場合、自分自身のメンバを優先してアクセスすると定められているため、コンパイル・エラーにはなりません。サブクラスのメンバがスーパークラスのメンバよりも優先され、その結果スーパークラスのメンバが見えなくなります。この作用を隠蔽と呼びます。

クラス変数の識別子が衝突した場合の解釈は単純で、アクセス式で明示的にクラスを指定すればよいのです。クラス変数は、インスタンスではなくクラスに関連付けられるため、クラス名からアクセスできるというのは承知でしょう。これを利用するのです。

コード1
class A { 
	public static String str = "Kitty on your lap";
}

class B extends A {
	public static String str = A.str + "~あなたのひざの仔猫~";
	public void show() {
		System.out.println(str);
	}
}

class Test {
	public static void main(String args[]) {
		new B().show();

		A ab = new B();
		System.out.println(ab.str);
	}
}
実行結果
>java Test
Kitty on your lap~あなたのひざの仔猫~
Kitty on your lap

コード1の A クラスと、これを継承する B クラスでは同じ識別子を持つ str 公開フィールドが宣言されています。B クラス内の文では、単純に str だけを指定すると、自分自身のフィールドが優先されます。そのため、A クラスの str フィールドにアクセスするには、明示的に A.str と記述しなければなりません。これは、B クラスの str フィールドの初期化と show() インスタンス・メソッドで指定されている str の違いを見ればわかります。

それとは別に、インスタンスからクラス変数にアクセスすることも可能でした。この場合、インスタンスへの参照を格納した参照型変数の型によってアクセスするべきフィールドが決定されます。B クラス型の変数から str フィールドにアクセスすれば B.str にアクセスしますが、main() メソッドで生成した B クラスのインスタンスを A クラス型の変数 ab に代入変換しています。そのため、ab.str は A クラスの str フィールドにアクセスします。

しかし、インスタンス変数が隠蔽された場合は、クラス名からアクセスができないため、サブクラスからスーパークラスの隠蔽されたメンバへのアクセス手段が問題となります。アクセス対象となるメンバはオブジェクトの型で識別されるため、上位クラスの隠蔽されたフィールドにアクセスするには、一度ワイドニング変換をしなければならないことになります。つまり、次のように記述します。

((基本クラスの型)this) . メンバ

キャスト演算子によってワイドニング変換された this は、基本クラスの型を表すため、これで隠蔽された基本クラスのメンバにアクセスすることができます。しかし、そのたびにキャスト演算子を使って変換するのは冗長です。そこで super キーワードを使って直接の基本クラスのメンバにアクセスすることができます。

基本クラスのメンバへのアクセス
superメンバ

super は this と同様にインスタンス・メソッドなど、静的コンテキスト以外の文で使用することができます。super キーワードを使えば、明示的にサブクラスから直接の基本クラスのメンバにアクセスすることができます。

コード2
class A { 
	public String str = "Kitty on your lap";
}

class B extends A {
	public String str = super.str + "~あなたのひざの仔猫~";
}

class Test {
	public static void main(String args[]) {
		B bb = new B();
		A ab = bb;

		System.out.println(bb.str);
		System.out.println(ab.str);
	}
}
実行結果
>java Test
Kitty on your lap~あなたのひざの仔猫~
Kitty on your lap

コード2の B クラスは、スーパークラス A の str インスタンス変数を隠蔽しています。このプログラムでは、B クラスのインスタンス変数の初期化で、A クラスの隠蔽した str フィールドにアクセスするために super を用いています。これは ((A)this).str と記述しても同じ効果が得られますが、super を用いたほうが効率的で間違いがありません。

5.4.2 メソッドの隠蔽

サブクラスのクラス・メソッドがスーパークラスのクラス・メソッドと同じシグネチャを持つ場合、スーパークラスのメソッドはその宣言によって隠蔽されてしまいます。基本的な概念はフィールドの隠蔽と同じですが、メソッドの場合はクラス・メソッドがインスタンス・メソッドを隠蔽できない点に注意が必要です。クラス・メソッドがインスタンス・メソッドを隠蔽しようとすると、コンパイル・エラーとなります。

また、フィールドとは異なりシグネチャの一致が隠蔽の条件となります。識別子だけの衝突であれば、オーバーロードと判断されるだけで、隠蔽されることはありません。また、戻り値の一致も隠蔽の条件となります。フィールドの隠蔽は型の異なるフィールドやクラス変数がインスタンス変数を隠蔽することができましたが、メソッドでは厳密に一致していなければ隠蔽できません。

隠蔽されたメソッドにアクセスするには、対象のクラス・メソッドを宣言するクラス名を指定した限定名を使います。変数からアクセスする場合は、その変数の型を目的のクラス型に変換しなければなりません。

コード3
class A {
	public static void show() {
		System.out.println("Kitty on your lap");
	}
}

class B extends A {
	public static void show() {
		A.show();
		System.out.println("\t~あなたのひざの仔猫~");
	}
}

class Test {
	public static void main(String args[]) {
		((A)new B()).show();
		new B().show();
	}
}
実行結果
>java Test
Kitty on your lap
Kitty on your lap
        ~あなたのひざの仔猫~

コード3のサブクラス B はスーパークラス A の show() クラス・メソッドを隠蔽しています。隠蔽されたメソッドは B クラス内の文からアクセスする場合、限定名を指定しなければなりません。B クラス内の文が show() メソッドを単純名で指定した場合は、B.show() が呼び出されてしまうからです。

オブジェクトからアクセスする場合は、変数の型によって呼び出されるメソッドが決定されています。main() メソッド内で、A クラス型にキャスト演算子で変換した B クラスのオブジェクトから show() を呼び出しています。結果は、A.show() が呼び出されます。フィールドの隠蔽同様に、識別子が隠蔽された場合、どのメンバを呼び出すかは参照型で決定されます。

5.4.3 メソッドのオーバーライド

サブクラスのインスタンス・メソッドが、スーパークラスの公開されているインスタンス・メソッドと同じシグネチャの場合、メソッドはオーバーライドされたと表現します。インスタンス・メソッドの衝突は、隠蔽ではありません。

隠蔽は、サブクラスによってスーパークラスの識別子が隠されるというもので、これは、ソースコード上の名前の問題でした。限定名を用いたり、参照型変数の型を目的のクラス型にキャストすることで、隠蔽されたメンバにアクセスすることができます。しかし、メソッドのオーバーライドは隠蔽と異なり、完全にスーパークラスのメソッドを支配してしまいます。

メソッドがサブクラスによってオーバーライドされた場合、外部からスーパークラスのメソッドにアクセスすることはできなくなります。たとえ変数をスーパークラスの型に変換しても、呼び出されるのはインスタンスのオーバーライドしたメソッドです。つまり、インスタンス・メソッドの呼び出しは、変数の型が何であれインスタンスの実質的な型が宣言するメソッドが起動します。オーバーライドされたスーパークラスのメソッドにアクセスできるのは、サブクラスから super キーワードを使って呼び出されたときのみとなります。

コード4
class A {
	public void show() {
		System.out.println("Kitty on your lap");
	}
}
class B extends A {
	public void show() {
		super.show();
		System.out.println("\t~あなたのひざの仔猫~");
	}
}

class Test {
	public static void main(String args[]) {
		B bb = new B();
		A ab = bb;

		bb.show();
		ab.show();
	}
}
実行結果
>java Test
Kitty on your lap
        ~あなたのひざの仔猫~
Kitty on your lap
        ~あなたのひざの仔猫~

コード4はメソッドのオーバーライドの効果を証明するプログラムです。クラス A の show() メソッドは、サブクラス B の show() メソッドによってオーバーライドされています。オーバーライドされた A クラスの show() メソッドは B クラスによって完全に覆い隠されてしまいます。B クラスのインスタンスが持つ show() インスタンス・メソッドの情報は常に B クラスの show() メソッドであり、インスタンスを参照する変数の型を A クラス型にワイドニング変換したとしても、呼び出されるのは B クラスの show() メソッドです。実行結果をみれば、B クラスの show() メソッドしか呼び出されていないことを確認できます。

そのため、オーバーライドされたスーパークラスのメソッドにアクセスできるのは、サブクラスから super を用いたメソッド起動式だけなのです。この、オーバーライドという機能は、クラスを継承したときに、スーパークラスを拡張する重要な存在です。