メンバーズキャリアNOW
メンバーズキャリアNOW

匿名クラスとラムダ式、jfluteさんに講義してもらいました

2019.01.10

研修テーマは「匿名クラス&ラムダ式&Optional」 WEBエンジニアGのEです。 社内勉強会で匿名クラス&ラムダ式&Optionalをテーマに、技術顧問の久保 雅彦(@jflute)さ...

イベントエンジニア勉強会

研修テーマは「匿名クラス&ラムダ式&Optional」

WEBエンジニアGのEです。
社内勉強会で匿名クラス&ラムダ式&Optionalをテーマに、技術顧問の久保 雅彦(@jflute)さんに講義をしていただきました。
その内容を自分なりに整理してみました。

匿名クラス&ラムダ式

問題:コールバックの中で呼び出し元のローカル変数を書き換えるとコンパイルエラーになるのはなぜ?


java
package console;

public class Test {
	public static void main(String[] args) {

		// mainのローカル変数
		Boolean mainFlg = false;
		
		// ️匿名クラス
		Hoge anonymous = new Hoge() {
			@Override
			public void print() {
				// ここでエラー
				mainFlg = true;
			}
		};
		anonymous.print();
	}

	public interface Hoge {
		void print();
	}
}

書き換え部分で以下のエラーが出ます
「Local variable mainFlg defined in an enclosing scope must be final or us final」

グーグル翻訳にかけてみると
「囲みスコープで定義されたローカル変数mainFlgはfinalまたはus finalでなければなりません」

上記を理解するために・・・
– 匿名(無名)クラスとは?
– ラムダ式とは?
– コールバックとは?

匿名(無名)クラスとは

一回しか利用しないクラスを定義&インスタンス作成します。


java
public static void main(String[] args) {

                // Hogeインターフェースを実装した匿名クラスのインスタンスを作成
		Hoge anonymous = new Hoge() {
			@Override
			public void print() {
				System.out.println("TEST1");
			}
		};
		anonymous.print();

	}

	public interface Hoge {
		void print();
}

出力結果:TEST1

ラムダとは

匿名クラスを簡単に書ける記法 (ひとまずそう捉えて良い)


java
package console;

public class Test {
	public static void main(String[] args) {
		// ラムダ式
		Hoge lambda = () -> System.out.println("TEST1");
		lambda.print();
	}

	public interface Hoge {
		void print();
	}
}

出力結果:TEST1

上記のように、匿名クラスのコードと同様の結果になります。
そのため、eclipseではクイックフィックス機能を利用することで相互に変換できます。

匿名クラスを理解できていると、ラムダ式がどのような動きをしているか理解しやすいのでセットで覚えたいですね。

コールバックとは

以下のコードの出力はどのようになるでしょうか?


java
public class Test {
	public static void main(String[] args) {
		
		System.out.println("TEST_A");
		
		// ラムダ式
		Hoge lambda = () -> System.out.println("TEST_B");
		
		System.out.println("TEST_C");
		
		lambda.print();
	}

	public interface Hoge {
		void print();
	}
}

実行すると、A→C→Bの順に出力されます。
これはBの実行がlambda.print();の時点で行われるためです。

以下のコードでも実行結果はA→C→B2となります。


java
public class Test {
	public static void main(String[] args) {

		System.out.println("TEST_A");

		// ラムダ式
		// Hoge lambda = () -> System.out.println("TEST_B");
		Hoge hogeImpl = new Hoge();

		System.out.println("TEST_C");

		// lambda.print();
		hogeImpl.print();
	}

	public interface Hoge {
		void print();
	}

}

class HogeImpl implements Test.Hoge {
	public HogeImpl() {
	}

	public void print() {
		System.out.println("TEST_B2");
	}
}

両者の違いは、匿名クラス(ラムダ式)でインスタンスを作成しているか、通常のクラス定義をしているかですが、この違いは”System.out.println(“TEST_B”)を記載する場所の違い”でもあります。

* ラムダ式の場合     = printメソッドの呼び出し元で記載
* 通常のクラス定義の場合 = printメソッドの呼び出し先で記載

