🍇

21.1 入れ子クラス(スタティックメンバークラス、メンバークラス、ローカルクラス、匿名クラス、クロージャなど)~Java Basic編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
  • 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
  • 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
  • 今後、フリーランスエンジニアとしてのキャリアを検討している方
  • Chat GPT」のエンジニアリングへの活用に興味のある方
  • Oracle認定Javaプログラマ」の資格取得を目指している方
  • IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

21.1 入れ子クラス

チャプターの概要

このチャプターでは、クラス内で宣言されるクラスである、入れ子クラス(Nested Class)の仕様について学びます。

21.1.1 入れ子クラスの概要

入れ子クラスとは

入れ子クラスとは、クラスの中で宣言されるクラスの総称で、Nested Classとも呼ばれます。
入れ子クラスには以下の4つの種類があります。

(1)スタティックメンバークラス
(2)メンバークラス
(3)ローカルクラス
(4)匿名クラス

この中で(2)、(3)、(4)の3つのことをインナークラスと呼びます。また入れ子クラスに対して、入れ子クラスを内包する外側のクラスをエンクロージングクラスと呼びます。

入れ子クラスの必要性

入れ子クラスは殆どのケースにおいて「必ずそれを使用しなければならない」ということはありません。いずれの種類も、入れ子クラスを使った方が便利だったり簡潔にコードが記述できたりする、という点がメリットです。そういった意味では入れ子クラスの習得はマストではありませんが、生産性を高めるための1つのオプションとして覚えておくと良いでしょう。

入れ子クラスの全体像

ここでは1つ1つの入れ子クラスの特徴を説明する前に、全体像を整理します。

【表21-1-1】入れ子クラスの全体像

種類 宣言する位置 保持できるメンバー 修飾子
(1)スタティックメンバークラス クラス宣言ブロックの下 インスタンスメンバー、スタティックメンバー staticは必須
public、protected、private、abstract、finalは適宜付与できる
(2)メンバークラス クラス宣言ブロックの下 インスタンスメンバーのみ staticは付与できない
public、protected、private、abstract、finalは適宜付与できる
(3)ローカルクラス メソッド宣言ブロックの下 インスタンスメンバーのみ staticは付与できない
public、protected、privateは付与できない
abstract、finalは適宜付与できる
(4)匿名クラス メソッド宣言ブロックの下(実装と同時にインスタンス生成する) インスタンスメンバーのみ あらゆる修飾子を付与できない

これらの入れ子クラスについて、詳細は次のレッスン以降で順次説明します。

21.1.2 スタティックメンバークラス

スタティックメンバークラスとは

スタティックメンバークラスとは、クラス宣言ブロックの中に宣言する独立したクラスです。スタティックメンバークラスは、エンクロージングクラスの名前空間の中に存在しているという点を除いて、通常のクラスと全く同様に扱われます。
通常のクラスと全く同様なのであれば、この種類のクラスの利用目的はどこにあるのでしょうか。主な目的の1つは、何らかのクラス(エンクロージングクラス)に強く関連する手続きや属性があった場合、それをより簡潔な方法でクラスの中にまとめて記述できる、という点にあります。
例えば列挙型は一般的に、特定のクラスに強く関連付き、記述量が小さくなる傾向があります。そのような場合は、列挙型をスタティックメンバークラスとして実装すると、コードの見通しが良くなります。具体的には、顧客種別(CustomerType)という列挙型を、顧客を表すCustomerクラスの中に、スタティックメンバークラスとして宣言するようなケースがこれに当たります。

スタティックメンバークラスの宣言

スタティックメンバークラスは、クラス宣言ブロックの中でclassキーワードにstatic修飾子を付与してstatic class クラス名と宣言します。
例えばFooというクラス(エンクロージングクラス)の中に、Barというスタティックメンバークラスを宣言する場合は、以下のようなコードになります。

