こんにちは、かつコーチです。
Javaには4種類の「クラスの中に書くクラス」があります。
今回はその使い分けと、実際に手を動かして分かった注意点を整理します。
4種類の内部クラスの違い
メンバ内部クラス(Inner Class)
外側のクラスのインスタンスに紐づくクラスです。
外側のインスタンスのフィールドに直接アクセスできます。
public class Outer {
private int value = 10;
public class Inner {
public void show() {
System.out.println("Outer.value = " + value);
}
}
}
// 呼び出し方
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.show(); // Outer.value = 10
outer.new Inner()という書き方に最初は違和感がありましたが、Innerが「Outerのインスタンスに従属する」ことを表す構文だと理解すると納得できます。
静的ネストクラス(Static Nested Class)
staticをつけると、外側のインスタンスと切り離されます。
外側のインスタンスなしで生成でき、外側のインスタンスフィールドには直接アクセスできません。
public class Outer {
public static class Nested {
public void show() {
System.out.println("Nestedから呼び出し");
}
}
}
Outer.Nested nested = new Outer.Nested();
nested.show();
実務では「外側のクラスと強い関連はあるが、外側のインスタンス状態には依存しない」ケース(Map.Entryのようなデータ保持クラスなど)でよく使われます。
ローカルクラス(Local Class)
メソッドの中で定義するクラスです。
public void process(List<String> names) {
class NameFormatter {
String format(String name) {
return "【" + name + "】";
}
}
NameFormatter formatter = new NameFormatter();
names.forEach(name -> System.out.println(formatter.format(name)));
}
そのメソッド内でしか使わない、小さなヘルパークラスを作りたいときに使います。
実務での出番は少なく、多くの場合はラムダ式や匿名クラスで代替できます。
匿名クラスの実践パターン
匿名クラスの基本形
クラス名を持たず、インターフェースや抽象クラスをその場で実装するクラスです。
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("匿名クラスで実行");
}
};
task.run();
匿名クラス(クラス定義とインスタンス生成を同時に行い、名前を持たないクラス)は、1回しか使わない実装をその場で書きたいときに便利です。
ラムダ式との使い分け
関数型インターフェース(抽象メソッドを1つだけ持つインターフェース)を実装する場合は、匿名クラスよりラムダ式の方が簡潔です。
// ❌Before:匿名クラス
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("実行");
}
};
// ✅After:ラムダ式
Runnable task2 = () -> System.out.println("実行");
ただし匿名クラスにしかできないこともあります。
複数の抽象メソッドを持つinterfaceや抽象クラスの実装、フィールドを持たせたい場合は匿名クラスが必要です。
abstract class Counter {
abstract void increment();
abstract int getCount();
}
Counter counter = new Counter() {
private int count = 0;
@Override
void increment() {
count++;
}
@Override
int getCount() {
return count;
}
};
つまずいたポイント:メモリリークとキャプチャ変数
内部クラスが外側の参照を持ち続ける問題
非staticの内部クラス・匿名クラス・ローカルクラスは、暗黙的に外側のインスタンスへの参照を保持します。
Androidアプリのコードをレビューした際、Activityの非static内部クラスとして実装されたリスナーが原因で、Activityが破棄されてもメモリ上に残り続けるメモリリーク(不要になったオブジェクトが参照され続け解放されない状態)を見つけたことがあります。
// ❌Before:非staticの内部クラスが外側の参照を暗黙的に保持する
public class MainActivity {
class ClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
// MainActivity.thisへの参照を暗黙的に保持
}
}
}
// ✅After:静的ネストクラス+弱参照で外側への依存を切る
public class MainActivity {
static class ClickListener implements View.OnClickListener {
private final WeakReference<MainActivity> activityRef;
ClickListener(MainActivity activity) {
this.activityRef = new WeakReference<>(activity);
}
@Override
public void onClick(View v) {
MainActivity activity = activityRef.get();
if (activity != null) {
// 処理
}
}
}
}
長時間動作するオブジェクトから短命なコンポーネントを参照する設計では、内部クラスをstaticにするかどうかを常に意識するようになりました。
ローカル変数のキャプチャは実質的にfinal
匿名クラスやローカルクラスからアクセスするローカル変数は、実質的にfinal(明示的にfinalを付けなくても再代入していなければfinal扱いになる)である必要があります。
public void run() {
int count = 0;
Runnable r = () -> System.out.println(count); // OK:countは再代入されていない
count = 1; // ここでラムダ式側がコンパイルエラーになる
}
初めてこのエラーに遭遇したときは意味が分からず戸惑いましたが、匿名クラス・ラムダ式は変数の「値」をコピーしてキャプチャする仕組みだと理解してからは納得できました。
変数そのものを共有しているわけではないため、後から変更されると不整合が起きる可能性があり、コンパイラがそれを防いでいるわけです。
まとめ
この記事のポイント
- 内部クラスには「メンバ内部クラス」「静的ネストクラス」「ローカルクラス」「匿名クラス」の4種類がある
- 外側のインスタンスに依存しないなら
staticをつけて静的ネストクラスにする - 関数型インターフェースの実装はラムダ式で簡潔に書ける
- 非staticの内部クラスは外側への参照を保持し続けるため、長寿命オブジェクトからの利用ではメモリリークに注意する
- ローカル変数のキャプチャは実質的にfinalである必要がある
次に読むべき記事
- ラムダ式の基本を先に押さえたい方は関数型プログラミング編の記事へ
- 抽象クラスの基礎は「抽象クラス(abstract)の使い方」の記事へ
タグ: Java, 上級者向け, オブジェクト指向