前置きが長くなってしまいましたが、コールバックとは呼び出し元で記載した処理(関数オブジェクト)を別の処理内で動作させるための方法のようなものです。
ラムダ式や匿名クラスは、「インターフェースに定義したメソッドの実装を呼び出し元で定義&1回限り利用するクラス定義→インスタンス作成」することができます。

冒頭の問題:コールバックの中で呼び出し元のローカル変数を書き換えるとコンパイルエラーになるのはなぜ?

コールバックとして定義した処理をどのように利用するかは、呼び出し先の処理に委ねられています。複数回利用されたり、1回も利用されないこともあり得ます。
(上記のサンプルでは呼び出し元で、ラムダ式などによって作成したインスタンスから、print()を呼び出していましたが、呼び出さないことも可能)

呼び出し元のローカル変数をコールバック関数に含めた場合、ローカル変数がどのように扱われるかは、呼び出し先に委ねられることになってしまいます。
ローカル変数は、宣言したスコープの中でのみ利用されるべきであり、別の処理内で操作されることは避けるべきです。
そのため、コンパイルエラーとなります。

匿名クラスとラムダ式の動きを理解できていれば、コンパイルエラーになる理由がよく理解できると思います。

Optional(null安全)

データは getした際に、データが取得できることが保証されていないリソースの場合、getの結果が nullであることが想定されます。そのため、取得したデータが nullではないことをチェックする必要があります。


java
public class Test {
	public static void main(String[] args) {
		String hoge = getHoge();
		
		//nullだった場合は処理しない
		if(hoge == null) {
			return;
		}
	}
	
	static String getHoge() {
		return null;
	}
}

上記のように取得後にnullチェックをすることを徹底すればエラーは発生しません。が、人間がコーディングする以上ミスはありえます。

そこで、以下のようにOptional型でラップした型(ここではString)を返却するようにします。


java
import java.util.Optional;

public class Test {
	public static void main(String[] args) {
		String hoge = getHoge(); //ここでコンパイルエラー
		
		//nullだった場合は処理しない
		if(hoge == null) {
			return;
		}
	}
	
    //返却する型をOptionalでラップする
	static Optional getHoge() {
		return null;
	}
}

こうすることで、mainの呼び出し部分で型が合わないためコンパイルエラーになります。

したがって呼び出し側は、getHogeを利用する際に以下のように呼び出すことを強制されます。


java
public class Test {
	public static void main(String[] args) {
		Optional hoge = getHoge(); //Optionalで結果を受け取る
		
		//hogeがnullではない場合、コールバック関数を実行するOptional.ifPresentを利用し、アンラップする。
		hoge.ifPresent(h -> System.out.println(h));
	}
	
	static Optional getHoge() {
		//Optional.ofNullable(null);
		return Optional.ofNullable("nullではない");
	}
}

実行結果:nullではない

Optional.ifPresentはhogeがnullではない場合のみ、引数のコールバック関数を実行するメソッドです。

このように、呼び出し側にnullチェックを強制させることで、実行時にぬるぽでアプリケーションが落ちることを防止できます(コンパイル時にエラーに気づける)。

ちなみに、上記でgetHogeからOptional.ofNullable(null)が返却された場合、実行結果は何もありません。(hoge.ifPresentで引数のコールバック関数が実行されない)

バグをコンパイル時に見つけることができるOptionalは、コードの品質に直結するのでぜひ使いこなしたいですね。

信頼してもらえるエンジニアになるために

講義の予定時間が少し余ったため、エンジニアとして仕事をしていく上で大切な心構えをお話ししていただきました。

久保さんのブログはこちら

上記に全て書いてあるのですが、私が重要だと感じた箇所は

– 質問する際は、その背景も添えて。
– 質問そのものが問題の本質であることが少ない
– 質問される側は、気付きとなる情報があれば問題の本質から解決できることが多い

です。

具体的な質問の仕方は、こちらが参考になるかと思います。

ご登壇いただいた講師

久保 雅彦氏
紹介記事はこちら