PR

【Java】内部クラス(nested class)の仕組みを3分でわかりやすく

Java

内部クラス(nested class)は、その名の通りJavaのクラス中に定義された別のクラスのことを指します(箱の中に小さな箱が入っているようなイメージ)。大きく分けて以下の4つの種類があり、場面に応じた適切な使い分けが求められます。

  1. インナークラス (Inner Class):
    クラスのメンバ(フィールドやメソッド)と同じように定義されるクラス。インスタンスメンバとして扱われるため、外側のクラスのインスタンスを通してのみインスタンス化できます。外側のクラスのprivateメンバにもアクセスできるのが特徴。
  2. staticネストクラス (Static Nested Class):
    クラス内に、static修飾子をつけて定義されたクラス。外側のクラスのインスタンスがなくてもインスタンス化でき、静的メンバのように振る舞います。外側のクラスの静的メンバにはアクセスできますが、インスタンスメンバには直接アクセスできません。
  3. ローカルクラス (Local Class):
    メソッドやコンストラクタ、イニシャライザなどのブロック内で定義されるクラス。そのブロック内でのみ有効で、外側のクラスのメンバや、そのブロックのfinalまたはeffectively finalなローカル変数にアクセスできます。名前を持たない匿名クラスとは異なり、名前を持つことができます。
  4. 匿名クラス (Anonymous Class):
    名前を持たないクラスで、クラスの宣言と同時にインスタンス化されるクラス。主に、インターフェースや抽象クラスを実装したり、クラスを継承したりする際に、その場で簡単な処理を記述するために使われます。ローカルクラスと同様に、外側のクラスのメンバや、そのブロックのfinalまたはeffectively finalなローカル変数にアクセスできます。

このページでは、上記4種類のJavaの内部クラスについて、初心者の方にも分かりやすいように解説していきます。

スポンサーリンク

【前提】クラスとスコープをおさらい

「クラス」はデータ(フィールド)と振る舞い(メソッド)を束ねる設計図で、「インスタンス」はその具体物です。

クラスとインスタンスの概念
図1:クラスとインスタンスの概念

参考 クラスとインスタンスの基本概念を1から

Javaではクラス宣言の内側にさらに別のクラスを記述することが許されており、これをネストクラス(入れ子クラス)と呼びます。トップレベルクラスと異なり、スコープ(見える範囲)が外側クラスの内部に限定される点が最大の特徴です。

なぜ“クラスの中にクラス”が必要か

理由1 「外側のクラスと強く関係する処理」をまとめたいから

たとえばButtonクラスがあって、そのボタンの「クリックイベント処理」を表すClickListenerクラスがあるとします。このClickListenerは他の用途には使いませんし、Buttonとセットで使うのが当然です。
だったら、わざわざ別ファイルに分けるより、中に書いた方が読みやすくて自然ですよね?

「このクラスはこの中でしか使わない」
なら中に書けばいいじゃないか!

理由2 「外側の状態(フィールド)を安全に共有」できるから

インナークラスは、外側クラスのprivateフィールドにアクセス可能です。通常、privateな情報は外から見えませんが、内部にいるクラスだからこそ見られる、という特徴を持ちます。
外側に余計なgetterを生やさなくていいという意味でも、安全な設計にすることが可能になります。

理由3 「外に見せたくない構造を隠せる」から

クラスを外部ファイルに出すと、他の人や他のクラスからもアクセスされる可能性があります。でも内部クラスなら、完全に外側クラスの一部として、スコープを制限できます。
「このロジックはこのクラス専用だよ」と明確な意図をコード上に示せるのが大きなメリットです。

つまり内部クラスは「関係が強く、独立性が低い」クラスのための居場所っていうイメージです。以下のような設計上の目的を果たすために、Javaは内部クラスという仕組みを提供しているとまずは理解しておきましょう。

  • ロジックのまとまり(凝集度)を保つ
  • 外部への漏洩を防ぐ(情報隠蔽)
  • コードの可読性・保守性を上げる

