Javaのクラス宣言5種+α

Javaのクラス宣言には5種類ある。
トップレベルクラス・ネストしたクラス・内部クラス・ローカル内部クラス・匿名クラス(無名クラスとも言われる)の5種類だ。
今回はこの5種類のクラス宣言のおさらい。

トップレベルクラス

これは普段使っているクラス。拡張子が.javaのファイルを作り、そのファイル名とクラス名を合致させなくてはいけない。そのjavaファイルのトップレベルに位置する。

ネストしたクラス

「ネストしたクラス」(Nested class)とはクラスの中にクラスがネストしている状態。トップレベルクラスの内側にstaticキーワードをつけてクラス宣言を行う。

public class Outer {
	public static class Nested {
		
	}
}

このネストしたクラスは、トップレベルクラスと同等の機能性を持つ。
クラス名はOuter.Nestedという名前で扱われるが、import文の記載によってNestedという単体の識別子でも利用できる。

内部クラス

内部クラス(Inner class)は外部クラスのインスタンスに紐付く。外部クラスのインスタンス1つに対して複数の内部クラスのインスタンスが生成できる。そして、内部クラスからは外部クラスのフィールドなどを参照することができる。

トップレベルクラスの内側にクラス宣言を行う。staticキーワードはつけない。

public class Outer {
	public class Inner {
		
	}
}

この内部クラス、喩えるならば、classとインスタンスの関係に近い。
外部クラスのインスタンスと内部クラスのインスタンスの関係はclassとインスタンスの関係のようで、
classに属するstaticフィールドはすべてのインスタンスから参照できるように
外部クラスのインスタンスフィールドは紐付く内部クラスのインスタンスから参照することができる。*1

そもそも紐付けとは何か。

内部クラスをnewする際、外部クラスのインスタンスを用意して

Outer o = new Outer();
Inner i = o.new Inner();

というように記述する。
このとき、コードがOuterクラスのインスタンスメソッド内であればOuterのインスタンスとしてthisを利用できる。

public class Outer {
	public class Inner {}

	public void hoge() {
		Inner i = this.new Inner();
	}
}

そして、thisは省略可能なので以下のようなコードで書かれることが多い。

public class Outer {
	public class Inner {}

	public void hoge() {
		Inner i = new Inner();
	}
}

ところで、staticメソッドの場合、thisは利用できない。
そのため、staticメソッド内で内部クラスをインスタンス化しようとした場合は先のように外部クラスのインスタンスを作ってからnewすることになる。

public class Outer {
	public class Inner {}

	public static void main(String[] args) {
		Outer o = new Outer();
		Inner i = o.new Inner();
	}
}

まだ、InnerクラスはOuterクラスのインスタンス1つにつき複数作ることができる。

Outer o = new Outer();
Inner i1 = o.new Inner();
Inner i2 = o.new Inner();
Inner i3 = o.new Inner();

これらi1-i3のインスタンスは、oのインスタンスフィールドにアクセスできる。

Outer o2 = new Outer();
Inner ix = o2.new Inner();

というように別のOuterのインスタンスを用意してInnerをnewした場合、このixのインスタンスからはo2のインスタンスフィールドを参照する。
なのでixからOuterのフィールドをいじった所でo2には影響を与えるがoには影響を与えない。

Inner型から見てOuter型のことをエンクロージング型(Enclosing type)と呼ぶ。
Innerクラスのインスタンスから見てOuterクラスのインスタンスをエンクロージングインスタンス(Enclosing instance)と呼ぶ。あるいはエンクロージングオブジェクトという表現がされることもある。

エンクロージングインスタンスの操作

InnerクラスからはOuterのインスタンスフィールドを参照することができる。また、インスタンスメソッドを呼び出すこともできる。
このとき、記法としては以下のようになる。

public class Outer {
	public String str;
	public void hoge() {}

	public class Inner {
		void piyo() {
			// 外部クラスのフィールド参照
			System.out.println(Outer.this.str);
			// 外部クラスのhoge()メソッド呼び出し
			Outer.this.hoge();
		}
	}
}

Innerクラスのメソッド内でthis.hoge()と書いた場合、このthisはInnerクラス自身のインスタンスを指す。
Outerクラスのインスタンス、つまりエンクロージングインスタンスを参照したい場合、そのOuterクラスのクラス名.thisという表記になる。
InnerクラスとOuterクラスでメソッド名やフィールド名が衝突しない場合、このOuter.thisは省略可能なので先の例は単に

public class Outer {
	public void hoge() {}

	public class Inner {
		void piyo() {
			// 外部クラスのフィールド参照
			System.out.println(str);
			// 外部クラスのhoge()メソッド呼び出し
			hoge();
		}
	}
}

というように書かれることが多い。
メソッド名やフィールド名が衝突した場合、Outer.thisを明記することで外部クラスのものを参照可能だ。
Outer.thisが省略されている場合、メソッド名・フィールド名はInner側のものが優先となる。

もし、多段の内部クラスを作った場合、直接のエンクロージングインスタンスの他にもエンクロージングインスタンスのエンクロージングインスタンスといった存在が生まれる。
これらにアクセスする場合、やはりクラス名.thisで参照できる。

public class Outer {
	class Inner {
		class Inner2 {
			void hoge() {
				System.out.println(Outer.this);
				System.out.println(Inner.this);
			}
		}
	}
}

ローカル内部クラス

