Java Generics Hell - 内部クラスと型変数のスコープ

Java Generics Hell アドベントカレンダー 19日目。

読者の推奨スキルとしてはOCJP Silverぐらいを想定している。

Javaでは1ファイルにトップレベルのpublicなclassはひとつしか置けないが、入れ子になったクラスなどを用意することができる。種類がいくつかあるので後に整理するが、内部クラスの場合、外側のインスタンススコープの型変数が内部クラスの内側でも有効となる。

今回はそのあたりを整理してみよう。Javaのクラス宣言の種類については拙稿 Javaのクラス宣言5種+αで以前に取り上げているので参考にされたい。

static

内部クラスの話の前に、そもそも論としてクラスのインスタンススコープの型変数は、staticなメソッドや、フィールドでは用いることは出来ない。なぜなら、インスタンスをスコープとしているからだ。(トートロジー的な)

なお、コンストラクタはインスタンスメソッド扱いで型変数を使える。その他、static初期化ブロックはstatic扱いなので型変数を使えない。インスタンス初期化ブロックであればインスタンスの型変数を用いることができる。

トップレベルの同居したクラス

Javaではpublic/protectedなトップレベルのclassは .java ファイルにひとつしか置けないが、パッケージプライベートなclassは複数置くことができる。なお、privateなトップレベルクラスはそもそも作れない。例えばTopLevel.javaというファイルに以下の内容を記述するとコンパイルすることが出来、TopLevel.class と TopLevel2.class が作られる。

public class TopLevel<T> {
}
class TopLevel2 {
}

上記例のTopLevel2はトップレベルなので、当然ながら相互に型変数のスコープは独立している。つまり、考慮しなくて良い。

ネストしたクラス

ネストしたクラス (Nested class) は、class 内部に

  • static class を宣言したもの
  • interface を宣言したもの
  • enum を宣言したもの

である。

これらは、名前が「外側のクラス名. ネストしたクラス名」で扱われるが、基本的にトップレベルクラスと同等である。

public class TopLevel<T> {
	static class Nest1 {
	}
	interface Nest2 {
	}
	enum Nest3 {
	}
}

違いといえば、アクセスレベルをパッケージプライベートやprivateにすることができる。private宣言すると内部的にclassを作るものの、外部に露出させないことが可能だ。

これらもトップレベルクラスと同等で、外側のTopLevelの型変数Tをこれらネストしたクラス内で用いることは出来ない。

内部クラス

内部クラス(Inner class)は外部クラスのインスタンスに紐付く。そのため、外側のクラスのインスタンスの型変数を用いることができる。

public class TopLevel<T> {
	class Inner {
		T t;
		void hoge(T t) {}
	}
}

しかし、内部クラスといえども、内部クラスのstaticメソッドではTopLevelのTを扱うことは出来ない。

なお、内部クラスが3段以上になった場合、3段目では、1番外側のクラスで宣言された型変数、2段目で宣言された型変数の両方を用いることができる。実用上はそういうケースはなかなか発生しないとは思うが。

ローカル内部クラス

ローカル内部クラス(Local inner class)はメソッドやコンストラクタなどのブロック内で宣言されるクラス。このクラスはブロックがスコープとなるのだが、そのブロックで有効な外側の型変数はこのクラス内でも用いることができる。

public class TopLevel<T> {
	/** staticメソッド */
	static <S> void staticMethod() {
		class LocalInnerClass {
			S s; // メソッドスコープのSは使える
			// T t; インスタンススコープのTはNG
		}
	}
	/** インスタンスメソッド */
	<I> void instanceMethod() {
		class LocalInnerClass {
			I i; // メソッドスコープのIが使える
			T t; // インスタンススコープのTも使える
		}
	}
}

上記例では、staticメソッドではメソッドスコープのSは使えるが、インスタンススコープのTは使えない。対して、インスタンスメソッドでは両方を用いることができる。

無名クラス

無名クラス(匿名クラスとも呼ばれる)(Anonymous class)は主に抽象クラスやinterfaceなどに対して名前を付けずにその場で実装を書くもの。
これもローカル内部クラス同様にそのブロックで有効な外側の型変数はこのクラス内でも用いることができる。

public class TopLevel<T> {
	/** staticメソッド */
	static <S> void staticMethod() {
		Runnable run = new Runnable(){
			S s; // メソッドスコープのSは使える
			// T t; インスタンススコープのTはNG
			@Override
			public void run() {
				S s2;
			}
		};
	}
	/** インスタンスメソッド */
	<I> void instanceMethod() {
		Runnable run = new Runnable(){
			I i; // メソッドスコープのIが使える
			T t; // インスタンススコープのTも使える
			@Override
			public void run() {
				I i2;
				T t2;
			}
		};
	}
}

余談

ローカル内部クラスや無名クラスの中に内部クラスを作ると、外側のブロックで有効なメソッドスコープの型変数を内部クラスで用いることができる。

public class TopLevel<T> {
	/** staticメソッド */
	static <S> void staticMethod() {
		/** ローカル内部クラス */
		class LocalInnerClass {
			/** 内部クラス */
			class InnerClass {
				S s;
			}
		}
	}
}

内部クラスは、外側のクラスで利用可能な型変数は使える、ぐらいに理解しておくと良いだろう。え?こんな使い方することはないって?