【Java】
クラスの中のクラス(インナークラス(内部クラス)、ローカルクラス、匿名クラス)の特徴と使い方

ネストしたクラスについて解説

投稿日 2022/03/06 更新日 2022/03/06


こんにちは。当サイトの管理者「元木皇天」です。

最近Javaの参考書を読み漁っていて、「そういえばクラスの中に書くクラスのことをあまり知らないな」、と思いました。

ということで、今回はJavaのネストしたクラスについて解説いたします。

環境
OS:MacOS Big Sur
Java:Ver 11
Eclipse:Ver 2021-12

やりたいこと

インナークラス(内部クラス)の特徴と使い方を知る

staticなインナークラス(内部クラス)の特徴と使い方を知る

ローカルクラスの特徴と使い方を知る

匿名クラスの特徴と使い方を知る

参考文献
徹底攻略Java SE 11 Gold問題集[1Z0-816]対応
スッキリわかるJava入門 実践編 第3版
やさしいJava 第7版
Java 第3版 実践編 アプリケーション作りの基本

インナークラス(内部クラス)の使い方と特徴

インナークラス(内部クラス)についてまずは解説いたします。

インナークラスはその名の通り、通常のクラスの内側で宣言されたクラスのことです。

宣言は以下のように行います。

public class Outer {

    public String outer = "Hello";

    //インナークラス(内部クラス)の宣言
    public class Inner {
        //フィールド
        private String sample;

        //コンストラクタ
        public Inner(String sample) {
            this.sample = sample;
        }

        //インナークラスのメソッド
        public void test() {
            //コンストラクタでセットしたフィールドの値を出力する
            System.out.println(outer + sample);
        }
    }
}

特徴としては、フィールド値、コンストラクタの宣言、メソッドの作成、public等のアクセス修飾子など、通常のクラスと何ら変わりなく扱うことができるという点です。

また、InnerクラスからOuterクラスの変数を使用することもできます。


では、実際にこのインナークラスのインスタンスを生成して、メソッドを実行してみましょう。

インスタンスの生成・実行は以下の通りです。

public class Main {
    public static void main(String[] args) {

        //インナークラス(内部クラス)のインスタンス生成
        Outer.Inner inner = new Outer().new Inner("サンプル");

        //インナークラスのメソッドを実行する
        inner.test();
    }
}

実行結果

インナークラスのメソッドの実行結果

注目すべきは、インスタンスの生成方法です。

new Outer()で通常のクラスをインスタンス化し、その後ろにnew Inner()を記述しています。

あまり馴染みのないコードかもしれませんが、実際は以下のような処理を1行でやっているだけです。

//Outer.Inner inner = new Outer().new Inner("サンプル");と同じ意味
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner("サンプル");

また、型宣言が冗長でめんどくさい場合はvarを用いてもいいかもしれないですね。

var inner = new Outer().new Inner("サンプル");

まとめると
インナークラスは通常のクラスの内部に記述するクラスで、フィールド値、コンストラクタの宣言、メソッドの作成、public等のアクセス修飾子など通常のクラスと変わりない記述で使用できる。

ただし、インナークラスのインスタンスの生成時には、必ず外側のクラスも一緒にインスタンス化しないといけない。

staticなインナークラス(内部クラス)の使い方と特徴

今度は先ほどと同じ分類のインナークラスについてですが、staticがついたインナークラスについて解説します。

staticがつくと、通常のインナークラスとは異なる扱いをしないといけないです。

staticなインナークラスは以下のように宣言します。

public class Outer {

    public static String outer = "Hello";

    //インナークラス(内部クラス)の宣言
    public static class Inner {
        //フィールド
        private String sample;

        //コンストラクタ
        public Inner(String sample) {
            this.sample = sample;
        }

        //インナークラスのメソッド
        public void test() {
            //コンストラクタでセットしたフィールドの値を出力する
            System.out.println(outer + sample);
        }
    }
}

特徴としては、みてわかる通りstaticがclassの前についたことです。

これでstaticなインナークラスが出来上がります。

