[Java] 18. 匿名クラス(Anonymous class)とクロージャ(closure)


Study / Java    作成日付 : 2019/09/02 20:30:34   修正日付 : 2021/01/22 14:47:14

こんにちは。明月です。


この投稿はJavaの匿名クラス(Anonymous class)とクロージャ(closure)に関する説明です。


以前にクラスとインタフェースに関して説明したことがあります。

link - [Java] 7. クラスを作成する方法(コンストラクタを作成方法)

link - [Java] 12. インタフェース(interface)


改めてまとめるとJavaで最小の動作単位はクラスだし、つまりオブジェクト生成(インスタンスを生成)する単位です。そしてそのクラスを抽象化することがインタフェースです。

インタフェースを作成する時には関数を作成します。

// インタフェース
interface Testable {
  // 二つの関数で構成
  void run();
  void execute();
}
// 上のインスタンスを継承
class Test1 implements Testable {
  // run関数を再定義
  @Override
  public void run() {
    // コンソールに出力
    System.out.println("Test - run!");
  }
  // execute関数再定義
  @Override
  public void execute() {
    // コンソールに出力
    System.out.println("Test - execute!");
  }
}
// 実行クラス
public class Example {
  // インタフェースTestableを継承したクラスを受け取る。
  public static void test(Testable param) {
    // run関数を呼び出す。
    param.run();
    // execute関数を呼び出す。
    param.execute();
  }
  // 実行関数
  public static void main(String... args) {
    // Test1インスタンスを生成 - 変数タイプはインターフェースに受ける。
    Testable case1 = new Test1();
    // test関数を呼び出す。
    test(case1);
  }
}


上の例は私がTestableのインターフェースを作成してTest1のクラスに継承してインスタンスを生成してtest関数を利用してTablableインターフェースを継承したクラスのrun関数とexecute関数を実行するプログラムです。

ここまで問題がないです。私は上の例を一つのファイルに作成しましたが、実のJavaの標準ならファイル一つで一つのクラスが原則なので、Testable.java、Test1.java、Example.javaの三つのファイルを作成しなければならないです。


ここで我々はtest関数を中心で確認しましょう。test関数のパラメータはTestableインターフェースのタイプで受け取ります。つまり、Testableインターフェースを継承したクラスなら良いです。

我々がTest1のクラスを作成してインスタンスを生成してtest関数に渡って実行しました。つまり、run関数とexecute関数を呼び出しました。

また、他のTestableのケースを作成する時にはクラスをまた生成しなければならないです。Test2クラスを作成してTestableを継承してインスタンス生成してtest関数に渡します。

また、他のTestableのケースを作成する時には繰り返します。つまり、クラスが増えるし、クラスファイル(.java)が増えるという意味です。各Testケースを作成するたびにクラスを生成することは非効率です。

もしかして,100個のケースがある場合、100個のクラスを生成するという意味になります。


そして我々はクラスを作成しなくてインターフェースだけでインスタンスを生成して使う方法があります。それを匿名クラス(Anonymous class)ということです。

// インスタンス
interface Testable {
  // 二つの関数で構成
  void run();
  void execute();
}
// 実行クラス
public class Example {
  // インタフェースTestableを継承したクラスを受け取る。
  public static void test(Testable param) {
    // run関数を呼び出す。
    param.run();
    // test関数を呼び出す。
    param.execute();
  }
  // 実行関数
  public static void main(String... args) {
    // インターフェースでインスタンスを生成(匿名クラス)
    Testable case1 = new Testable() {
      // run関数を再定義
      @Override
      public void run() {
        // コンソールに出力
        System.out.println("run call!");
      }
      // execute関数を再定義
      @Override
      public void execute() {
        // コンソールに出力
        System.out.println("execute call!");
      }
    };
    // test関数を呼び出す。
    test(case1);
  }
}


上の例をみればインターフェースをnewでインタフェースを生成しました。別にクラスを作成しずに、インタフェース生成ができます。そうするとTestableケースがたくさん多くてもクラスを増やさなくても作成する方法があります。


でも、匿名クラスがただクラスやコードステップを減らす機能だけあれば、使いをお勧めしないです。

