WisdomSoft - for your serial experiences.

6.3 内部クラス

クラスの内部で宣言されたクラスを内部クラスと呼びます。内部クラスは囲んでいるクラスのインスタンスと関連付けられながらも独立したオブジェクトとして振る舞います。

6.3.1 メンバ・クラス

Java 言語の構文の中でも特に複雑で難しく、オブジェクト指向の上級者向けの仕様に内部クラスが挙げられます。内部クラスとは、これまでのようなトップレベルの宣言ではなく、クラスの中で宣言されるクラスのことを表します。内部クラスはプログラム中に多用されるようなものではなく、ごく一部の特殊な構造をもつクラス関係をスマートに実現するために上級プログラマが利用します。

内部クラスはいくつかに分類することができますが、この場では特に基本であるメンバ・クラスを解説しましょう。メンバ・クラスとは、クラスのメンバとして宣言された内部クラスのことです。このクラスは特殊な性質を持っており、インスタンスの中に新しいインスタンスが存在します。メンバ・クラスは次のように宣言します。

class トップクラス名 {
	クラス修飾子 class メンバ・クラス名 {
		...
	}
}

トップレベルのクラス宣言(パッケージ・メンバとしてのクラス宣言)では、クラスの修飾子は abstract と public しか指定することができません。しかし、メンバ・クラスはメソッドやフィールドなどのように、protected や private などのアクセス修飾子を指定することも可能です。ただし static を指定した場合は内部クラスとは呼びません。内部クラスと定義できるメンバ・クラスは非 static メンバ・クラスのみです。

内部クラスの中に、さらに内部クラスを宣言することも問題はありません。もちろん、そのようなプログラムを書いても無意味に複雑になるだけなので、賢明な設計者は避けるでしょう。クラスの中で宣言された内部クラスの中でも、特に自分のメンバとして宣言されている内部クラスのことを直接の内部クラスと呼びます。一方、内部クラスにとって、自分をメンバとするクラスのことを囲んでいるクラスと表現します。

メンバ・クラスの概念はオブジェクト指向の経験がある者でも首を傾げるほど厄介です。メンバ・クラスは、単純にクラスのスコープ内で宣言されたクラスというわけではなく、密接に囲んでいるクラスのインスタンスと関連してしまうのです。暗黙的に囲んでいるクラスのインスタンスも保有するため、囲んでいるクラスのメンバに単純名でアクセスすることができます。

メンバ・クラスのインスタンスは囲んでいるクラスのインスタンスと関連します。そのため、メンバ・クラスのインスタンスを作成するには、まず、メンバ・クラスを囲んでいるクラスのインスタンスを生成しなければなりません。メンバ・クラスを囲んでいるクラスからは、単純名でクラスを参照することができ、これまでのようにインスタンスを生成することができます。しかし、外部からメンバ・クラスを参照するには、メンバ・クラスを囲んでいるクラスを通す必要があります。メンバ・クラスの型の参照方法と、クラス・インスタンス生成式は次のような形になるでしょう。

内部クラスのインスタンス生成式
インスタンス . new メンバ・クラスのコンストラクタ;

重要なのは、クラス・インスタンス生成式において new キーワードの前にメンバ・クラスを保有するインスタンスを指定していることです。この構文を用いた場合、新しく生成されるメンバ・クラスのインスタンスは、new キーワードで指定された囲んでいるクラスのインスタンスによって生成されます。

コード1
class Parent {
	public String str;
	public Child getChild() {
		Child temp = new Child();
		return temp;
	}
	class Child {
		public void show() {
			System.out.println(str);
		}	
	}
}

class Test {
	public static void main(String args[]) {
		Parent p = new Parent();
		Parent.Child c = p.new Child();

		p.str = "Kitty on your lap";
		c.show();
		p.getChild().show();
	}
}
実行結果
>java Test
Kitty on your lap
Kitty on your lap

コード1は、メンバ・クラスの動作を確かめるためのプログラムです。メンバ・クラスのインスタンスが、囲んでいるクラスのインスタンスを暗黙的に参照していることを証明する興味深い実行結果が得られます。

このプログラムでは、Parent クラスのメンバに、メンバ・クラス Child が宣言されています。Parent クラスからは静的コンテキストを除いて、内部クラスを単純名だけで参照することができます。インスタンス this を保有するコードからならば、暗黙的にそれを利用してメンバ・クラスのインスタンスを生成できるのです。

しかし、外部からメンバ・クラスにアクセスするのは容易ではありません。Test クラスでは Child クラスのインスタンスを取得するために、まず Parent クラスのインスタンスを生成し、その後、

Parent.Child c = p.new Child();

という文でメンバ・クラス Child のインスタンスを生成しています。メンバ・クラスのインスタンスを生成するには、それを囲んでいるクラスのインスタンスが必須となるため、クラス・インスタンス生成式では new キーワードの前に Parent クラスのインスタンス p を指定しています。新しいインスタンスは、この p を利用して作成されます。また、Child クラス型を参照するための限定名が Parent . Child となっていることにも注目してください。

内部クラスは、囲んでいるクラスのインスタンスと関連しているという特殊な状態を除けば、その他の機能は通常のトップレベルで宣言されたクラスと同じです。メソッドを宣言したり、フィールドを保有することができます。ただし、囲んでいるクラスのインスタンスと関連する束縛があるため static メンバ及び静的初期化子を宣言することができません。例外として static final 修飾子を指定した定数フィールドは許されます。

class O {
	class C {
		public static final int X = 10;	//OK
		public static int Y = 10;		//コンパイルエラー
	}
}

ここで気になるのは、内部クラスが囲んでいるクラスのメンバ宣言と衝突した場合です。内部クラスは囲んでいるクラスのインスタンスと関連するため、単純名で囲んでいるクラスのフィールドやメソッドにアクセスすることができましたが、メンバが衝突した場合は囲んでいるクラスのメンバが隠蔽されます。

コード2
class StaticField {
	public static String str = "StaticField";
}

class Parent {
	public String str = "Parent";
	public class Child extends StaticField {
		public String str = "Child";
		public void showStr() {
			System.out.println(str);
		}
	}
}

class Test {
	public static void main(String args[]) {
		Parent.Child c = new Parent() . new Child();
		c.showStr();
	}
}
実行結果
>java Test
Child

コード2では、Parent クラスとその内部クラス Child で str フィールドが衝突しています。通常ならば Child クラスの内部から Parent クラスの str フィールドを参照することができましたが、Parent クラスの str フィールドが Child クラスの str フィールド宣言によって隠蔽されているため、表示される結果は Child クラスの str フィールドであることが実行結果から確認できます。

因みに、今回のプログラムでは Child クラスが StaticField クラスを継承しています。興味深いことに、内部クラスは static メンバを保有することができないにもかかわらず、この継承は有効です。Child クラスの showStr() メソッドで str を表示してますが、println() メソッドに渡す str フィールドを、限定名で super . str に変更すれば、スーパークラスである StaticField クラスの str フィールドが表示されます。