Javaで無名関数

前置き

Javaに無名関数という機能はありませんが、無名インナークラスを使えば、それっぽいことはできます。あと、クロージャと高階関数についてもちょっとだけ。

[2014-10-16]Java8ではラムダが使えるようになってますね。モナド的な操作(flatMapとか)も導入されてます。

本文

定石

  • インターフェイス(あるいは抽象クラス)を使って、実装クラス(あるいは子クラス)に、あるメソッドの実装(あるいはオーバーライド)を強要する
  • 無名インナークラスとして、そのインターフェイス(あるいは抽象クラス)の実装クラス(あるいは子クラス)を定義
  • 定義と同時にオブジェクト生成(new)する
  • 必要に応じてクロージャに仕立てる

このようなパターンを見つけたら、このメソッドは無名関数的な存在と考えて良いと思います。

例題

本記事の説明で使う例題について。GUIクラスライブラリを作っており、その中のButtonクラスを設計していると想定します。ボタンが押されたら関数をコールバックするような仕組みを入れたいですよね。Buttonクラスには、コールバック関数を登録するメソッドが必要です。名前はsetCallback()としましょう。このメソッドの引数をどうするか、というのが問題です。

Runnableインターフェイスを使ってみる

Runnableインターフェイスには、run()という引数なしreturn値なしのメソッドが定義されています。つまり、実装クラスにrun()の実装を強要しているわけです。RunnableはThreadクラスと一緒に使うという印象を持ちがちですが、実際はもっと汎用的なインターフェイスです。Buttonクラスにも使えます。

public class Button {
    private Runnable onPush;

    public void setCallback(Runnable cb) {
        onPush = cb;
    }
}

ボタンが押されたときに、onPush.run();とすれば、コールバック関数を呼ぶことができます。Buttonの利用者側のコードは以下のような感じになります。

public class Foo {
    public void foo() {
        Button b = new Button();
        b.setCallback(new Runnable() {
            public void run() {
                ...
            }
        });
    }
}

一見、Runnableをnewしているように見えますが、実際はRunnableをimplementsした無名インナークラスを定義し、run()を実装し、その無名クラスのオブジェクトを生成(new)して、setCallback()の引数に渡しています。

無名じゃない(名前付きの)インナークラスで書くなら、以下のようになります。

public class Foo {
    public void foo() {
        Button b = new Button();
        b.setCallback(new MyRunnable());
    }

    private class MyRunnable implements Runnable {
        public void run() {
            ...
        }
    }
}

こう書いてしまうと、もはや無名関数的とは言えませんね。どっちを使うかは好みの問題かもしれませんが、特にrun()の内容がシンプルな場合などは、軽快な印象の無名インナークラス(無名関数)の方が適していると思います。

Listenerインターフェイスを使ってみる

Runnableの場合、run()に引数やreturn値が無いのがネックになるかもしれません。また、汎用的すぎて誤用の心配がありそうです。そういった場合は、専用のListenerクラスを定義しましょう。

public class Button {
    private OnPushListener onPushListener;

    public void setCallback(OnPushListener listener) {
        onPushListener = listener;
    }

    public interface OnPushListener {
        public boolean onPush(int keyFlag);
    }
}

利用者側のコードは以下のような感じになります(無名クラスの例のみ示します)。

public class Foo {
    public void foo() {
        Button b = new Button();
        b.setCallback(new OnPushListener() {
            public boolean onPush(int keyFlag) {
                ...
            }
        });
    }
}

クロージャ

コールバック関数を実装するときに、利用者側の情報にアクセスしたい場合、どうすれば良いでしょうか? 例えば、前述のOnPushListenerのonPush()の中でButtonオブジェクトを参照したい場合を考えてみましょう。

public class Foo {
    public void foo() {
        Button b = new Button();
        b.setCallback(new OnPushListener() {
            public boolean onPush(int keyFlag) {
                if ( keyFlag & Button.KEYFLAG_SHIFTKEY != 0 ) {
                    b.setEnable(false);       // ボタンを無効化する。
                                              // ここでコンパイルエラーになる。
                    return true;
                }
                return true;
            }
        });
    }
}

これは残念ながらうまくいきません。なぜなら、onPush()の中では、その外側のローカルオブジェクトbにアクセスできないからです。しかし、これには解決策がいくつかあります。

