Java Goldの勉強も兼ねて。Java 17を基準にしています。
はじめに
Javaのインナークラスは、言語仕様によると以下のように定義されています。
- 明示的または暗黙的に
static
でないメンバークラス - 暗黙的に
static
でないローカルクラス - 匿名クラス
メンバークラスについて、ネットに転がっている記事や書籍の一部ではstatic
なメンバークラスについてもインナークラス(staticインナークラス)と呼称していることがありますが、厳密には誤りです。
本記事では2つ目のローカルクラスについて取り上げます。
1つ目のメンバークラスについての記事はこちらです。
記法
以下のように書けます。
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();
}
}
このように関数、コンストラクタ、イニシャライザ(初期化子)の中であればどこにでも記述することができます。クラス内の他のメンバーへのアクセスやインポートを必要とするクラスへのアクセスも可能です。
なお、実際に利用することは無いと思いますが、ブロック内であればいいので以下のようにも書けます。
public void foo() {
{
class InnerInTheBlock {
// some code
}
}
}
ただローカルクラスを定義するだけだとローカル変数等と同様にメソッド内のみの利用に留まりますが、通常のインタフェースやクラスを継承してその型で返すことで、間接的にローカルクラスを外出しすることもできます(※推奨はしません)。
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
でもよいぐらいに思えますが、記述量を減らすために修飾子無しということになっていると思われます。
また、static
、sealed
、non-sealed
といった修飾子も記述することができません。static
については暗黙的にstatic
なローカルクラスが存在するのでstatic
にできないというわけではありません(後述)。sealed
、non-sealed
はJava 17から導入された継承先のクラスを制御するための修飾子です。本記事の主題から外れるため割愛しますが、詳細は言語仕様や各種記事を参照してください。
なお、final
は付与可能です。class
に付与するfinal
は継承を禁止するためのものですが、同じスコープ内でローカルクラスが別のローカルクラスを継承するような「やんちゃな実装」を防ぎたいということでもない限り使うことは無いでしょう。
主に修飾子に関して紹介しましたが、他にも細かい制約があります。詳細は言語仕様を確認してみてください。
インナークラスでないローカルクラス
ローカルクラスは暗黙的にstatic
でなければインナークラスということでした。一方でローカルクラスにはstatic
を付与することが出来ません。ではどのような場合にローカルクラスがstatic
の扱いになるかというと、以下のような記述の場合が当てはまります。
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点あります。
- テストが出来ないため
- 実装の時点で特定のケースでのみ必要であっても、スコープをローカルに絞る必要性が低いため
メソッド内に定義されたクラスは当然テストすることができません。もちろん、private
メソッドのテストを考える時のようにコードパスを意識して間接的に結果を確かめることも出来ますが、そうしないといけないと分かっていて敢えてローカルクラスを利用する価値は低いと思います。
また、2に挙げたように、スコープをテストもままならないほど絞る必要は無いと思います。また、万が一の再利用も想定して独立したクラスとして定義しておいた方が無難でもあります。どうしてもロジックを外部から利用されたくなければ、Java 9以降であればモジュールシステムを活用することができますし、パッケージを使うにしても専用のものを用意してクラスをアクセス修飾子なしとすればパッケージ外からは利用できなくなります。
下記の例はあくまで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
となりインナークラスの定義から外れる - 実用上使用することは無いと考えてよく、あくまで言語仕様のひとつとして認識しておいた方がよい