ここからは1個1個の内部クラスの特徴や注意点について解説します。

インナークラス (Inner Class)

インナークラスは、外側のクラスの「メンバー」として定義される内部クラスです。これは、クラスの中にあるフィールド(変数)やメソッド(関数)と同じような立ち位置だと考えると分かりやすいです。

class Outer {
    private int outerField = 10;

    class Inner {
        public void innerMethod() {
            // 外側のprivateメンバにアクセス
            System.out.println("Outerのフィールドの値: " + outerField);
        }
    }

    public void createInnerAndCall() {
        // 外側のクラスから内部クラスのインスタンスを作成
        Inner inner = new Inner();
        inner.innerMethod();
    }

    public static void main(String[] args) {
        Outer outer = new Outer();
        // 出力: Outerのフィールドの値: 10
        outer.createInnerAndCall();

        // 外側のクラスのインスタンスを通して、内部クラスのインスタンスを作成
        Outer.Inner innerInstance = outer.new Inner();
        // 出力: Outerのフィールドの値: 10
        innerInstance.innerMethod();
    }
}

この例では、Outerクラスの中にInnerクラスが定義されています。InnerクラスのinnerMethodメソッドは、外側のOuterクラスのprivateなフィールドであるouterFieldにアクセスすることが可能。また、Innerクラスのインスタンスを作成する際には、まずOuterクラスのインスタンス (outer) を作り、それを通して outer.new Inner() のようにして作成します。

特徴

  • インスタンスに紐づく:
    インナークラスのインスタンスを作るには、前提として外側クラスのインスタンスが必要です。
  • 外側のクラスのメンバにアクセス可能:
    インナークラスからは、外側のクラスのprivateなメンバを含むすべてのメンバ)に自由にアクセスすることが可能です。

staticネストクラス (Static Nested Class)

staticネストクラスは、staticキーワードをつけて定義された内部クラスです。これは外側のクラスのインスタンスに直接紐付かず、独立して存在できる点がインナークラスとは異なります。

class OuterStatic {
    private static int staticOuterField = 20;
    private int instanceOuterField = 30;

    static class InnerStatic {
        public void innerStaticMethod() {
            // 静的メンバにアクセス
            System.out.println("OuterStaticの静的フィールドの値: " + staticOuterField);
            // エラー!インスタンスメンバにはアクセスできない
            // System.out.println("OuterStaticのインスタンスフィールドの値: " + instanceOuterField); 
        }
    }

    public static void main(String[] args) {
        // 外側のクラスのインスタンスなしで、staticネストクラスのインスタンスを作成
        OuterStatic.InnerStatic innerStatic = new OuterStatic.InnerStatic();
        // 出力: OuterStaticの静的フィールドの値: 20
        innerStatic.innerStaticMethod();
    }
}

この例では、OuterStaticクラスの中にstatic修飾子が付いたInnerStaticクラスが定義されています。InnerStaticクラスのinnerStaticMethodメソッドは、外側のOuterStaticクラスのstaticなフィールドであるstaticOuterFieldにアクセスできていますが、インスタンスフィールドであるinstanceOuterFieldにはアクセスしようとするとエラーになります。

staticなメソッドはstaticではないメンバへのアクセスができない、という意味でJavaの原理原則と一致しており、不自然なルールではありません。

尚、staticネストクラスのインスタンスは、OuterStatic.InnerStatic innerStatic = new OuterStatic.InnerStatic(); のように、外側のクラス名を指定して直接作成できます。

イメージ

大きな家の敷地内にある、独立した小さな小屋(staticネストクラス)を想像してください。小屋は家の一部ではありますが、家の主人(外側のクラスのインスタンス)がいなくても存在できます。ただし、小屋からは家の中の共有スペース(外側のクラスのstaticなメンバ)は見えますが、個人の部屋(外側のクラスのインスタンスメンバ)には直接入ることはできません。

