Java lambda における this 参照について


はじめに

lambda は、匿名クラスの単なる構文糖ではなく、その実現方法の違いから発生するいくつかの違いがあります。

ここでは、lambda ブロックにおける this 参照の違いについて説明します。


匿名クラスの this 参照

匿名クラス内部で this 参照をすれば、それは匿名クラス自身を指し示します。

public class Main {

  public void run() {
    Runnable r = new Runnable() {
      @Override public void run() {
        System.out.println(this.getClass().toString());
      }
    };
    r.run();
  }

  public static void main(String[] args) {
    new Main().run();
  }

}

出力は当然 Main クラスの匿名内部クラスである Main$1 となります。

class Main$1


匿名クラス内で自身の参照が必要になるケースは多くはありませんが、リスナー登録して、実行後に自身の登録を削除するといった場合などで使うことがあります。

外部のクラスを参照したい場合は Main.this のようにして参照します。

  public void run() {
    Runnable r = new Runnable() {
      @Override public void run() {
        System.out.println(Main.this.getClass().toString());
      }
    };
    r.run();
  }

出力は以下のようになります。

class Main


lambda の this 参照

匿名クラスを Lambda で書き換えた場合には以下のようになります。

public class Main {
  public void run() {
    Runnable r = () -> System.out.println(this.getClass().toString());
    r.run();
  }
  public static void main(String[] args) {
    new Main().run();
  }
}

この場合、出力は以下のようになります。

class Main

匿名クラスの場合とは異なり、this 参照は Main クラス自体を指し示します。


これは、言語仕様に以下のように記載されています。

Unlike code appearing in anonymous class declarations, the meaning of names and the this and super keywords appearing in a lambda body, along with the accessibility of referenced declarations, are the same as in the surrounding context (except that lambda parameters introduce new names).

The transparency of this (both explicit and implicit) in the body of a lambda expression - that is, treating it the same as in the surrounding context - allows more flexibility for implementations, and prevents the meaning of unqualified names in the body from being dependent on overload resolution.

Practically speaking, it is unusual for a lambda expression to need to talk about itself (either to call itself recursively or to invoke its other methods), while it is more common to want to use names to refer to things in the enclosing class that would otherwise be shadowed (this, toString()). If it is necessary for a lambda expression to refer to itself (as if via this), a method reference or an anonymous inner class should be used instead.


ざっと内容を説明すると、

lambda ボディの中のコンテキストは、匿名クラスの場合とは異なり、前後のコンテキストと同じになる。 実際問題として lambda が内部から自身の参照が必要なケース(自身を再帰的に呼び出したり、自身の他のメソッドを呼び出したり)は稀であり、エンクロージングクラスで定義された参照の名前を使う方が一般的なケースなので、もし必要な場合にはメソッド参照か匿名内部クラスを使う必要がある。

ということです。

ちなみにこれはコンパイルエラーになります。

public class Main {
    public static void main(String[] args) {
        Runnable r = () -> System.out.println(this);
        r.run();
    }
}

static メソッドからエンクロージャの Main の参照を取得できないためです。


lambda コードの展開

先程の lambda のコードは、実行時に invokedynamic により以下のようなコードとして実行されます。

import java.lang.invoke.LambdaForm.Hidden;

public class Main {

    final static class Lambda$1 implements Runnable {
        private final Main arg$1;
        private Lambda$1(Main var1) {
            this.arg$1 = var1;
        }
        private static Runnable get$Lambda(Main var0) {
            return new Lambda$1(var0);
        }
        public void run() {
            this.arg$1.lambda$run$0();
        }
    }

    private void lambda$run$0() {
        System.out.println(this.getClass().toString());
    }

    public void run() {
        Runnable r = Lambda$1.get$Lambda(this);
        r.run();
    }

    public static void main(String[] args) {
        new Main().run();
    }

}

Lambda$1 という内部クラスを経由して、 lambda$run$0() という(暗黙的に追加された)メソッドが呼び出されます。

ここでの this はもちろん Main を指し示すため、前述の通り、lambda 内での this 参照はエンクロージングクラスになります。


lambda における invokedynamic については以下を参照してください。

blog1.mammb.com


lambda で自身を参照する

配列でなくても良いですが、なんらかのミュータブルなホルダーを経由することで自身の参照を得ることができます。

public class Main {
  public void run() {
    Runnable[] ra = new Runnable[1];
    ra[0] = () -> System.out.println(ra[0].getClass().toString());
    ra[0].run();
  }

  public static void main(String[] args) {
    new App().run();
  }
}


または以下のようにクラスフィールドとすることで、Main クラスの参照経由で自身を参照することができます。

public class Main {

  private final Runnable runnable = () ->
      System.out.println(this.runnable.getClass().toString());

  public void run() {
    runnable.run();
  }
  public static void main(String[] args) {
    new Main().run();
  }

}


おしまい。



[asin:B00VM0FMIW:detail]

Effective Java

Effective Java

Amazon