Javaの「クラスの中に書くクラス」は、staticネストクラス・内部クラス(メンバクラス)・ローカルクラス・匿名クラスの4種類です。 いちばん大きな違いは「外側のインスタンスにひも付くかどうか」で、static が付いたものだけが外側のインスタンスなしで作れます。残りの3つ(内部・ローカル・匿名)は、外側のインスタンスのフィールドやメソッドを直接使える代わりに、外側のインスタンスへの参照を内部に持ちます。

この記事では、4種類を「外側メンバへのアクセス」「生成構文」「static メンバを持てるか」「生成される class ファイル名」で横並びに比較し、すべて OpenJDK 21.0.10 で動かした結果を載せています。内部クラスが外側のインスタンスを保持し続ける様子(メモリリークの原因になるしくみ)、匿名クラスとラムダ式の this の違い、Java 16 以降の変更点、どれを選ぶかの判断フローまで、コピペして試せるコードでまとめました。

4種類の比較早見表

項目 staticネストクラス 内部クラス(メンバクラス) ローカルクラス 匿名クラス
書く場所 クラス直下(static 付き) クラス直下(static なし) メソッドやブロックの中 式の中(new 型() { ... })
名前 あり あり あり なし
外側の static メンバ 使える 使える 使える 使える
外側のインスタンスメンバ 使えない 使える 使える(※1) 使える(※1)
メソッドのローカル変数 ― ― 使える(事実上 final のみ) 使える(事実上 final のみ)
生成構文 new Outer.Nested() outer.new Inner() new Local()(定義したメソッド内だけ) new 型() { ... }(定義と同時)
外側インスタンスへの参照 持たない 持つ 持つ(※1) 持つ(※1)
static メンバ 持てる Java 16 以降は持てる Java 16 以降は持てる Java 16 以降は持てる
コンストラクタ 書ける 書ける 書ける 書けない
アクセス修飾子 public / protected / private など public / protected / private など 付けられない 付けられない
class ファイル名 Outer$Nested.class Outer$Inner.class Outer$1Local.class Outer$1.class
よくある用途 Builder、補助的なデータ型 Iterator など外側のデータをたどる部品 メソッド内だけで使う小さな型 その場かぎりの実装(コールバックなど)

※1 static メソッドの中で定義した場合は、外側のインスタンスがないので使えず、参照も持ちません。

迷ったら「まず static を付ける。外側のインスタンスのメンバを使う必要が出たら static を外す」と覚えておくと、ほとんどの場面で困りません。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java内部クラス・staticネスト・匿名クラスの違い比較表」

用語の整理|ネストクラスと内部クラスの関係

呼び方が混ざりやすいので、先に整理します。

用語 指すもの
ネストクラス(ネストしたクラス) 他のクラスの中に書かれたクラス全体。4種類すべて
staticネストクラス(static メンバクラス) static が付いたネストクラス
内部クラス(インナークラス) static が付かないネストクラス。メンバクラス・ローカルクラス・匿名クラスの3つ
外側のクラス(エンクロージングクラス) ネストクラスを囲んでいるクラス
flowchart TD
    N[ネストクラス] --> S[static ネストクラス]
    N --> I[内部クラス = インナークラス]
    I --> M[メンバクラス 非static]
    I --> L[ローカルクラス]
    I --> A[匿名クラス]

Java 言語仕様の用語では、static ネストクラスは「内部クラス(inner class)」に含まれません。一方、会話や記事では「クラスの中のクラス」全体を内部クラスと呼ぶこともあります。この記事では、「内部クラス」は static なしのメンバクラスを指すことにし、4種類をまとめて言うときは「ネストクラス」と書きます。

4種類を1つのファイルで書き比べる

前提:OpenJDK 21.0.10。Outer.java として保存し、javac -d out Outer.java → java -cp out Outer で実行します(class ファイル名を確認するため、ここでは javac を使います)。

public class Outer {
    private static String staticName = "static フィールド";
    private String instanceName = "インスタンスフィールド";

    // 1. static ネストクラス:外側のインスタンスなしで作れる
    static class StaticNested {
        void show() {
            System.out.println("[static ネスト] " + staticName);
            // System.out.println(instanceName);  // コンパイルエラー
        }
    }

    // 2. 内部クラス(メンバクラス):外側のインスタンスにひも付く
    class Inner {
        void show() {
            System.out.println("[内部] " + staticName + " / " + instanceName);
        }
    }