特徴

  • 外側のクラスのインスタンスが不要:
    staticネストクラスのインスタンスは、外側のクラスのインスタンスがなくても直接作成できます。
  • 外側のクラスの静的メンバにのみアクセス可能:
    staticネストクラスからは、外側のクラスのstaticなフィールドやメソッドに直接アクセスできますが、staticでないインスタンスメンバにはアクセスできません。

ローカルクラス (Local Class)

ローカルクラスは、メソッドやコンストラクタなどの特定のブロックの中で定義されるクラスです。その定義されたブロック内でのみ有効で、外からはアクセスできません。

ブロック内のみで有効→つまり、ローカルクラスにはアクセス修飾子を付ける必要がない→アクセス修飾子を付けるとコンパイルエラーになります。

class OuterLocal {
    private int outerField = 40;

    public void outerMethod() {
        // finalまたはeffectively finalである必要がある
        int localVariable = 50; 

        class LocalInner {
            public void localInnerMethod() {
                // 外側のメンバにアクセス
                System.out.println("Outerのフィールドの値: " + outerField);
                // finalまたはeffectively finalなローカル変数にアクセス
                System.out.println("ローカル変数の値: " + localVariable);
            }
        }

        LocalInner localInner = new LocalInner();
        // 出力: Outerのフィールドの値: 40, ローカル変数の値: 50
        localInner.localInnerMethod();
    }

    public static void main(String[] args) {
        OuterLocal outer = new OuterLocal();
        outer.outerMethod();
    }
}

イメージ

あなたが一時的に借りた部屋(メソッドなどのブロック)の中で、その部屋の中だけで使うための小さな道具(ローカルインナークラス)を作るようなイメージです。その道具は部屋の外には持ち出せませんし、他の部屋の人が使うこともありません。

特徴

  • 定義されたブロック内でのみ有効:
    ローカルクラスは、それが定義されたメソッドなどのブロックが終わると、その存在は消えます。
  • 外側のクラスのメンバにアクセス可能:
    ローカルクラスからは、外側のクラスのすべてのメンバ(privateを含む)にアクセスできます。
  • ブロック内のfinalまたはeffectively finalなローカル変数にアクセス可能:
    ローカルクラスが定義されているメソッドなどのブロック内のローカル変数にアクセスする場合、その変数はfinalであるか、実質的にfinal(一度代入された後、値が変更されない)である必要があります。
Q
なぜ、ローカルクラスは、ローカル変数にアクセスする際にそれが final であるか、実質的に final である必要がある?
A

Javaのメモリ管理と変数のライフサイクルが深く関わっています。

理由を理解するためのステップ:

  1. ローカル変数のライフサイクル:
    メソッドが実行されると、そのメソッド内で宣言されたローカル変数はスタック領域に確保されます。メソッドの処理が終わると、これらのローカル変数はスタックから解放され、消滅します。
  2. ローカルインナークラスのインスタンスのライフサイクル:
    一方、ローカルクラスのインスタンスは、それが定義されたメソッドの実行が終了した後も、ヒープ領域に生き残る可能性があります。例えば、ローカルクラスのインスタンスが、メソッドの外の別のオブジェクトに渡されたり、保持されたりする場合です。
  3. データの不整合のリスク:
    もしローカルイクラスが、メソッド終了後に消滅する可能性のあるローカル変数を直接参照できたとすると、以下のようなデータの不整合が起こりえます。
    • メソッドが終了し、ローカル変数がメモリから解放される。
    • しかし、ローカルクラスのインスタンスはまだ生き残っており、解放されたメモリ領域を参照しようとする。→これは危険な状態です。

Javaの解決策:

Javaは、この問題を避けるために、ローカルクラスがアクセスするローカル変数を final または実質的に final にすることを要求しています。

  • final 変数の場合:
    final 変数は一度値が代入されると変更できないため、ローカルクラスが参照する値は常に一定です。メソッドの実行が終了しても、ローカルクラスのインスタンスが参照する値は不変であることが保証されます。
  • 実質的に final な変数の場合:
    実質的に final な変数は、宣言後に一度だけ代入されるか、全く代入されずに使用される変数です。コンパイラは、これらの変数が事実上 final であると認識し、final 変数と同様に扱います。これにより、コードの柔軟性を保ちつつ、データの不整合のリスクを回避できます。
