【Java】インナークラスについて（ローカルクラス編）

Java Goldの勉強も兼ねて。Java 17を基準にしています。

# はじめに
Javaのインナークラスは、[言語仕様](https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.1.3)によると以下のように定義されています。

* 明示的または暗黙的に`static`**でない**メンバークラス
* 暗黙的に`static`**でない**ローカルクラス
* 匿名クラス

メンバークラスについて、ネットに転がっている記事や書籍の一部では`static`なメンバークラスについてもインナークラス（staticインナークラス）と呼称していることがありますが、厳密には誤りです。<br>
本記事では2つ目のローカルクラスについて取り上げます。
1つ目の[メンバークラス](https://qiita.com/KaZu_rtr/items/5e26e257ed4aac64474e)、3つ目の[匿名クラス]()についても取り上げています。
# 記法
以下のように書けます。
```java:Outer.java
public class Outer {

	private final int outerField = 10;
	private static final int staticOuterField = 20;

	public void display() {
		class Inner {
			public void innerMethod() {
				System.out.println("Instance method: Inner class method called.");
				System.out.println("Instance method: Outer field: " + outerField);
				System.out.println("Instance method: Static outer field: " + staticOuterField);
			}
		}
		final var inner = new Inner();
		inner.innerMethod();
	}

	// イニシャライザにもローカルクラスを定義できる
	{
		class Inner {
			public void innerMethod() {
				System.out.println("Initializer: Inner class method called.");
				System.out.println("Initializer: Outer field: " + outerField);
				System.out.println("Initializer: Static outer field: " + staticOuterField);
			}
		}
		final var inner = new Inner();
		inner.innerMethod();
	}

	public Outer() {
		// コンストラクタにもローカルクラスを定義できる
		class Inner {
			public void innerMethod() {
				System.out.println("Constructor: Inner class method called.");
				System.out.println("Constructor: Outer field: " + outerField);
				System.out.println("Constructor: Static outer field: " + staticOuterField);
			}
		}
		final var inner = new Inner();
		inner.innerMethod();
	}
}
```
このように関数、コンストラクタ、イニシャライザ（初期化子）の中であればどこにでも記述することができます。クラス内の他のメンバーへのアクセスやインポートを必要とするクラスへのアクセスも可能です。<br>
なお、実際に利用することは無いと思いますが、ブロック内であればいいので以下のようにも書けます。
```java
public void foo() {
	{
		class InnerInTheBlock {
			// some code
		}
	}
}
```
ただローカルクラスを定義するだけだとローカル変数等と同様にメソッド内のみの利用に留まりますが、通常のインタフェースやクラスを継承してその型で返すことで、間接的にローカルクラスを外出しすることもできます（**※推奨はしません**）。
```java:Outer2.java
interface InnerInterface {
	void display();
}

public class Outer2 {
	public InnerInterface display() {
		class Inner implements InnerInterface {
			@Override
			public void display() {
				System.out.println("Inner class method called.");
			}
		}
		return new Inner();
	}
}
```

# 制約
ローカルクラスのアクセス修飾子は無し（`package protected`）しか使用することができません。外部からのアクセスは元々不可能なので`private`でもよいぐらいに思えますが、記述量を減らすために修飾子無しということになっていると思われます。<br>
また、`static`、`sealed`、`non-sealed`といった修飾子も記述することができません。`static`については暗黙的に`static`なローカルクラスが存在するので`static`にできないというわけではありません（後述）。`sealed`、`non-sealed`はJava 17から導入された継承先のクラスを制御するための修飾子です。本記事の主題から外れるため割愛しますが、詳細は[言語仕様](https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.1.1.2)や各種記事を参照してください。
なお、`final`は付与可能です。`class`に付与する`final`は継承を禁止するためのものですが、同じスコープ内でローカルクラスが別のローカルクラスを継承するような「やんちゃな実装」を防ぎたいということでもない限り使うことは無いでしょう。<br>

主に修飾子に関して紹介しましたが、他にも細かい制約があります。詳細は[言語仕様](https://docs.oracle.com/javase/specs/jls/se17/html/jls-14.html#jls-14.3)を確認してみてください。
# インナークラスでないローカルクラス
ローカルクラスは暗黙的に`static`でなければインナークラスということでした。一方でローカルクラスには`static`を付与することが出来ません。ではどのような場合にローカルクラスが`static`の扱いになるかというと、以下のような記述の場合が当てはまります。

```java:Outer3.java
public class Outer3 {

	private final int outerField = 10;
	private static final int staticOuterField = 20;

	public static void displayStatic() {
		// staticメソッドに定義されたローカルクラスは暗黙的にstaticになる
		class ImplicitlyStaticInner {
			public void innerMethod() {
				System.out.println("Inner class method called.");
				// 暗黙的にstaticなローカルクラスから外部のインスタンスフィールドにはアクセスできない
				// System.out.println("Outer field: " + outerField);
				System.out.println("Static outer field: " + staticOuterField);
			}
		}
		final var implicitlyStaticInner = new ImplicitlyStaticInner();
		implicitlyStaticInner.innerMethod();
	}

	// staticイニシャライザに定義されたローカルクラスは暗黙的にstaticになる
	static {
		class ImplicitlyStaticInner {
			public void innerMethod() {
				System.out.println("Inner class method called.");
				// staticメソッドと同様にインスタンスフィールドにはアクセスできない
				// System.out.println("Outer field: " + outerField);
				System.out.println("Static outer field: " + staticOuterField);
			}
		}
		final var implicitlyStaticInner = new ImplicitlyStaticInner();
		implicitlyStaticInner.innerMethod();
	}
}

```
`static`メソッドまたは`static`イニシャライザに定義されたローカルクラスが暗黙的に`static`なローカルクラスとして扱われます。上記例にもある通り、ローカルクラスからインスタンスフィールドやインスタンスメソッドにアクセスすることは出来ません。
# 具体的な利用例
一例だけ紹介しておきます。
## 利用例を確認する前に注意してほしいこと
ローカルクラスは実用上のコーディングでは書くべきでは無いと考えています。理由は主に2点あります。

1. テストが出来ないため
1. 実装の時点で特定のケースでのみ必要であっても、スコープをローカルに絞る必要性が低いため

メソッド内に定義されたクラスは当然テストすることができません。もちろん、`private`メソッドのテストを考える時のようにコードパスを意識して間接的に結果を確かめることも出来ますが、そうしないといけないと分かっていて敢えてローカルクラスを利用する価値は低いと思います。
また、2に挙げたように、スコープをテストもままならないほど絞る必要は無いと思います。また、万が一の再利用も想定して独立したクラスとして定義しておいた方が無難でもあります。どうしてもロジックを外部から利用されたくなければ、Java 9以降であればモジュールシステムを活用することができますし、パッケージを使うにしても専用のものを用意してクラスをアクセス修飾子なしとすればパッケージ外からは利用できなくなります。<br>
下記の例はあくまでJavaの言語仕様のひとつとして理解を深めるための例で、業務など実際のコーディングの際に使うべきものではないと思ってください。
## 利用例
特定のデータに対して分析を行うためのヘルパークラスを提供するという例が考えられます。
```java:Sample.java
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Sample {
	public void analyzeCustomers(final List<Customer> customers) {
		// 特定の分析に必要なローカルヘルパークラス
		class CustomerAnalyzer {
			private Map<String, Integer> regionCounts = new HashMap<>();

			void categorizeByRegion() {
				for (Customer customer : customers) {
					final var region = customer.getRegion();
					regionCounts.put(region, regionCounts.getOrDefault(region, 0) + 1);
				}
			}

			String getMostPopulatedRegion() {
				// 最も顧客数の多い地域を返す
				return regionCounts.entrySet().stream()
					.max(Map.Entry.comparingByValue())
					.map(Map.Entry::getKey)
					.orElse("Unknown");
			}
		}

		final var analyzer = new CustomerAnalyzer();
		analyzer.categorizeByRegion();
		final var topRegion = analyzer.getMostPopulatedRegion();
		System.out.println("最も顧客の多い地域: " + topRegion);
	}
}

class Customer {
	private String name;
	private String region;

	public Customer(String name, String region) {
		this.name = name;
		this.region = region;
	}

	public String getName() {
		return name;
	}

	public String getRegion() {
		return region;
	}
}
```
この例では、顧客情報のリストを引数で受け取って、その内容から顧客数が多い地域を分析するための処理をローカルクラスにまとめています。すべての処理を関数に直接書くよりは関心が分離出来ており、可読性が些か高いコードを書くことが出来ます。
# まとめ
* インナークラスとしてのローカルクラスは、コンストラクタ、非staticな関数、非staticなイニシャライザ内に定義される
* 通常クラスと異なり修飾子に多くの制限があり、外部から利用できない
* ローカルクラスに`static`は付与できないが、メソッドまたはイニシャライザが`static`だと暗黙的に`static`となりインナークラスの定義から外れる
* 実用上使用することは無いと考えてよく、あくまで言語仕様のひとつとして認識しておいた方がよい