    void run() {
        String local = "ローカル変数";

        // 3. ローカルクラス:メソッドの中で定義する名前つきクラス
        class Local {
            void show() {
                System.out.println("[ローカル] " + instanceName + " / " + local);
            }
        }
        new Local().show();

        // 4. 匿名クラス:定義と生成を同時に行う名前のないクラス
        Runnable anonymous = new Runnable() {
            @Override
            public void run() {
                System.out.println("[匿名] " + instanceName + " / " + local);
            }
        };
        anonymous.run();

        // 参考:ラムダ式
        Runnable lambda = () -> System.out.println("[ラムダ] " + instanceName + " / " + local);
        lambda.run();
    }

    public static void main(String[] args) {
        // static ネスト:new Outer.StaticNested()
        Outer.StaticNested nested = new Outer.StaticNested();
        nested.show();

        // 内部クラス:外側のインスタンス.new Inner()
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.show();

        outer.run();

        System.out.println(nested.getClass().getName());
        System.out.println(inner.getClass().getName());
    }
}

実行結果:

[static ネスト] static フィールド
[内部] static フィールド / インスタンスフィールド
[ローカル] インスタンスフィールド / ローカル変数
[匿名] インスタンスフィールド / ローカル変数
[ラムダ] インスタンスフィールド / ローカル変数
Outer$StaticNested
Outer$Inner

外側の private フィールドにも、ネストクラスからそのままアクセスできます。 同じファイルの中に書かれた「身内」として扱われるためです。

生成される class ファイル名

ls out の結果です。

Outer$1.class
Outer$1Local.class
Outer$Inner.class
Outer$StaticNested.class
Outer.class
種類 class ファイル名の規則 今回の例
static ネストクラス 外側$名前.class Outer$StaticNested.class
内部クラス 外側$名前.class Outer$Inner.class
ローカルクラス 外側$番号+名前.class Outer$1Local.class
匿名クラス 外側$番号.class Outer$1.class
ラムダ式 class ファイルは作られない (なし)

スタックトレースに Outer$1.run と出てきたら、Outer の中の1番目の匿名クラスだと読み取れます。ラムダ式は class ファイルを生成せず、実行時にクラスが用意されます(後述の実行結果では ThisDemo$$Lambda/0x… という名前でした)。

javap で「外側への参照」を見る

javap -p -cp out 'Outer$Inner' 'Outer$StaticNested' 'Outer$1Local' 'Outer$1' の結果です。

class Outer$Inner {
  final Outer this$0;
  Outer$Inner(Outer);
  void show();
}
class Outer$StaticNested {
  Outer$StaticNested();
  void show();
}
class Outer$1Local {
  final java.lang.String val$local;
  final Outer this$0;
  Outer$1Local();
  void show();
}
class Outer$1 implements java.lang.Runnable {
  final java.lang.String val$local;
  final Outer this$0;
  Outer$1();
  public void run();
}
  • 内部・ローカル・匿名クラスには this$0 という隠れたフィールドがあり、ここに外側のインスタンスが入っています。 static ネストクラスにはありません。
  • ローカル・匿名クラスの val$local は、使っているローカル変数のコピーです。コピーなので、元の変数が後から変わると食い違いが生じます。そのため「事実上 final(一度しか代入しない)」の変数だけが使える、というルールになっています。

staticネストクラス|外側のインスタンスなしで使える

Outer.StaticNested nested = new Outer.StaticNested();

外側のクラス名を「名前空間」として借りているだけの、独立したクラスです。外側のインスタンスを作らずに生成でき、外側のインスタンスへの参照も持ちません。

外側のインスタンスフィールドを使おうとすると、コンパイルエラーになります。

public class ErrStaticAccess {
    private String instanceName = "インスタンスフィールド";
    static class StaticNested {
        void show() {
            System.out.println(instanceName);
        }
    }
}
ErrStaticAccess.java:5: エラー: staticでない変数 instanceNameをstaticコンテキストから参照することはできません
            System.out.println(instanceName);
                               ^
エラー1個

(英語表示では non-static variable instanceName cannot be referenced from a static context。)

直し方は2通りです。外側のデータが必要なら、コンストラクタやメソッドの引数で受け取ります。外側のインスタンスと常に一緒に動く部品なら、static を外して内部クラスにします。

