Edited at

プログラミング初心者がラムダ式を使えるようになるまで

新人SIerがJava8のラムダ式を使える(使えるとは言っていない)ようになるまでのメモ

使い始めるまでにこけたところを列挙しています。

大雑把に言えばStreamのおかげで簡単な使い方がわかりました。


ラムダ式?さっぱり分からない!

ラムダ式はJava8以降追加された機能で、

関数型プログラミングやStreamなどと一緒に聞くことが多いかと思われます。

私もif文やfor文を理解してしばらくしたころにラムダ式と関数型プログラミングの存在を知りました。

そのときの率直な感想は

「なんだこれは、さっぱり分からない。」


なぜラムダ式が分からないのか

私の場合、大きく3点

1. ラムダ式という単語の意味が分からない

2. ラムダ式の利点が分からない

3. ラムダ式の書き方が分からない

が挙げられました。


単語の意味が分からない

一般的な人々にとってはラムダという単語になじみがないはずです。

ラムダ式を検索すると日本語wikiには


ラムダ式 (lambda expression) はラムダ計算と関係が深く、関数型言語で特によく採用されている。


と書かれています。いやラムダ計算ってなんやねん。


利点が分からない


HelloWorldLambda.java

public class Main {

public static void main(String[] args) {
Runnable r = () -> System.out.println("Hello World");
r.run(); // Hello World
}
}

よくあるHello Worldの例です。

当時の私は

「ふつうに System.out.println("Hello World"); するだけじゃダメなのか…?」

と考えてしまっていました。

そりゃそうです。この例は、ラムダ式を使うことが目的になっているのです。

これでは、ラムダ式を利用するとどういった利点があるのかが分かりません。


書き方が分からない


ExLambda.java

Runnable r = () -> System.out.println("Hello World");


(仮引数) -> {処理} という文法でラムダ式は記述されます。

ラムダ式は省略できる部分が多く、初心者にとってはそこも分からなくなる原因です。

厄介なことにこれまで学んできたJavaで -> なんて見た事がありません。

しかも、上記の例では仮引数だと言っている割に、()の中身は空っぽです。

こいつら{}はどこへ行ったんだ。

いったいこれはrに何を代入してるんだ?


で、結局どうやって理解したの?


Streamを使ってみた

ラムダ式はJava8で追加された機能ですが、

同時にStream APIという機能が追加されています。

これはまさしくラムダ式を利用するための機能、

というよりStream APIを利用しやすくするために、

ラムダ式が実装されたといっても過言ではないのでしょう。

(型推論の強化など様々な要因を含んでいるとは思いますが)

これのおかげで私はラムダ式の使い方のイメージがつきました。


.forEach()の利用

さくっと例を交えて説明していきます。


ExIterable.java

public class ExIterable {

public static void main(String[] args) {

List<String> list = new ArrayList<String>();

list.add("Apple");
list.add("Banana");
list.add("Cherry");

list.forEach(s -> System.out.println(s));
// Apple
// Banana
// Cherry
}
}


この例では Apple, Banana, Cherry を要素に持つリストの中身をそれぞれ出力しています。

ここでラムダ式を利用しているのは

list.forEach(s -> System.out.println(s));

の部分ですね。

日本語訳すると

listの中身を1つずつsに詰め込んでSystem.out.println(s)でlistの中身を1つずつ出力する。

ということになります。

これって似たようなものを聞いたことはありませんか?

そうですね拡張for文です。

for (String s : list) {

System.out.println(s);
}

上記の拡張for文は先ほどのラムダ式を用いたものと同様の結果が得られます。

とりあえずここで、ラムダ式は拡張for文の代わりになる!という1つの使い方を把握できました。

使える場所が1つでも分かると、そこから理解がしやすくなります。

でもこれだけではラムダ式を用いる利点がそこまで見えてきません。

拡張for文でいいじゃん!となってしまいます。


.mapToInt()の利用

では今度は["1", "2", "3"]を要素に持つリストを利用してその数値の合計を表示してみましょう。

Stringの形で格納されているのでintなどにパースする必要があります。


ExForEachSum.java

public class ExForEachSum {

public static void main(String[] args) {

List<String> list = new ArrayList<String>();

list.add("1");
list.add("2");
list.add("3");

System.out.println(list); // [1, 2, 3]

int sum = 0;

for (String s : list) {
sum += Integer.parseInt(s);
}

System.out.println(sum); // 6

}
}


for文より先にint sum = 0 のように先に変数を準備する必要があったり、

for文の中で加算をする必要があるなど、先ほどのようにただ出力するより手間が増えていますね。

ではこれをStreamとラムダ式を用いて書いてみます。


ExStreamSum.java

public class ExStreamSum {

public static void main(String[] args) {

List<String> list = new ArrayList<String>();

list.add("1");
list.add("2");
list.add("3");

System.out.println(list); // [1, 2, 3]

int sum = list.stream() // ListをStreamに変換する
.mapToInt(s -> Integer.parseInt(s)) // StreamをIntStreamに変換する
.sum(); // intに変換された要素を合計する

System.out.println(sum); // 6

}
}


ここでのmapToIntは受け取った要素をintに変換してStreamをIntStreamに変換するメソッドです。

そしてその引数にどうやって要素をintに変換するのか記述する必要があります。

勘のいい方はもうお分かりかと思いますが、

ここで記述した s -> Integer.parseInt(s) が要素を変換する方法を示したラムダ式です。

仮引数として受け取ったString sに対してInteger.parseIntを用いてパースしています。

intに変換して要素を渡せばよいので下記のように3倍してみたり

int sum = list.stream()

.mapToInt(s -> {
int num = Integer.parseInt(s);
return num * 3;
})
.sum();
System.out.println(sum); // 18

はたまた、要素の中身を握りつぶしてとりあえず100にしたりもできます。

int sum = list.stream()

.mapToInt(s -> {
return 100; //とりあえず100を渡してみる
})
.sum();
System.out.println(sum); // 300

詳細は割愛しますが、

ほかにもsortedとComparatorを用いてソートを行ったり、

filterを用いてフィルターをかけたり、

Streamを通じて学ぶことでラムダ式の使い方を知ることができました。

StreamAPIについては

Java Stream APIをいまさら入門

などが詳しいと思われます。

こういったことがラムダ式を用いてできるのは関数型インターフェースのおかげです。

気になった方はQiitaにも以下のような記事がありますのでご覧ください。

関数型インターフェースとは何か?(Java)

関数型インターフェースを実装したオブジェクトを記述する代わりに、ラムダ式を記述することができる、

ということが分かればなんとかやっていけると思います。


なんとか少しは使えるようになった

備忘録も兼ねて初めてQiita記事を書いてみました。

ローカル変数はFinalとして認識するラムダの仕様に悩まされたり、

ラムダの中の例外処理を外側からキャッチできなかったり(ラムダの仕様上当然か)

いくつか詰まるポイントはありますが、

Stream APIと合わせて使う際のラムダ式の破壊力は抜群です。

(極論、関数型インターフェースの名前やメソッドを覚えていなくても書けてしまいます。)

他言語で無名関数やlambdaを利用する際の基本的な考え方も学べるので、

ぜひこの記事をご覧になった方もラムダ式の利用を試みていただければと思います。

やはり意味が分からないものは使って覚えるしかありませんね。