1つ目は、アウタークラスのフィールドを使う方法です。無名インナークラスはインナークラスの一種なので、アウタークラス(この例ではFoo)のフィールドにはアクセスできます。

public class Foo {
    private Button button;

    public void foo() {
        button = new Button();
        button.setCallback(new OnPushListener() {
            public boolean onPush(int keyFlag) {
                if ( keyFlag & Button.KEYFLAG_SHIFTKEY != 0 ) {
                    button.setEnable(false);       // ボタンを無効化する。
                    return true;
                }
                return true;
            }
        });
    }
}

2つ目の方法では、名前付きインナークラスを使います。コンストラクタ経由でbを渡してやれば良いですね。

public class Foo {
    public void foo() {
        Button b = new Button();
        b.setCallback(new Listener(b));
    }

    private class Listener implements OnPushListener {
        private Button button;
        public Listener(Button b) {
            button = b;
        }
        public boolean onPush(int keyFlag) {
            if ( keyFlag & Button.KEYFLAG_SHIFTKEY != 0 ) {
                button.setEnable(false);       // ボタンを無効化する。
                return true;
            }
            return true;
        }
    }
}

最後がクロージャです。クロージャは、無名関数をサポートするプログラミング言語で良く見かけるテクニックです。(厳密な定義はともかく私の理解では、)クロージャとは、それを生成した側のコンテキストを共有する無名関数のことです。また、生成した側がコンテキストを失ったあとも、クロージャはコンテキストにアクセスできます。

意味不明ですか? 例で示しましょう。そもそも、やりたかったのはこれでした。

public class Foo {
    public void foo() {
        Button b = new Button();
        b.setCallback(new OnPushListener() {
            public boolean onPush(int keyFlag) {
                if ( keyFlag & Button.KEYFLAG_SHIFTKEY != 0 ) {
                    b.setEnable(false);       // ボタンを無効化する。
                                              // ここでコンパイルエラーになる。
                    return true;
                }
                return false;
            }
        });
    }
}

つまり、無名関数(onPush)に、それを生成した側(foo)のコンテキスト(b)を共有させたいわけです。そして、生成側(foo)が終了してbを失ったあとも、クロージャ(onPush)はbにアクセスできなければなりません。クロージャをサポートする言語では、これはナチュラルに実現されます。JavaScriptの例を書いておきます。

function foo() {
  var b = new Button();
  b.setCallback(function(keyFlag) {
    if ( keyFlag & KEYFLAG_SHIFTKEY != 0 ) {
      b.setEnable(false);        // bにアクセスできる。
      return true;
    }
    return false;
  });
}

一方Javaでは、明示的にはクロージャがサポートされてません(たぶん)。そもそも無名関数自体がサポートされてないわけですから。しかし、クロージャっぽいことは実現できます。前置きが長くなりましたが、bをfinalとして宣言すればOKです。

public class Foo {
    public void foo() {
        final Button b = new Button();        // finalで宣言。
        b.setCallback(new OnPushListener() {
            public boolean onPush(int keyFlag) {
                if ( keyFlag & Button.KEYFLAG_SHIFTKEY != 0 ) {
                    b.setEnable(false);       // ボタンを無効化する。
                    return true;
                }
                return false;
            }
        });
    }
}

高階関数

昔のブログにも載せたのですが、無名インナークラスを応用すると、高階関数(つまり、関数を引数にとる関数)っぽいコードが書けます。

例えば、2つの整数と、演算内容を実装した関数を引数にとる関数を書きたいとしましょう。JavaScriptなら、こんな感じです。

function hof(a, b, f)     // Higher Order Function.
{
  return f(a, b);
}

var sum = hof(12, 8, function(a, b) { return a + b; });    // 20.

Javaで似たようなことをしたければ、こんな感じになるでしょう。

abstract class HOF {
    // ホントはfを引数として取りたいが、Javaでは許されないので、
    // 抽象メソッドで代替する。
    public int hof(int a, int b) {
        return f(a, b);
    }
    abstract int f(int a, int b);
}

public class Main {
    public static void main(String[] args) {
        int sum = new HOF() {
            int f(int a, int b) { return a + b; }
        }.hof(12, 8);       // 20.
    }
}
Last modified:2011/05/30 08:28:45
Keyword(s):
References:[言語Tips]
This page is frozen.