static class StaticNested {
    private final String name;
    StaticNested(String name) { this.name = name; }   // 必要な値だけ受け取る
    void show() { System.out.println(name); }
}

内部クラス(メンバクラス)|outer.new Inner() で作る

内部クラスは、必ず外側のインスタンス1つにひも付いて作られます。外側のクラスの外や static メソッドから作るときは、外側のインスタンス.new 内部クラス名() という構文を使います。

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();     // 2行で書く
Outer.Inner inner2 = new Outer().new Inner();  // 1行でも書ける

外側のインスタンスメソッドの中では、this が外側のインスタンスなので、new Inner() だけで作れます(this.new Inner() の省略形)。

static メソッドから new Inner() と書いたときのエラー

public class ErrNewInner {
    class Inner {}
    public static void main(String[] args) {
        Inner inner = new Inner();
    }
}
ErrNewInner.java:4: エラー: staticでない変数 thisをstaticコンテキストから参照することはできません
        Inner inner = new Inner();
                      ^
エラー1個

(英語表示では non-static variable this cannot be referenced from a static context。)main メソッドは static なので、ひも付ける相手の this がありません。メッセージに「変数 this」と出るのが、このエラーの見分け方です。

直し方 コード 向いている場面
外側のインスタンスを作って .new new ErrNewInner().new Inner() 外側のメンバを本当に使う内部クラス
static を付ける static class Inner {} → new Inner() 外側のインスタンスメンバを使っていない

学習中によく出会うのは後者で、main メソッドと同じファイルに書いた補助クラスは static class にすると解決します。

同じ名前の変数があるときは 外側.this で指定する

public class Shadow {
    int x = 1;
    class Inner {
        int x = 2;
        void show(int x) {
            System.out.println("引数 x          = " + x);
            System.out.println("this.x         = " + this.x);
            System.out.println("Shadow.this.x  = " + Shadow.this.x);
        }
    }
    public static void main(String[] args) {
        new Shadow().new Inner().show(3);
    }
}

実行結果:

引数 x          = 3
this.x         = 2
Shadow.this.x  = 1

内部クラスの中の this は内部クラス自身、外側のインスタンスは 外側のクラス名.this で指します。

ローカルクラス|メソッドの中だけで使う名前つきクラス

メソッドの中で定義し、そのメソッドの中だけで使えるクラスです。匿名クラスと違って名前があるので、コンストラクタを書いたり、同じメソッド内で何度も new したりできます。

ローカル変数を使うときは、その変数が「事実上 final」である必要があります。値を書き換えようとすると、匿名クラス・ローカルクラス・ラムダ式のどれでも同じ種類のエラーになります。

public class ErrLocalVar {
    void run() {
        int count = 0;
        Runnable r = new Runnable() {
            @Override
            public void run() {
                count++;
            }
        };
        class Local {
            void up() { count++; }
        }
        Runnable l = () -> count++;
    }
}
ErrLocalVar.java:7: エラー: 内部クラスから参照されるローカル変数は、finalまたは事実上のfinalである必要があります
                count++;
                ^
ErrLocalVar.java:11: エラー: 内部クラスから参照されるローカル変数は、finalまたは事実上のfinalである必要があります
            void up() { count++; }
                        ^
ErrLocalVar.java:13: エラー: ラムダ式から参照されるローカル変数は、finalまたは事実上のfinalである必要があります
        Runnable l = () -> count++;
                           ^
エラー3個

(英語表示では local variables referenced from an inner class must be final or effectively final。)

直し方:数を数えたいなら、ローカル変数ではなくクラスのフィールドに持たせます。ローカルクラスや匿名クラスなら自分のフィールドを持てます。

class Counter {
    private int total;               // 自分のフィールドは書き換えられる
    void add(int n) { total += n; }
    int total() { return total; }
}

Java 16以降:メソッド内で record・enum・interface も定義できる

import java.util.List;
import java.util.Comparator;

public class LocalRecord {
    public static void main(String[] args) {
        // Java 16 以降:メソッドの中で record / enum / interface を定義できる
        record Score(String name, int point) {}
        enum Rank { A, B }
        interface Judge { Rank judge(Score s); }

        Judge judge = s -> s.point() >= 80 ? Rank.A : Rank.B;
        var scores = List.of(new Score("佐藤", 72), new Score("鈴木", 91), new Score("高橋", 85));
        scores.stream()
              .sorted(Comparator.comparingInt(Score::point).reversed())
              .forEach(s -> System.out.println(s + " -> " + judge.judge(s)));
        System.out.println(Score.class.getName());
    }
}