class OuterLocalExtended {
    private int outerField = 40;

    public LocalInner getLocalInnerInstance() {
        // finalまたはeffectively finalである必要がある
        int localVariable = 50;

        class LocalInner {
            public void localInnerMethod() {
                System.out.println("Outerのフィールドの値: " + outerField);
                System.out.println("ローカル変数の値: " + localVariable);
            }
        }
        // LocalInnerのインスタンスをメソッドの外に返す
        return new LocalInner();
    }

    public static void main(String[] args) {
        OuterLocalExtended outer = new OuterLocalExtended();
        // メソッド終了後にインスタンスを受け取る
        LocalInner instance = outer.getLocalInnerInstance();
        // ... 何らかの処理 ...
        // メソッド終了後にローカル変数にアクセスしようとする(実際にはコピーを参照)
        instance.localInnerMethod(); 
    }
}

この例では、getLocalInnerInstance メソッド内で LocalInner のインスタンスが作成され、メソッドの戻り値として外に渡されます。main メソッドでは、getLocalInnerInstance の実行が終了した後も、LocalInner のインスタンス (instance) が存在し続ける可能性があります。

もし localVariablefinal でなかった場合、getLocalInnerInstance メソッドが終了し、スタック上の localVariable が解放された後も、instance がその解放されたメモリ領域を参照しようとする危険性があります。これは、プログラムのクラッシュや予期しない動作を引き起こす可能性があります。

final または実質的に final であることで、ローカルクラスのインスタンスが生成された時点で、参照するローカル変数の値がコピーされ、そのコピーがインスタンス内に保持されます。そのため、元のローカル変数がメソッド終了時に消滅しても、コピーされた値は安全にアクセスできるということになります。

匿名クラス (Anonymous Class)

匿名クラスは、名前を持たないクラスで、クラスの宣言と同時にそのインスタンスが作成されます。主に、インターフェースを実装したり、クラスを継承したりする際に、その場で簡単な処理を記述するために使われます。

interface Greeting {
    void greet(String name);
}

class OuterAnonymous {
    private String message = "Hello, ";

    public void performGreeting(String personName) {
        // 匿名クラスを使ってGreetingインターフェースを実装
        Greeting anonymousGreeting = new Greeting() {
            @Override
            public void greet(String name) {
                // 外側のメンバにアクセス
                System.out.println(message + name);
            }
        };
        // 出力: Hello, Taro
        anonymousGreeting.greet(personName);
    }

    public static void main(String[] args) {
        OuterAnonymous outer = new OuterAnonymous();
        outer.performGreeting("Taro");
    }
}

この例では、Greetingというインターフェースを、performGreetingメソッドの中で匿名クラスを使って実装しています。new Greeting() { ... } の部分が匿名クラスの定義とインスタンス化を同時に行っている箇所です。匿名クラスの中のgreetメソッドでは、外側のOuterAnonymousクラスのmessageフィールドにアクセスしています。

匿名クラスも、メソッド内で定義しているクラスなので、ローカルクラスと同様でアクセス修飾子を付けるとコンパイルエラーが発生します。

イメージ

例えるなら、注文したケーキ屋さんで、その場で簡単なデコレーション(匿名クラスによる処理の実装)を加えてもらうようなイメージです。デコレーションの名前はないけれど、その場でケーキに特別な機能を追加できます。

特徴

  • 名前がない:
    クラス定義に名前がありません。
  • 宣言と同時にインスタンス化:
    new インターフェース名() または new スーパークラス名() のようにして、その場でクラスの定義とインスタンスの作成を同時に行います。
  • 外側のクラスのメンバやfinal/effectively finalなローカル変数にアクセス可能:
    ローカルインナークラスと同様のアクセスルールを持ちます。
タイトルとURLをコピーしました