われプログラミングする、ゆえにバグあり

私だって価値を創造してみたいのです

ラムダ式で this が指すもの

java8からはラムダ式が使えて便利です。

この便利なラムダ式、匿名クラスのシンタックスシュガーのようにも思えるのですが、実際にはそうではないらしいです。(そのあたりの詳細は各々ググってください)

ただのシンタックスシュガーでない事が理由なのかわかりませんが、匿名クラスを使ったコードをラムダ式に書き換えた(書き換えようとした)場合に違いがありましたので、この記事ではそれを取り上げます。


匿名クラスを使ったコードとラムダ式を使ったコードを比較してみます。

匿名クラスを使ったコード

匿名クラスを使ったコードです。(これをラムダ式で書き換えます)

public class Hoge {

    public static void main(String[] args) {
        new Hoge().exec();
    }
    
    void exec() {
        String msg = "Hollo, World";
        Consumer<String> c = new Consumer<String>() {

            @Override
            public void accept(String msg) {
                Object ths = this;
                System.out.println(ths == Hoge.this); // => false
                System.out.println(msg);       // => Hello, World
            }
        };
        c.accept(msg);
    }
}

"this" は Comsumer の匿名クラスのインスタンスを指すので、Hoge.this とは異なります。

 

ラムダ式で書き換えようとしたコード

NetBeansでは匿名クラスが"this"を参照している場合は、ラムダ式に変換できません。なので、自分で書き換えました。(※注意。書き換えはうまくいっていません)

public class Hoge {

    public static void main(String[] args) {
        new Hoge().exec();
    }
    
    void exec() {
        String msg = "Hollo, World";
        Consumer<String> c = (msg1) -> { // "msg" は宣言済みで使えない
                Object ths = this;
                System.out.println(ths == Hoge.this); // => true
                System.out.println(msg1);     // => Hello, World
            };
        c.accept(msg);
    }
}

ラムダ式の引数の変数名に msg を使おうとすると、宣言済みで使えません。
"this" は Hoge.this を指すので、結果が変わっています
余談ですが、引数がひとつだけのケースなので、(msg1) は msg1 とも書けます。

 

ラムダ式では、"this" の参照が匿名クラスとは異なるので困りました。
では、どのようにすれば匿名クラスと同じ "this" を参照できるのでしょうか。
ちょっと書いてみました。

以下は正しい結果になります。

ラムダ式で自身を参照する

インスタンス変数にしてしまうやり方
public class Hoge {

    public static void main(String[] args) {
        new Hoge().exec();
    }

    Consumer<String> c = (msg1) -> {
        Object ths = this.c;       // Hoge.this.c でも同じ
        System.out.println(ths == Hoge.this); // => false
        System.out.println(msg1);      // => Hello, World
    };

    void exec() {
        String msg = "Hollo, World";
        c.accept(msg);
    }
}

ただし、ラムダ式初めて実行された時にクラスが生成されるらしいので、この方法だとその恩恵が受けにくくなります。

 

ローカルに参照を残すやり方
public class Hoge {

    public static void main(String[] args) {
        new Hoge().exec();
    }
    
    void exec() {
        String msg = "Hollo, World";
        Consumer<String> c;
        {
            Consumer<String>[] ref = (Consumer<String>[]) new Consumer<?>[1];
            c = (ref[0] = (msg1) -> {
                    Object ths = ref[0];   // ローカル変数から取り出す
                    System.out.println(ths == Hoge.this); // => false
                    System.out.println(msg1);      // => Hello, World
                });
        }
        c.accept(msg);
    }
}

かなり面倒です。さらに、ラムダの外部の変数を参照することになるので、効率化は望めません。(ラムダ式外部の変数を参照していなければ、シングルトン化されるらしいです)

 

まとめ

  • ラムダ式より前で使っているローカル変数名は使えない
  • "this" は外部のインスタンスを指すことに注意する。

ラムダ式で匿名クラスと同じ "this" を参照する方法で、実は簡単な方法があるかもしれません。(ご存知の方がいましたら、コメントしてれると嬉しいです)