実行結果:

Score[name=鈴木, point=91] -> A
Score[name=高橋, point=85] -> A
Score[name=佐藤, point=72] -> B
LocalRecord$1Score

集計の途中だけで使う「名前と点数の組」のような小さな型は、ローカル record にするとメソッド内で完結します。 ローカル record は暗黙的に static 扱いで、外側のインスタンスやローカル変数は参照しません。

匿名クラス|定義と生成を同時に行う

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

new インターフェース名() { ... } または new 親クラス名(引数) { ... } と書き、その場で1回だけ使う実装を作ります。名前がないのでコンストラクタは書けません。書くと、メソッドと解釈されて次のエラーになります。

ErrAnonCtor.java:5: エラー: 無効なメソッド宣言です。戻り値の型が必要です。
            Greeter() {}
            ^
エラー1個

抽象クラスを継承する場合は、親クラスのコンストラクタに引数を渡せます。

public class AnonAbstract {
    static abstract class Greeter {
        private final String name;
        Greeter(String name) { this.name = name; }
        abstract String greet();
        String name() { return name; }
    }
    interface Calc { int apply(int a, int b); int twice(int a); }

    public static void main(String[] args) {
        // 抽象クラスを継承する匿名クラス(コンストラクタ引数を渡せる)
        Greeter g = new Greeter("佐藤") {
            @Override String greet() { return "こんにちは、" + name() + "さん"; }
        };
        System.out.println(g.greet());

        // メソッドが2つあるインターフェースは、ラムダでは書けないので匿名クラス
        Calc c = new Calc() {
            @Override public int apply(int a, int b) { return a + b; }
            @Override public int twice(int a) { return a * 2; }
        };
        System.out.println(c.apply(1, 2) + " " + c.twice(5));
    }
}

実行結果:

こんにちは、佐藤さん
3 10

匿名クラスとラムダ式の違い|thisが指すもの

Java 8 以降、抽象メソッドが1つだけのインターフェース(関数型インターフェース)は、匿名クラスの代わりにラムダ式で短く書けます。見た目は似ていますが、this の意味が違います。

public class ThisDemo {
    private final String name = "外側のインスタンス";

    @Override
    public String toString() { return "ThisDemo(" + name + ")"; }

    void run() {
        Runnable anonymous = new Runnable() {
            private final String name = "匿名クラスのインスタンス";
            @Override
            public void run() {
                System.out.println("匿名: this.name        = " + this.name);
                System.out.println("匿名: this のクラス     = " + this.getClass().getName());
                System.out.println("匿名: ThisDemo.this    = " + ThisDemo.this);
            }
        };
        Runnable lambda = () -> {
            System.out.println("ラムダ: this.name      = " + this.name);
            System.out.println("ラムダ: this のクラス   = " + this.getClass().getName());
            System.out.println("ラムダ: this           = " + this);
        };
        anonymous.run();
        lambda.run();
        System.out.println("ラムダ自体のクラス名   = " + lambda.getClass().getName().replaceAll("/0x[0-9a-f]+", "/0x…"));
    }

    public static void main(String[] args) {
        new ThisDemo().run();
    }
}

実行結果(javac -d out5 ThisDemo.java → java -cp out5 ThisDemo):

匿名: this.name        = 匿名クラスのインスタンス
匿名: this のクラス     = ThisDemo$1
匿名: ThisDemo.this    = ThisDemo(外側のインスタンス)
ラムダ: this.name      = 外側のインスタンス
ラムダ: this のクラス   = ThisDemo
ラムダ: this           = ThisDemo(外側のインスタンス)
ラムダ自体のクラス名   = ThisDemo$$Lambda/0x…

このときの ls out5 は ThisDemo$1.class と ThisDemo.class の2つだけで、ラムダ用の class ファイルはありませんでした。

比べる点 匿名クラス ラムダ式
this が指すもの 匿名クラス自身のインスタンス 外側のインスタンス
外側のインスタンスを指す書き方 外側のクラス名.this this
自分のフィールド(状態) 持てる 持てない
実装できる相手 インターフェース、抽象クラス、普通のクラス 関数型インターフェースだけ
抽象メソッドが2つ以上 書ける 書けない
メソッドのローカル変数と同じ名前の変数を中で宣言 できる(隠す) できない(「変数 nはすでに…定義されています」のエラー)
class ファイル 外側$番号.class が作られる 作られない
外側インスタンスへの参照 外側のメンバを使うと持つ(※2) 外側のメンバを使ったときだけ持つ

