Java SE 8で導入された構文であるラムダ式の使い方についてです。 去年の記事でJava8とラムダ式についても別記事で書きたいと言っておきながらやっと手が付けられました。
ラムダ式とは
Java8から関数型インターフェース(実装が必要なメソッドを1つだけ持つインターフェース)の変数に代入する箇所をラムダ式で渡すことが出来るようになりました。基本文法は以下の通りです。
( 引数 ) -> { 処理 }
これはどういう事かというと、関数型インターフェースのひとつである Consumer.class の場合、実装が必要なメソッドはaccept(T t)
なので以下のように記述しますが、
Consumer<String> cons = new Consumer<String>() { @Override public void accept(String str) { System.out.println(str); } };
ラムダ式で記述した場合は以下のように短く記述することが出来ます。関数型インターフェースは実装が必要な抽象メソッドが1つだけなので、ラムダ式がどのメソッドを実装することになるのかは選択の余地なく決まるのです。そのため、抽象メソッドが複数あるとコンパイルエラーになりますので、ラムダ式で記述することは出来ません。
Consumer<String> cons = ( str ) -> { System.out.println(str); };
以上のことからラムダ式とは内部的には匿名クラスと同じであることがわかると思います。
ラムダ式の構文
ラムダ式の文法について、基本文法は上述した( 引数 ) -> { 処理 }
ですが書き方がいくつかあります。慣れないうちは一度匿名クラスで実装してからラムダ式に直すようにすると分かりやすいかもしれません。
引数部分の記述
() -> { 処理 } // 引数が0個の場合 (str) -> { 処理 ] // 引数が1個の場合 str -> { 処理 } // 引数が1個の場合は括弧の省略可 (str, n) -> { 処理 } // 引数が2個の場合 (String str, int n) -> { 処理 } // 型は推論されるので不要だが記述してもOK
処理部分の記述
( 引数 ) -> System.out.println(str) // 処理が一文の場合、returnも波括弧も不要 ( 引数 ) -> { System.out.println(str); // 波括弧を使って複数の処理文を記述 return n; // 戻り値が必要な関数型インターフェースの場合はreturnを記述 }
使い方
Iterable インタフェースに追加された forEach メソッドで実際に試してみたいと思います。forEach メソッドの引数にはConsumer<? super T> action
を渡さなくてはいけません。
import java.util.Arrays; import java.util.List; import java.util.function.Consumer; public class LambdaTest { public static void main(String[] args) { List<String> strs = Arrays.asList("hoge", "fuga", "bar"); strs.forEach(new Consumer<String>() { @Override public void accept(String str) { System.out.println(str); } }); } }
実行結果は次の通りです。
hoge fuga bar
これをラムダ式で記述すると以下のように短く記述できます。シンプルですね。
... public static void main(String[] args) { List<String> strs = Arrays.asList("hoge", "fuga", "bar"); strs.forEach(str -> System.out.println(str)); } ...
また、以下のようにラムダ式(関数型インターフェース)を実装したインスタンスを渡す事も可能です。
... public static void main(String[] args) { List<String> strs = Arrays.asList("hoge", "fuga", "bar"); Consumer<String> cons = str -> System.out.println(str); strs.forEach(cons); } ...
変数のスコープ
ラムダ式の外側で定義された変数を使用する場合、参照する事は可能ですが、値を代入するとコンパイルエラーになります。
これは実質的には匿名クラスと同じである事を考えると、匿名クラスの外側で定義された変数を使用する場合、final もしくは実質的 final である必要があるためです。Java8からは final を省略できるようになったために迷いやすいポイントです。
int cnt = 0; strs.forEach(str -> { System.out.println(str + cnt); // 参照なのでOK cnt++; // 代入はコンパイルエラー });
外側で変数への代入をした場合でも、ラムダ式の内部で参照しているとコンパイルエラーになってしまいます。
int cnt = 0; strs.forEach(str -> { System.out.println(str + cnt); // 参照でもコンパイルエラー }); cnt++; // 外側で代入
例外処理
ラムダ式の例外処理は今までの感覚で以下のように書くと、例外を処理しろと言われてコンパイルエラーになってしまいます。
try { strs.forEach(str -> { doMethod(); // Exceptionをthrowするメソッド }); } catch (Exeption e) { e.printStackTrace(); }
一度catchしてthrowしようとしても出来ません。
try { strs.forEach(str -> { try { doMethod(); // Exceptionをthrowするメソッド } catch (Exception e) { throw e; // コンパイルエラー } }); } catch (Exception e) { e.printStackTrace(); }
このような場合に取れる方法は2つのみです。例外をラムダ式の内部で処理するか、ラムダ式内部で例外をcatchして非チェック例外として再スローするかです。
try { strs.forEach(str -> { try { doMethod(); // Exceptionをthrowするメソッド } catch (Exception e) { throw new RuntimeException(e); // 非検査例外でラップ } }); } catch (RuntimeException re) { Throwable e = re.getCause(); // 元の例外を取り出す }
ただし、チェック例外をスローする関数型インターフェースを作成した場合のみ、外側でcatchする方法が可能です。以下はMyConsumerという例外をスローする関数型インターフェースを作成して外側でcatchできるか試してみました。
@FunctionalInterface public interface MyConsumer<T, R extends Throwable> { void accept(T t) throws R; } ... MyArrayList<String> myArray = new MyArrayList<>(strs); try { myArray.forEach(new MyConsumer<String, IOException>() { @Override public void accept(String t) throws IOException { throw new IOException(t); } }); } catch (Throwable e) { e.printStackTrace(); }
関連記事
Java8のforEachを使った繰り返し処理について - TASK NOTES
Java8のStream APIの使い方(Streamの生成編)
Java8のStream APIの使い方(中間操作編① - filter, map)
Java8のStream APIの使い方(中間操作編② - flatMap, distinct, limit, skip)
Java8のStream APIの使い方(中間操作編③ - sorted, peek)
Java8のStream APIの使い方(終端操作編① - anyMatch, allMatch, noneMatch) - TASK NOTES