pro.kensait.java.basic.lsn_21_1_2.Foo
public class Foo {
    private int x;
    private int y;
    public Foo(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public static class Bar {
        private int x;
        private int y;
        public Bar(int x, int y) {
            this.x = x;
            this.y = y;
        }
        public void doSomething() {
            ........
        }
    }
}

Barクラスを見てください。スタティックメンバークラスはこのように、static classというキーワードによってクラスの中に宣言します。
スタティックメンバークラスは通常のクラスと同様に、インスタンスメンバー、スタティックメンバーを保持することができます。
スタティックメンバークラスから、自身を内包するエンクロージングクラスへのアクセス方法は、何らかの特別な方法があるわけではなく、あくまでも通常のクラスと同様です。要は、スタティックメンバーであれば、直接エンクロージングクラスを介してアクセスし、インスタンスメンバーであれば、まずエンクロージングクラスのインスタンスを生成し、そのインスタンスを経由してアクセスします。

スタティックメンバークラスへの外部からのアクセス

スタティックメンバークラスは、外部に対しては「エンクロージングクラス.スタティックメンバークラス」という名前で公開されます。従って前項のBarクラスのインスタンスは、以下のように生成します。

snippet (pro.kensait.java.basic.lsn_21_1_2.Main)
Foo.Bar bar = new Foo.Bar(10, 20);
bar.doSomething();

またBarクラスのFQCNをimport文に指定すれば、Barクラスの名前だけでインスタンスを生成することもできます。

snippet
Bar bar = new Bar(10, 20);

21.1.3 メンバークラス

メンバークラスとは

メンバークラスはクラス宣言ブロックの中に宣言され、クラスのインスタンスメンバーとして扱われるクラスです。
メンバークラスの利用は、当該のエンクロージングクラスをAPIとして利用者に提供する際に、内部構造を隠蔽しながら「状態と振る舞い」をセットで渡したい、というケースが比較的よく見るケースです。この説明だけでは理解は難しいと思いますので、具体例を持って後ほど説明しますが、かなり特殊なケースに限定されるという点は間違いありません。

メンバークラスの宣言

メンバークラスは、クラス宣言ブロックの中で、通常のクラスと同様にclassキーワードによって宣言します。
例えばFooというクラス(エンクロージングクラス)の中に、Barというメンバークラスを宣言する場合は、以下のようなコードになります。

pro.kensait.java.basic.lsn_21_1_3.Foo
public class Foo {
    private int x;
    public Foo(int x) {
        this.x = x;
    }
    public class Bar { // メンバークラス
        private int y;
        public Bar() {
            this.y = x * 2; // エンクロージングクラスのメンバーにアクセス可能
        }
        public int getY() {
            return y;
        }
    }
    public Bar getBar() { // メンバークラスのインスタンスを返す
        return new Bar();
    }
}

メンバークラスの中にインスタンスメンバーは保持できますが、スタティックメンバーは保持することができません。
メンバークラスから、自身を内包するエンクロージングクラスへのアクセス可否は、通常のクラスと同様です。メンバークラス自体がインスタンスメンバーなので、ある意味当然ではありますが、インスタンスメンバーおよびスタティックメンバーに対してアクセス可能です。

メンバークラスへの外部からのアクセス

メンバークラスは、外部に対しては「エンクロージングクラス.メンバークラス」という名前で公開されます。前項のBarクラスのインスタンスは、以下のコードのように取得します。

snippet (pro.kensait.java.basic.lsn_21_1_3.Main)
Foo foo = new Foo(10);
Foo.Bar bar = foo.getBar();

外部からメンバークラスには(インスタンスメンバーなので必然的に)まずエンクロージングクラスのインスタンスを生成し、その変数を経由してアクセスします。前項のFooクラスでは、getBar()メソッドの中で、メンバークラスであるBarクラスのインスタンスを生成していました。このようにメンバークラスのインスタンスは、エンクロージングクラスのメソッドやコンストラクタで生成するケースが多いでしょう。

メンバークラスの必要性

それではここで、メンバークラスの必要性を説明するために、以下のような例を考えます。
何らかの複数のスコア(学科試験の点数など)を一元管理するためのScoreManagerクラスを作成して、それを外部にAPIとして提供します。このクラスは、内部的には複数のスコアを配列で保持し、最大値や平均値を返すためのメソッドをAPIとして提供します。具体的には以下のようなクラスです。

pro.kensait.java.basic.lsn_21_1_3.ScoreManager
public class ScoreManager {
    private int[] array;
    public ScoreManager(int... array) {
        this.array = array;
    }
    public int getMax() {
        // 最大値を返す
        ........
    }
    public double getAvg() {
        // 平均値を返す
        ........
    }
}

ここでこのクラスを拡張し「スコアを順に取り出すためのAPI」を提供することになったとします。この仕様を実現するために、メンバークラスArrayIteratorとそれを返すためのgetIterator()メソッドを、以下のように追加します。

snippet (pro.kensait.java.basic.lsn_21_1_3.ScoreManager)
public class ArrayIterator {  // メンバークラス
    private int index = 0; // インデックス
    public int next() {
        // 配列の要素を返し、インデックスを1つ追加
        // インデックスを超えたアクセスがあった場合に備えて本来はエラー処理が必要
        return array[index++];
    }
}
public ArrayIterator getIterator() { // メンバークラスを返すメソッド
    return new ArrayIterator();
}

ArrayIteratorクラスは内部的に配列でスコアを保持しており、next()メソッド呼び出しによって、スコアを順に取り出すことができます。
ScoreManagerクラスの「利用者」は、この新しいAPIを利用することで、以下のコードのようにスコアを順番に取り出すことが可能になります。

snippet (pro.kensait.java.basic.lsn_21_1_3.Main_2)
ScoreManager sm = new ScoreManager(90, 85, 70);
ScoreManager.ArrayIterator ai = sm.getIterator(); //【1】
System.out.println(ai.next());
System.out.println(ai.next());
System.out.println(ai.next());
ScoreManager.ArrayIterator ai2 = sm.getIterator(); //【2】改めて取り出す

【1】でArrayIteratorクラス(メンバークラス)を取得します。このクラスのnext()メソッドを呼び出すことで、スコアを順番に取り出します。
さてこの一連の処理は、敢えてメンバークラスを使わなくても、近しいことは実現できます。例えばスコアを順番に取り出したいのであれば、配列そのものを「利用者」に返却する方法が考えられます。ただしこの方法では「ScoreManagerクラスは内部的に配列によってスコアを管理している」ことが「利用者」から見えてしまうため、カプセル化の観点で課題が残ります。また別の方法として、ScoreManagerクラス本体にindexフィールドとnext()メソッドを実装し、スコアを順番に取り出させることも可能です。ただしこの方法では、next()メソッドを呼び出すたびにindexフィールドは加算されていきます。従って、ScoreManagerインスタンスから「インデックスが0から始まるスコアグループ」を【2】のように何度も取り出すことはできません。

21.1.4 ローカルクラス

ローカルクラスとは

ローカルクラスは、メソッド宣言ブロックの中に宣言されるクラスです。
ローカルクラスの目的は、当該メソッド内でインスタンスを生成することにより、機能を再利用することにあります。ただし実際のJavaアプリケーション開発でローカルクラスを使うケースは限定的なため、補助的な理解に留めておけば十分でしょう。
ローカルクラスは、メソッド宣言ブロックの中で、通常のクラスと同様にclassキーワードによって宣言します。
ローカルクラスの中にはフィールド、メソッドといったメンバーを保持できます。ローカルクラスの中からは、自クラスのインスタンスメンバーはもちろん、エンクロージングクラスのインスタンスメンバーにもアクセス可能です。ただしローカルクラスから自身を内包するメソッドの引数やローカル変数に対してアクセスすることはできますが、これらの変数は暗黙的にfinalの扱いになるため、更新はできません。

ローカルクラスの具体例

それではローカルクラスの具体例を見ていきましょう。以下のメソッドを見てください。

snippet (pro.kensait.java.basic.lsn_21_1_4.Main)
public static void main(String[] args) {
    //【1】ローカルクラスの宣言
    class CalcFunction {
        public int calc(int x, int y) {
            return x + y;
        }
    };
    CalcFunction cf = new CalcFunction(); //【2】インスタンス生成
    int answer = cf.calc(30, 10); //【3】メソッド呼び出し
    ........
}

このメソッドでは、まずローカルクラスCalcFunctionを宣言し【1】、そのインスタンスを生成します【2】。次に生成したインスタンスに対してメソッドを呼び出して、その結果を取得しています【3】。
これだけの処理であれば、ローカルクラスを利用する必要性は殆ど感じられないと思いますが、このメソッドの中で当該ローカルクラスのインスタンスを何度も使い回しする場合を考えると、使い道が出てくるかもしれません。

21.1.5 匿名クラス

匿名クラスとは

匿名クラスはclass宣言を必要としない名前のないクラスで、入れ子クラスの一種です。
匿名クラスを生成するためには、何らかのインタフェースが必要です。new演算子にインタフェースを指定し、その場で当該インタフェースをimplementsして匿名クラスとして実装する、というのが特徴的な点です。
匿名クラスの中にはフィールド、メソッドといったメンバーを保持できます。匿名クラスから自身を内包するメソッドのローカル変数に対してアクセスすることはできますが、これらの変数は暗黙的にfinalの扱いになるため、更新はできません。

匿名クラスの具体例

それではここで、匿名クラスの具体例を見ていきましょう。まず以下のような、抽象メソッドを1つだけ持つインタフェースがあるものとします。

pro.kensait.java.basic.lsn_21_1_5.CalcFunction
interface CalcFunction {
    int calc(int x, int y);
}

次に匿名クラスによって、このインタフェースのインスタンスを生成するためのコードを示します。

snippet (pro.kensait.java.basic.lsn_21_1_5.Main_1)
//【1】匿名クラスのインスタンスを生成する
CalcFunction cf = new CalcFunction() {
    @Override
    public int calc(int x, int y) {
        return x + y;
    }
};
int answer = cf.calc(30, 10); //【2】メソッド呼び出し

このコードでは、new演算子にCalcFunctionインタフェースを指定している、というのが特徴的な点です。そしてその後ろにブロックを記述して、その場で抽象メソッドをオーバーライドすることでインスタンスを生成し、変数cfに代入します【1】。もちろんCalcFunctionインタフェースをimplementsしたクラスを別途作成しても同じことは実現可能ですが、一度しかインスタンス生成しないのであれば、匿名クラスを使った方が簡潔です。インスタンスを生成したら、次にそのメソッドを呼び出し、結果を取得しています【2】。

匿名クラスとローカル変数

匿名クラス内では、その実装において外部に位置するローカル変数を参照することができます。それを確認するために、前項のコードを以下のように修正します。

snippet (pro.kensait.java.basic.lsn_21_1_5.Main_2)
int base = 100; //【1】ローカル変数
// 匿名クラスのインスタンスを生成する
CalcFunction cf = new CalcFunction() {
    @Override
    public int calc(int x, int y) {
        return base + x + y; //【2】
    }
};
int answer = cf.calc(30, 10); //【3】

このコードでは、ローカル変数としてbase(初期値100)を宣言しています【1】。そして匿名クラスの実装において、それを参照しています【2】。このとき変数cfのcalc()メソッドに30と10を渡すと、変数baseの値100が加算されるため140が返されます【3】。

遅延実行とクロージャ

これまでは、生成した匿名クラスのインスタンスのメソッドを直ちに呼び出していました。メソッドを呼び出すと、言うまでもなくその場で処理が実行されます。ただし場合によっては、処理をいったんどこかに溜め込み必要に応じて後から実行したい、というケースがあります。このような場合は「後から実行したい処理」を「単一のメソッドを持つクラス」としてまとめ、それを引数にして別のメソッドに渡します。前項の例では、生成したCalcFunctionインスタンスが「後から実行したい処理」に相当します。
別のメソッドに渡された処理は、その後任意のタイミングで実行されます。このように「後から必要に応じて処理を行うこと」を、遅延実行と呼びます。匿名クラスの主な利用目的に、このような遅延実行を実現することがあります。
それでは遅延実行の挙動を確認するために、以下のようなProcessorクラスを用意します。

pro.kensait.java.basic.lsn_21_1_5.Processor
public class Processor {
    public void process(CalcFunction cf) {
        System.out.println(cf.calc(30, 10));
    }
}

このクラスのprocess()メソッドは、CalcFunctionインスタンスを受け取ります。CalcFunctionインスタンスを受け取った後、どういったタイミングや条件でこのインスタンスを利用するかは実装次第です。ここでは直ちにcalc()メソッドを呼び出し、その結果をコンソールに表示していますが、要件に応じて遅延実行が可能です。
次にこのprocess()メソッドに、匿名クラスのインスタンスを渡してみましょう。

snippet (pro.kensait.java.basic.lsn_21_1_5.Main_3)
int base = 100; // ローカル変数
// 匿名クラスのインスタンスを生成する
CalcFunction cf = new CalcFunction() {
    @Override
    public int calc(int x, int y) {
        return base + x + y;
    }
};
Processor p = new Processor();
p.process(cf);

このコードを実行すると、どのような計算が行われるでしょうか。匿名クラスの実装から、calc()メソッド内の計算処理はbase + x + yになります。ただし変数baseは、CalcFunctionインスタンスに内包されているわけではなく、ローカル変数としてCalcFunctionインスタンスの外側に位置しています。そう考えると、匿名クラスがprocess()メソッドに渡された後、process()メソッド内で後からcalc()メソッドを呼び出しても、もはや変数baseを参照することはできないのでは、と感じるかもしれません。ところが実際には、変数baseの値は100として認識され、コンソールには計算結果として140が表示されるのです。
なぜこのようなことが実現できるのでしょうか。それはこの匿名クラスの実装で変数baseを参照した時点で、その値(100)がキャプチャされ、CalcFunctionインスタンスの中に閉じ込められたためです。
このように、匿名クラスの中にローカル変数の値を閉じ込めてしまう仕組みを、クロージャと呼びます。なお匿名クラス内からローカル変数にアクセスがあると、その変数は暗黙的にfinalになり、値を更新しようとするとコンパイルエラーになります。これは、ローカル変数の値を後から更新されると、クロージャを実現できないためです。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. 入れ子クラスの種類や利用目的について。
  2. スタティックメンバークラスの記述方法や使い方について。
  3. メンバークラスの記述方法や使い方について。
  4. ローカルクラスの記述方法や使い方について。
  5. 匿名クラスの記述方法や使い方について。
  6. 遅延実行の意味やクロージャの仕組みについて。

Discussion