※2 Java 17 以前のコンパイル対象では、使わなくても持ちます(次の章で確認します)。

抽象メソッドが2つあるインターフェースや抽象クラスにラムダ式を代入すると、次のエラーになります。

E1.java:5: エラー: 不適合な型: Calcは機能インタフェースではありません
        Calc c = (a, b) -> a + b;
                 ^
    インタフェース Calcで複数のオーバーライドしない抽象メソッドが見つかりました
E1.java:6: エラー: 不適合な型: Greeterは機能インタフェースではありません
        Greeter g = () -> "hi";
                    ^
エラー2個

「メソッド1つのインターフェースならラムダ、それ以外や状態を持たせたいときは匿名クラス」が基本の使い分けです。

内部クラスが外側のインスタンスを保持する|メモリリークのしくみ

内部クラス・匿名クラスのインスタンスは、this$0 を通じて外側のインスタンスを参照しています。そのため、内部クラスのインスタンスだけを長く持ち続けると、外側のインスタンスもガベージコレクション(GC:使われなくなったオブジェクトの自動回収)の対象になりません。 外側が大きなデータを持っていると、使っていないつもりのメモリが残り続けます。これがメモリリークと呼ばれる状態です。

50MB の配列を持つ外側のクラスを用意し、「タスクだけを持ち続けて外側への参照は手放す」実験をします。

import java.lang.ref.WeakReference;

public class LeakDemo {
    private final byte[] big = new byte[50 * 1024 * 1024];   // 50MB のデータを持つ外側
    private int count = 0;                                   // 外側のフィールド

    class InnerTask implements Runnable {            // 内部クラス(外側のフィールドを使う)
        public void run() { count++; }
    }
    static class StaticTask implements Runnable {    // static ネストクラス
        public void run() { System.out.println("static"); }
    }

    Runnable innerTask()     { return new InnerTask(); }
    Runnable staticTask()    { return new StaticTask(); }
    Runnable anonymousTask() {
        return new Runnable() { public void run() { count++; } };
    }
    Runnable lambdaUsingField() { return () -> count++; }
    Runnable lambdaNoCapture()  { return () -> System.out.println("lambda"); }

    interface Factory { Runnable create(LeakDemo outer); }

    static void check(String label, Factory factory) throws Exception {
        LeakDemo outer = new LeakDemo();
        WeakReference<LeakDemo> ref = new WeakReference<>(outer);
        Runnable task = factory.create(outer);   // タスクだけを持ち続ける
        outer = null;                            // 外側への参照は手放す
        for (int i = 0; i < 5 && ref.get() != null; i++) {
            System.gc();
            Thread.sleep(100);
        }
        System.out.printf("%-34s 外側は %s%n", label,
            ref.get() == null ? "回収された" : "残っている(50MB を保持)");
        task.hashCode();                         // task をここまで生かしておく
    }

    public static void main(String[] args) throws Exception {
        check("内部クラス", LeakDemo::innerTask);
        check("匿名クラス(外側のフィールド使用)", LeakDemo::anonymousTask);
        check("ラムダ(外側のフィールド使用)", LeakDemo::lambdaUsingField);
        check("static ネストクラス", LeakDemo::staticTask);
        check("ラムダ(外側を使わない)", LeakDemo::lambdaNoCapture);
    }
}

実行結果(java -Xmx512m LeakDemo.java、OpenJDK 21.0.10):

内部クラス                              外側は 残っている(50MB を保持)
匿名クラス(外側のフィールド使用)                  外側は 残っている(50MB を保持)
ラムダ(外側のフィールド使用)                    外側は 残っている(50MB を保持)
static ネストクラス                      外側は 回収された
ラムダ(外側を使わない)                       外側は 回収された

WeakReference は「GC の邪魔をしない参照」で、ref.get() が null になれば回収されたとわかります。外側のフィールドを使う内部クラス・匿名クラス・ラムダは外側を保持し、static ネストクラスと外側を使わないラムダは保持しないことが確認できました。

外側を使わない内部クラスの扱い(コンパイル対象バージョンで変わる)

