PR

【Java】インナークラスとは?内部クラスの仕組みを3分でわかりやすく

Java

インナークラスは、Javaのクラスの中に定義された別のクラスのことです(箱の中に小さな箱が入っているようなイメージ)。大きく分けて以下の4つの種類があり、場面に応じた適切な使い分けが必要です。

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

Javaのインナークラスについて、初心者の方にも分かりやすいように解説していきます。

スポンサーリンク

メンバインナークラス (Member Inner Class)

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

class Outer {
    private int outerField = 10;

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

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

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

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

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

特徴

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

静的インナークラス (Static Inner Class)

静的インナークラスは、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) {
        // 外側のクラスのインスタンスなしで、静的インナークラスのインスタンスを作成
        OuterStatic.InnerStatic innerStatic = new OuterStatic.InnerStatic();
        innerStatic.innerStaticMethod(); // 出力: OuterStaticの静的フィールドの値: 20
    }
}

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

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

イメージ

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

特徴

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

ローカルインナークラス (Local Inner Class)

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

class OuterLocal {
    private int outerField = 40;

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

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

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

    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() {
        int localVariable = 50; // finalまたはeffectively finalである必要がある

        class LocalInner {
            public void localInnerMethod() {
                System.out.println("Outerのフィールドの値: " + outerField);
                System.out.println("ローカル変数の値: " + localVariable);
            }
        }
        return new LocalInner(); // 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); // 外側のメンバにアクセス
            }
        };
        anonymousGreeting.greet(personName); // 出力: Hello, Taro
    }

    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をコピーしました