しかし、よく見ると外側のクラスのフィールド値もstaticがついていることがわかるでしょう。

これはstaticの仕組み上、そうしないとコンパイルエラーになってしまうためです。


通常のクラスでも、staticなメソッドからstaticでないフィールドにアクセスすることはできなかったように、staticなインナークラスからstaticでないフィールド値・メソッドを参照することはできないのです。

そのため、フィールド値もstaticで修飾しています。


では、これをインスタンス化してメソッドを実行してみましょう。

インスタンスの生成と実行は以下の通りです。

public class Main {
    public static void main(String[] args) {

        //インナークラス(内部クラス)のインスタンス生成
        Outer.Inner inner = new Outer.Inner("サンプル");

        //インナークラスのメソッドを実行する
        inner.test();

    }
}

実行結果

staticなインナークラスのメソッドの実行結果 staticなインナークラスのメソッドの実行結果

特徴としては、Outerクラスのインスタンス化を必要としない点です。

staticがついていないインナークラスでは、Outerクラスをインスタンス化して、そのインスタンスを使用してインナークラスをインスタンス化していました。

しかし、staticなインナークラスでは直接インナークラスをインスタンス化することができるため、コードの書き方が変わります。

これは、staticな値はプログラムが実行された時点で、メモリのstaticな領域に配置されるためです。

Java実行時にstaticなクラスは既にメモリ上に展開されているため、外側のクラスのインスタンスを生成しなくても参照することができるということです。

逆にstaticでないインナークラスは、メモリ上に展開されていないため、外側のクラスのインスタンスをメモリのヒープ領域に展開してからでないと参照できないといった感じです。

そのため、インスタンスの生成方法が変わるので注意が必要です。


まとめると
staticなインナークラスからは、staticなフィールド・メソッドしか参照できない。

また、インスタンスの生成には外側のクラスのインスタンス化は不要です。

ローカルクラスの特徴と使い方と特徴

ローカルクラスについて、次は解説いたします。

先ほどのインナークラスはclass配下にclassを記述していましたが、ローカルクラスではメソッド配下にclassを記述します。

つまり、スコープの範囲がメソッド内のみに狭まるということです。

では、実際に使い方を見てみましょう。

ローカルクラスの宣言と実行は以下のように行います。

public class Main {
    public static void main(String[] args) {
    	Main main = new Main();

    	//ローカルクラスを持つメソッドの実行
    	main.sample();
    }

    public void sample() {
    	String test = "Hello";

    	//ローカルクラス
        class Inner {
            //フィールド
            String sample;

            //コンストラクタ
            public Inner(String sample) {
                this.sample = sample;
            }

            //メソッド
            public void sample() {
                System.out.println(test + this.sample);
            }
        }

        //インスタンス生成
        Inner inner = new Inner("サンプル");

        //ローカルクラスのメソッド実行
        inner.sample();
    }
}

実行結果

staticなインナークラスのメソッドの実行結果 staticなインナークラスのメソッドの実行結果

ローカルクラスも前項で紹介したインナークラスのように、フィールド、コンストラクタ、メソッドを持つことができます。

ただしローカルクラスなので、宣言したメソッド内でしか使用できないというのが一つ注意点です。

また、ローカルクラス内から、上位のメソッドで使用している変数を使用することができるのですが、2点注意が必要です。

1点は、ローカルクラスで使用する上位メソッド内のローカル変数は、ローカル変数よりも前に記述しなければならないという点です。

当たり前と言えば当たり前ですが、念のため書いておきました。

以下はローカルクラスよりも後に変数を宣言をしている例です。コンパイルエラーになっていることがわかります。

test を変数に解決できません

もう1点は、ローカルクラス内から上位のメソッドで使用しているローカル変数を使用する場合、そのローカル変数は実質的finalでなければならないという点です。

どういう意味なのかわかりにくいかと思うので、先にコードを見せます。

Local variable test defined in an enclosing scope must be final or effectively final

このように、ローカルクラス内で使用している上位メソッド内のローカル変数の値を変更すると、一見影響ないように見えてもローカルクラス内でコンパイルエラー(Local variable test defined in an enclosing scope must be final or effectively final)が出てしまいます。