「外側のメンバを一切使わない内部クラス・匿名クラス」でも同じ実験をしました(Unused.java。内部クラスの run() は固定の文字列を表示するだけ)。

コンパイル方法 this$0 フィールド(javap -p) 外側のインスタンス
javac Unused.java(JDK 21 の既定) なし 回収された
javac --release 17 Unused.java あり(final Unused this$0;) 残っている

JDK 21 の javac は、外側を使っていない内部クラスから this$0 を省いていました。一方 --release 17 を指定すると this$0 が残り、外側も保持されました。コンパイルの対象バージョンによって結果が変わるので、「使っていなければ保持されない」ことを前提にせず、外側が不要なら static を付けて意図を明示するのが確実です。

保持が問題になりやすい場面と対応

場面 起きること 対応
内部クラス・匿名クラスのインスタンスを static フィールドや長く残るリストに登録する 外側のインスタンスが残り続ける static ネストクラスにし、必要な値だけ引数で渡す
リスナーやコールバックとして登録したまま解除しない 登録先が生きているかぎり外側も残る 不要になったら解除する。外側を使わない実装にする
スレッドプールやタイマーに渡した処理が長時間残る 処理が終わるまで外側が残る 処理に必要なデータだけを取り出して渡す
内部クラスのインスタンスを直列化(Serializable)する 外側のインスタンスも一緒に対象になる static ネストクラスにする

直す前と後の例です。

// 直す前:内部クラス。外側(大きなデータを持つ)への参照を持つ
class Report {
    private final byte[] data = new byte[50 * 1024 * 1024];
    private final String title = createTitle();

    class Summary {
        String text() { return "概要: " + title; }
    }
}

// 直した後:static ネストクラス。必要な title だけを受け取る
class Report {
    private final byte[] data = new byte[50 * 1024 * 1024];
    private final String title = createTitle();

    static class Summary {
        private final String title;
        Summary(String title) { this.title = title; }
        String text() { return "概要: " + title; }
    }
}

(この直す前・直した後のコードは構成の説明用で、単体では実行していません。保持の有無は上の LeakDemo で確認しています。)

Java 16以降の変更点|内部クラスにもstaticメンバを書ける

Java 15 までは、内部クラス(static なしのメンバクラス・ローカルクラス・匿名クラス)に static メンバを書けるのは定数(static final int MAX = 10; のようなコンパイル時定数)だけでした。Java 16 でこの制限がなくなり、static フィールド・static メソッド・record なども書けるようになりました(OpenJDK の JEP 395「Records」のページに、この制限を緩めたことが書かれています)。

public class StaticMember {
    class Inner {
        static int counter = 0;          // Java 16 以降は書ける
        static final int MAX = 10;       // 定数は以前から書けた
        static void hello() { System.out.println("内部クラスの static メソッド"); }
        record Pair(int a, int b) {}     // record は暗黙的に static
    }
    void run() {
        class Local {
            static int n = 5;
        }
        Runnable r = new Runnable() {
            static int m = 7;
            public void run() { System.out.println("匿名クラスの static フィールド m=" + m); }
        };
        r.run();
        System.out.println("ローカルクラスの static フィールド n=" + Local.n);
    }
    public static void main(String[] args) {
        Inner.counter++;
        Inner.hello();
        System.out.println("counter=" + Inner.counter + " MAX=" + Inner.MAX + " " + new Inner.Pair(1, 2));
        new StaticMember().run();
    }
}

実行結果(OpenJDK 21.0.10):

内部クラスの static メソッド
counter=1 MAX=10 Pair[a=1, b=2]
匿名クラスの static フィールド m=7
ローカルクラスの static フィールド n=5

同じ形(static フィールドと static メソッドを持つ内部クラス)を javac --release 15 でコンパイルすると、次のエラーになりました。

S15.java:3: エラー: 内部クラスS15.Innerの静的宣言が不正です
        static int counter = 0;
                   ^
  修飾子'static'は定数および変数の宣言でのみ使用できます
S15.java:4: エラー: 内部クラスS15.Innerの静的宣言が不正です
        static void hello() {}
                    ^
  修飾子'static'は定数および変数の宣言でのみ使用できます
エラー2個

(英語表示では Illegal static declaration in inner class S15.Inner。)

バージョン別の変更点まとめ