なぜなら、Javaの規約でクラス単位で作るのはプロジェクトを管理しやすいことと、可読性のため、決まっている規則ですが、匿名クラスをたくさん使うことにするとコード可読性に悪くなるためです。

でも、匿名クラスを単純にインタフェースをしやすくインスタンスを作成するための機能ではなくクロージャ機能のために使うことです。

// インタフェース
interface Testable {
  // 関数を抽象
  void run();
}
public class Example {
  // インタフェースTestableを継承したクラスを受け取る。
  public static void test(Testable param) {
    // run呼び出す。
    param.run();
  }
  // 実行関数。
  public static void main(String... args) {
    for (int i = 0; i < 100; i++) {
      // 定数化。クロージャ機能を使うため、値が定数化になりべき。
      // クロージャ値を使う匿名クラスでこの値を変更できないため。
      final int index = i;
      // Testableインスタンス生成
      Testable case1 = new Testable() {
        // run関数を再定義
        @Override
        public void run() {
          // indexはこのインスタンス外部で宣言した変数。
          System.out.println("run call! index - " + index);
        }
      };
      // test関数を呼び出す。
      test(case1);
    }
  }
}


上の例でrun関数とexecute関数に使うindexの値はTestableのインスタンスの中ではなく、main関数のfor文の値で定数化(fianl)になった整数です。

つまり、匿名クラスの内部で宣言した値ではなく、上のスタックで宣言したデータを飛び渡して使うことができます。

これがJavaのクロージャ機能(closure)ということです。


もし、匿名クラスとクロージャ機能がないと思ってクラスで実装すると思えば、コンストラクタや受け取る関数が必要になります。

// インタフェース
interface Testable {
  // 関数の抽象化
  void run();
}
// Testableインタフェースを継承
class Test1 implements Testable {
  // メンバー変数
  private int index;
  // クロージャ機能みたいにするためコンストラクタから値を受け取る。
  public Test1(int index) {
    // メンバー変数に格納
    this.index = index;
  }
  // run関数を再定義
  @Override
  public void run() {
    // コンソールに出力
    System.out.println("run call! index - " + index);
  }
}
// 実行クラス
public class Example {
  // インタフェースTestableを継承したクラスを受け取る。
  public static void test(Testable param) {
    // run呼び出す。
    param.run();
  }
  // 実行関数
  public static void main(String... args) {
    for (int i = 0; i < 100; i++) {
      // Test1クラスのインスタンスを生成
      Testable case1 = new Test1(i);
      // test関数を呼び出す。
      test(case1);
    }
  }
}


もし、渡すデータが多くなるとコンストラクタや関数などが多くなります。つまり、この場合は匿名クラスを使うほうがもっと綺麗に作成ができます。

上の例は単純に原始データタイプ(Primitive type)を使って定数化すると変更ができないですが、クラスを使うと変数は定数化になりますが、Heapのメモリのデータ(インスタンスデータ)は変更ができます。

// インターフェース
interface Testable {
  // 関数の抽象化
  void run();
}
// カウンティングクラス
class Node {
  // counting関数を呼び出した回数
  private int count = 0;
  // count値をリターン、呼び出すたびに1を増加する。
  public int counting() {
    return this.count++;
  }
}
// 実行クラス
public class Example {
  // インタフェースTestableを継承したクラスを受け取る。
  public static void test(Testable param) {
    // run呼び出す。
    param.run();
  }
  // 実行関数
  public static void main(String... args) {
    // 定数化。クロージャ機能を使うため、値が定数化になりべき。
    // クロージャ値を使う匿名クラスでこの値を変更できないため。
    final Node node = new Node();
    for (int i = 0; i < 100; i++) {
      // Testableインスタンスを生成
      Testable case1 = new Testable() {
        // run関数を再定義
        @Override
        public void run() {
          // nodeクラスはインスタンス外部で宣言した値
          // counting()関数を呼び出したらNodeクラスのcount変数は変更する。
          System.out.println("run call! index - " + node.counting());
        }
      };
      // test関数を呼び出す。
      test(case1);
    }
  }
}


上の結果でNodeクラスはnode変数で定数化になりましたが、Nodeインスタンスの中のcount値はcounting関数が呼び出すたびに変更することを確認できます。


ここまでJavaの匿名クラス(Anonymous class)とクロージャ(closure)に関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

最新投稿