java8のラムダ式を理解する

 java8が公開されてから2年以上が経過しました。
次期バージョンのjava9の公開まで半年余りとなりましたが、いまだにjava8の有用性を取り入れられていないプロジェクトもあると思います。

 今回解説するラムダ式は、従来の記述方法に比べると、その異質さから、可読性、学習コストなどを理由に避けられてきました。

 しかし、Haskellやscalaに代表される関数型言語の流行もあり、C#やjava、ECMAScript6でのアロー関数サポートなど、世の中の主要なプログラミング言語においては、より簡潔にコーディング可能なラムダ式(アロー関数)を積極的に取り入れるという方向に向かっています。ラムダ式のコーディング技術が必修科目になるのも、そう遠くない未来かもしれません。

従来の構文との比較

 たとえば、ある文字列をスペースで切り分け、特定の文字数の単語のみコンソールに出力するプログラムを作ってみます。

public class Main {

    private static final String STR = "The quick brown fox jumps over the lazy dog";

    public static void main(String[] args) throws Exception {
       for(String s : STR.split(" ")){
           if(s.length() == 5){
               System.out.println(s);
           }
       }
    }
}

結果

quick
brown
jumps

 STRを” “でsplitすることで文字列の配列を作り、それに対して拡張for文(forEach)を回しています。

 for文の中でif文による分岐を行い、もし文字数が5であればprintlnで出力します。

 では、java8流儀で書き直してみましょう。

import java.util.*;

public class Main {

    private static final String STR = "The quick brown fox jumps over the lazy dog";

    public static void main(String[] args) throws Exception {
        Arrays.stream(STR.split(" "))
            .filter(s -> s.length() == 5)
            .forEach(System.out::println);
    }
}

 文字数は増加しましたが、余計なネストが減りました。これが最新式の構文ですね。

解説

 それでは、順に解説していきます。

Arrays.stream(STR.split(" "))

 STRを” “でsplitした配列を、Arrays.streamメソッドでstreamに変換しています。

.filter(s -> s.length() == 5)

 上記で返却されたStreamをそのまま利用し、filterメソッドによって判定しています。

 この引数に入っている不思議な構文が、ラムダ式です。詳しい解説は、後述いたします。

.forEach(System.out::println);

 filterメソッドで得られた結果に対し、forEach文を発行しています。

 ここも先ほどとは違った構文が出現していますが、後ほど解説します。

ラムダ式

 さて、先ほどのラムダ式を抜粋してみます。

s -> s.length() == 5

 これは、sを引数にとり、その文字数を取得し、5であればtrueを返し、それ以外ならfalseを返すラムダ式です。

しかし、あまり直観的ではありません。
「sって何?何が入ってるの?」
「条件判定だけして、returnしていないけど、何がどう使ってるの?」
といった疑問もあると思います。

 前述のラムダ式は、簡略化された形式であり、本来は以下の構文で記述されます。

(引数) -> {処理}

 つまり、以下のふたつは等価になります。

s -> s.length() == 5
(s) -> {return s.length() == 5;}

 ここで、javascriptを思い出してみましょう。
同じように書いてみます。

function(s){
    return s.length === 5;
}

 ここでお気づきだと思いますが、何も難しいことはありません。ラムダ式の正体は、ただの匿名関数だったのです。

 つまり、sには、呼び元で得られたオブジェクトが入っています。今回のfilterメソッドは、リストの内部をひとつずつ引数として渡し、ラムダ式(匿名関数)で検査し、trueを得られたオブジェクトを返す、という処理になっているわけです。

 ところで、内部的にはどうなっているのでしょうか?マニアックな構文になりますが、以下も問題なく動作します。

import java.util.*;
import java.util.function.*;

public class Main {

    private static final String STR = "The quick brown fox jumps over the lazy dog";

    public static void main(String[] args) throws Exception {
        Arrays.stream(STR.split(" "))
            .filter(new Predicate<String>(){
                @Override
                public boolean test(String s){
                    return s.length() == 5;
                }
            })
            .forEach(new Consumer<String>(){
                @Override
                public void accept(String s){
                    System.out.println(s);
                }
            });
    }
}

 可読性も何もあったものではありませんが、内部仕様を知る良いケースですね。

 つまり、ラムダ式で記述されたものをコンパイラは上記のように解釈して、あたかも関数を渡しているかのようにオブジェクトを生成するわけです。

 あとは、filterメソッドやforEachメソッド側で、渡されたオブジェクトのtestメソッドや、acceptメソッドなどを呼べば良いということになります。

 これにより、関数型プログラミングに近いことがjavaでも出来るようになりました。

メソッド参照

 最後に、メソッド参照について言及しておきます。

 メソッド参照とは、以下のような構文をいいます。

System.out::println

 コロン(:)2つ繋ぎであらわされるこの構文は、メソッドへの参照を渡しています。

 これは、ラムダ式で受ける引数が1つであり、更に渡すメソッドの引数も1つ、実行したい命令も1つであるという前提の上に成り立ちます。

 上記は、以下と等価の結果になります。

s -> System.out.println(s)

最後に

 このように、java8のラムダ式では、コーディングを楽にする様々な手段が用意されています。

 これらを前提にコーディングするだけで、関数型プログラミングへの理解もスムーズに出来るようになるかと思います。

 また、最初に述べたように、ラムダ式は複数のプログラミング言語でサポートされていますので、もし他のプログラミング言語を利用することになっても、スムーズに移行できるようになるはずです。

 新しい技術を学ぶことは、コストがかかるものですが、それ以上のリターンが見込めるものが多いです。
技術を選定する際は、恐れず、最適な手段を突き詰めたいものですね。