そのため、finalで修飾していなくても値を変更してはいけないのです。

なぜ、このようなことをしないといけないかというと、メソッド内のローカル変数とローカルクラスのライフサイクルが異なるからです。

まとめると
ローカルクラスから上位のメソッドのローカル変数を使用する場合は、その変数は実質的final出なければならない。

コンストラクタやフィールド、メソッドなど、通常のクラスと同様に宣言できる。

匿名クラスの特徴と使い方と特徴

最後に匿名クラスについて解説いたします。

匿名クラスは、インターフェースや抽象クラスがどのようなフィールドやメソッドを持つべきかを実現クラス・具象クラスとして名前のないクラスを宣言して定義したものです。

なんかわかりにくい言い方をしてしまいましたが、以下のような感じで、インスタンス生成と同時にその中身を定義するといった感じです。

public class Main {
    public static void main(String[] args) {
        Runnable sample = new Runnable() {
            @Override
            public void run() {
                System.out.println("サンプル");
            }
        };
        sample.run();
    }
}

実行結果

匿名クラスのメソッドの実行結果

インターフェースは通常インスタンス化できませんが、匿名クラスを使用すると、インターフェースを簡単に実装してインスタンスを生成することができます。


また、インスタンス生成時に匿名クラスを使用することで、既存のクラスに対して別のメソッドを追加したり、既存のメソッドをOverrideをしたりして仕様を変更することができたりします。

オーバーライドして使用したい場合は、以下のように宣言します。

import java.util.ArrayList;
    import java.util.List;

    public class Main {
        public static void main(String[] args) {
            //ArrayListの匿名クラスを作成
            List<String> list = new ArrayList<String>() {
                @Override
                public boolean add(String str) {
                    System.out.println(str + "を追加します");

                    return super.add(str);
                }
            };

            //匿名クラスのメソッドを実行
            list.add("1");
            list.add("2");
            list.add("3");
        }
    }

実行結果

ArrayListクラスのaddメソッドを匿名クラスでオーバーライドする

通常のadd()メソッドはコンソール出力の機能はないですが、匿名クラスにてoverrideしているためコンソールに出力されていることがわかります。


さらに、既存のクラスを継承・インターフェースの実装を行って別の機能を追加したいが、一時的にしか使用しない、といった場合は以下のように匿名クラスを使用することも可能です。

以下の例では、ArrayListクラスを継承した匿名クラスを作成し、新たにsampleFuncというメソッドを追加しています。

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        //ArrayListを継承した匿名クラスを作成
        var list = new ArrayList<String>() {
            //匿名クラスにメソッドを追加
            public void sampleFunc() {
                System.out.print("サンプルメソッドの実行");
            }
        };

        //匿名クラスのメソッドを実行
        list.sampleFunc();
    }
}

実行結果

匿名クラスを使用して、ArrayListを継承した匿名クラスを作成する

このとき注意が必要なのは、インスタンスを生成してそれを格納する変数の型はvarにしないといけないということです。

なぜなら、この匿名クラスは継承・実装しているクラスのため、継承・実装したクラスの型を宣言しなければなりません。

しかし、匿名クラスというだけあって、そのクラスに名はついていません。

そこで、型推論を行ってくれるvarを使用する必要があるのです。


また、匿名クラスは今までに紹介した「インナークラス」「ローカルクラス」とは異なり、コンストラクタを定義できません

これはコンストラクタにはそのクラス名を使用しますが、匿名クラスには名前がないからです。疑似的にコンストラクタを実施したい場合は初期化子を使用してください。


まとめ
匿名クラスを使用して継承・実装を行う場合は、型をvarにする

コンストラクタは定義できない。定義したい場合は代わりに初期化子を用いる

まとめ

Javaでクラスの中のクラスには

インナークラス(内部クラス)
staticなインナークラス(内部クラス)
ローカルクラス
匿名クラス

がある。

参考文献・おすすめ文献

(今回は少し多めに紹介しています)