ローカル内部クラス(Local inner class)はメソッドやコンストラクタなどのブロック内で宣言されるクラス。
これらのクラスはそのブロック内のみがスコープとなるので、ブロックの外側からはクラスの識別子を利用できない。

public class Outer {
	public static void main(String[] args) {
		class LocalClass {}

		LocalClass l = new LocalClass(); // OK
	}

	LocalClass field; // コンパイルエラー
	static void hoge() {
		LocalClass l = new LocalClass(); // コンパイルエラー
	}
}

IF文などのブロック内で宣言した場合、フロックの外側では利用できない。

if (true) {
	class LocalClass {}
}
LocalClass l = new LocalClass(); // NG

ローカル内部クラスは2種類あり、staticメソッド内で宣言されたものと、インスタンスメソッド内で宣言されたものだ。*2
インスタンスメソッド内で宣言された場合、内部クラス同様にOuterクラスのインスタンスへアクセス可能でOuter.thisを利用できる。
staticメソッド内で宣言されたものはこれができない。

匿名クラス

匿名クラス(Anonymous class)は主に抽象クラスやinterfaceなどに対して名前を付けずにその場で実装を書くもの。

Runnable r = new Runnable() {
	@Override
	public void run() {
	}
};

例はRunnableインターフェースを実装する匿名クラスをnewしたところ。
最後のセミコロンに注意。これはr = の部分に対してのセミコロンとなる。
new Runnable()の後ろの{}までが匿名クラスの宣言なのだが、これはインスタンスを扱うどこにでも記述可能で
通常の代入式の右辺の部分がnew Runnable(){...}に差し替わったと理解するといい。
ソースコードの見た目はいびつになるが、構文解析としてはその部分をまるまるくくりだして落ち着いて考えれば大丈夫。

匿名クラスは抽象クラスやinterfaceに対して利用されることが多いが、具象型に対しても使うことはできる。

List<String> list = new ArrayList<String>(){{add("hoge")}};

これは自動テストなどでListなどの初期化が面倒くさい時とかに使われていることがある。
構文的には、匿名クラスをつくりインスタンス初期化ブロックの中でaddメソッドを呼んでいるわけだ。
デバッグ用にtoString()だけオーバライドしたりなどいろいろやれるが、わりと邪道なテクニックなので多用するべきではないだろう。

この匿名クラスも2種類あり、ローカル内部クラス同様にインスタンスメソッド内で宣言されたものはOuter.thisにアクセス可能となる。
匿名クラスはnewするその瞬間に実装を書くので、インスタンスを2つ以上つくりたいような場合はローカル内部クラスにするか内部クラスにするかしよう。

外部メソッドのfinal変数の参照

ローカル内部クラスと匿名クラスでは宣言された箇所で参照可能なfinalなローカル変数をクラス内部で参照することができる。

public static void main(String[] args) {
	final int i = 0;
	class LocalClass {
		void hoge() {
			System.out.println(i);
		}
	}
}

これは匿名クラスなどで実装を書く時などによく利用される。見た目に外のメソッドにある変数が使えるように見えるわけだが、実際にはこのローカル内部クラスや匿名クラスのインスタンスはどこかに運ばれてそこで初めてメソッドが呼び出されたりするかもしれない。そのとき、外のメソッドにある変数が参照できるとはどういうことなのか?

端的に言えば、これらの情報はクラス生成時点でこっそり渡されているというカラクリになっている。詳しく知りたければclassファイルの中を覗いてみるなり逆コンパイルしてみるなどするといい。

interfaceと列挙

interfaceとenumもネストすることができる。
ネストしたクラスと同様でトップレベルのクラスの下にだけネスト可能。
interfaceとenumはstaticをつけて宣言しても、つけずに宣言しても、static扱いとなる。

トップレベルクラスに併記

名称があるのかよく分からないのだけど、トップレベルクラスと同じjavaファイル内にパッケージプライベートなクラスを併記することができる。
例えばHoge.javaファイルの中に

public class Hoge{
}
class Piyo {
}

と書くとコンパイルすることができる。

まとめ

5種類のクラス宣言についておさらいした。とくに内部クラスからエンクロージングインスタンスを参照するあたりは実際にコードを書いていじらないとしっくりこないかもしれない。

内部クラスの利用例としてはIteratorなどが挙げられる。Listから取得されるIteratorはListとは別の型で別のインスタンスなわけだが、元となるList内のデータを参照することになる。JDK付属のソースを読むとわかるが、このIteratorはList(の実装クラスの共通部にあたるAbstractList)の内部クラスとして作らている。

AndroidのUI開発などをすると設計上、こうしたインスタンス間の紐付けを多用することになるので油断ならない。しっかり基礎を固めておこう。

追記

Anonymous class に対しては訳語として「匿名クラス」と「無名クラス」がある。Anonymousに対する訳語としては一般に「匿名」とされるので「匿名クラス」が自然に思われる。

Sun(Javaを開発した会社。現在はOracleに吸収されている)の公式本では

となっている。個人的には音節の関係か「無名クラス」の方が言いやすいのでつい言ってしまう。あまり目くじらを立てないでいただきたい :-P

*1:このクラス-インスタンス関係の類似性を高階インスタンスと捉えて応用したのがJavaによる高階型変数の実装 - プログラマーの脳みそ

*2:本当はこの他にもコンストラクタや初期化ブロックでも宣言できる