バージョン 変更点 この記事での確認
Java 8 ラムダ式が入り、関数型インターフェースの匿名クラスを短く書けるようになった。ローカル変数は「事実上 final」でよくなった 一般に知られた仕様。挙動は Java 21 で確認
Java 16 内部クラスに static メンバを書ける。メソッド内で record・enum・interface を定義できる --release 15 でエラー、21 で動作
Java 18 以降のコンパイル対象 外側を使わない内部クラスから this$0 が省かれる --release 17 と既定(21)で javap の結果を比較

どれを選ぶかの判断フロー

flowchart TD
    A[クラスの中にクラスを書きたい] --> B{使うのは1つのメソッドの中だけ?}
    B -- いいえ --> C{外側のインスタンスのメンバを使う?}
    C -- 使わない --> S[static ネストクラス]
    C -- 使う --> I[内部クラス]
    B -- はい --> D{その場で1回だけ使う実装?}
    D -- いいえ --> L[ローカルクラス / ローカル record]
    D -- はい --> E{抽象メソッド1つのインターフェース?}
    E -- はい --> LM[ラムダ式]
    E -- いいえ --> AN[匿名クラス]
やりたいこと 選ぶもの 理由
Builder、補助的なデータ型、定数のグループ static ネストクラス 外側のインスタンスが不要。参照も持たない
外側のコレクションをたどる Iterator 内部クラス 外側のフィールドを直接使える
メソッド内で使う「値の組」 ローカル record メソッド内で完結し、外側を参照しない
メソッド内で何度か new する、状態を持つ部品 ローカルクラス 名前とコンストラクタを持てる
ソートの比較ルール、ボタンを押したときの処理 ラムダ式 メソッド1つなら最も短い
抽象クラスの一部だけ実装、メソッド2つ以上のインターフェース 匿名クラス ラムダでは書けない
他のファイルからも広く使う 独立したクラス(別ファイル) ネストにする理由がなければトップレベルに

定番の使い方をまとめて動かす

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

public class Typical {
    // --- static ネストクラスの定番:Builder ---
    static class Task {
        private final String title;
        private final int priority;
        private Task(Builder b) { this.title = b.title; this.priority = b.priority; }
        @Override public String toString() { return title + "(優先度" + priority + ")"; }

        static class Builder {
            private String title = "無題";
            private int priority = 3;
            Builder title(String t) { this.title = t; return this; }
            Builder priority(int p) { this.priority = p; return this; }
            Task build() { return new Task(this); }   // private コンストラクタを呼べる
        }
    }

    // --- 内部クラスの定番:外側のデータをたどる Iterator ---
    static class TaskList implements Iterable<Task> {
        private final List<Task> tasks = new ArrayList<>();
        void add(Task t) { tasks.add(t); }

        @Override public Iterator<Task> iterator() { return new Itr(); }

        private class Itr implements Iterator<Task> {
            private int index = 0;
            @Override public boolean hasNext() { return index < tasks.size(); }  // 外側の tasks を直接使う
            @Override public Task next() { return tasks.get(index++); }
        }
    }

    public static void main(String[] args) {
        TaskList list = new TaskList();
        list.add(new Task.Builder().title("見積もり作成").priority(2).build());
        list.add(new Task.Builder().title("備品の発注").priority(1).build());
        list.add(new Task.Builder().build());

        for (Task t : list) {
            System.out.println(t);
        }

        // --- 匿名クラスとラムダ:その場かぎりの比較ルール ---
        List<String> names = new ArrayList<>(List.of("ペン", "ノートパソコン", "付箋"));
        names.sort(new Comparator<String>() {
            @Override public int compare(String a, String b) { return a.length() - b.length(); }
        });
        System.out.println("匿名クラス: " + names);
        names.sort((a, b) -> b.length() - a.length());
        System.out.println("ラムダ    : " + names);

        // --- ローカルクラス:メソッド内だけで使う小さな型(状態と複数メソッドを持てる) ---
        class Counter {
            private int total;
            void add(int n) { total += n; }
            int total() { return total; }
        }
        Counter counter = new Counter();
        for (String n : names) counter.add(n.length());
        System.out.println("文字数の合計: " + counter.total());
    }
}

実行結果(java Typical.java):

見積もり作成(優先度2)
備品の発注(優先度1)
無題(優先度3)
匿名クラス: [ペン, 付箋, ノートパソコン]
ラムダ    : [ノートパソコン, ペン, 付箋]
文字数の合計: 11

Task.Builder は static ネストクラスなので new Task.Builder() で作れ、Task の private コンストラクタも呼べます。Itr は内部クラスなので、外側の tasks を引数なしで使えます。「外側のインスタンスのデータを使うかどうか」で static の有無が決まっていることが、この1ファイルで見て取れます。

つまずきやすいエラーの早見表

エラー文(日本語表示) 英語表示 原因 直し方
staticでない変数 thisをstaticコンテキストから参照することはできません non-static variable this cannot be referenced from a static context static メソッドから new Inner() と書いた static class にする、または outer.new Inner()
staticでない変数 instanceNameをstaticコンテキストから参照することはできません non-static variable instanceName cannot be referenced from a static context static ネストクラスから外側のインスタンスフィールドを使った 引数で受け取る、または static を外す
内部クラスから参照されるローカル変数は、finalまたは事実上のfinalである必要があります local variables referenced from an inner class must be final or effectively final ローカル・匿名クラスの中でローカル変数を書き換えた フィールドに持たせる
内部クラスS15.Innerの静的宣言が不正です Illegal static declaration in inner class S15.Inner Java 15 以前で内部クラスに static メンバを書いた Java 16 以降にする、または static ネストクラスにする
無効なメソッド宣言です。戻り値の型が必要です。 invalid method declaration; return type required 匿名クラスにコンストラクタを書いた 初期化はフィールドの初期値で行う。必要ならローカルクラスにする
不適合な型: Calcは機能インタフェースではありません incompatible types: Calc is not a functional interface 抽象メソッドが2つ以上の型にラムダを代入した 匿名クラスで書く

よくある質問

Q. 内部クラスと static ネストクラスは、どちらを使えばよいですか? A. 外側のインスタンスのフィールドやメソッドを使わないなら static ネストクラスにします。外側のインスタンスへの参照を持たないので、単独で作れて、外側のインスタンスを残し続けることもありません。外側のインスタンスのデータを直接使う必要があるとき(Iterator など)に、static を外して内部クラスにします。

Q. outer.new Inner() という書き方はいつ使いますか? A. 内部クラス(static なし)のインスタンスを、外側のクラスの外や static メソッドから作るときです。内部クラスは必ず外側のインスタンス1つにひも付くので、「どの外側か」を outer. で指定します。外側のインスタンスメソッドの中では new Inner() だけで作れます。

Q. 匿名クラスはラムダ式にすべて置き換えられますか? A. 置き換えられるのは、抽象メソッドが1つだけのインターフェース(関数型インターフェース)を実装している場合です。抽象クラスを継承する場合、メソッドが2つ以上ある場合、フィールドを持たせたい場合は匿名クラスを使います。置き換えるときは、this の意味が「匿名クラス自身」から「外側のインスタンス」に変わる点を確認します。

Q. Outer$1.class というファイルは何ですか? A. Outer クラスの中に書かれた1番目の匿名クラスの class ファイルです。名前つきのネストクラスは Outer$名前.class、ローカルクラスは Outer$1名前.class になります。ラムダ式は class ファイルを生成しません。

まとめ

  • ネストクラスは static ネスト・内部(メンバ)・ローカル・匿名の4種類。static が付かない3つが「内部クラス(インナークラス)」。
  • static ネストクラスは new Outer.Nested()、内部クラスは outer.new Inner() で作る。static メソッドから new Inner() と書くと「変数 this を static コンテキストから参照できない」エラーになる。
  • 内部・ローカル・匿名クラスは this$0 で外側のインスタンスを保持する。長く残る場所に登録すると外側も残り続けるので、外側を使わないなら static を付ける。
  • 匿名クラスの this は自分自身、ラムダ式の this は外側のインスタンス。ラムダは class ファイルを作らない。
  • class ファイル名は Outer$Inner・Outer$1Local・Outer$1。スタックトレースを読むときの手がかりになる。
  • Java 16 以降は内部クラスにも static メンバを書け、メソッド内で record・enum・interface も定義できる。
  • 選び方は「まず static ネスト → 外側のメンバを使うなら内部クラス → 1メソッド内だけならローカル → その場かぎりならラムダか匿名クラス」。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java内部クラス・staticネスト・匿名クラスの違い比較表」

株式会社RJCでは、Javaの内部クラスやネストクラスの使い分けを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?