Java
ラムダ式
StreamAPI

今さらのJavaラムダ式とStreamAPI

More than 1 year has passed since last update.

Java8のリリースから随分経ちましたが、今まで何となくラムダ式とStreamAPI使っていたので改めてまとめてみます。

ラムダ式

ラムダ式は関数型インターフェイスと呼ばれるメソッドを一つしか持たないインターフェイスを簡単に実装できる方法といえます。

関数型インターフェイス

lispのような関数型言語やC言語のような関数ポインタを持つような言語では簡単に「関数」を引き渡すことができますが、javaでは関数がオブジェクトにならないと引き渡すことができません。そこで下のような関数型インターフェイスを用意することで関数を引き渡すことを実現します。

Func.java
package jp.foo;

public interface Func {
    public double apply(double x);
}

関数型インターフェイスとは抽象メソッドを1つだけ持つようなインターフェイスです。

関数型インターフェイスの実装

java7までは以下のように抽象メソッドをオーバーライドしていました。

Func g = new Func(){
    @Override
    public double apply(double x) {
        return x * x;
    }
};

java8では関数型インターフェイスが1つだけメソッドを持つことを利用して記述を次のように簡略化できます。これがラムダ式です。

Func f = x -> x * x;

もう少しきっちリ書くと

Func f = (x) -> {return x * x;};

一般に(引数1, 引数2,・・・) -> {処理1; 処理2;・・・}と記述します。
もちろん引数がなかったり返却がなければ() -> {}のように空で書くこともできます。
これは関数型インターフェイスのただ1つのメソッドをオーバーライドしているにほかなりません。
関数型インターフェイスではオーバーライドするメソッドが1つしかないことを利用して記述を簡潔にしているのです。

このラムダ式が威力を発揮するのは、例えば、関数型インターフェイスを引数とする(例えば、下のような)メソッドを使うときです。

FuncUser.java
package jp.foo;

public class FuncUser {
    public static void samplePrintFunc(Func f) {
        for(double x = 1.0; x <= 3.0; x += 1.0) {
            System.out.println(f.apply(x));
        }
    }
}

次のように簡単な記述で済むようになります。

FuncUser.samplePrintFunc(x -> x * x);
FuncUser.samplePrintFunc(x -> Math.sin(x));

処理が少し複雑になる場合は{// 複数の処理を記述 }で囲むことで複数の処理を記述できます。

FuncUser.samplePrintFunc(x -> { a = Math.sin(x); b = Math.cos(x); return a * b; });

処理が1つであって、関数型インターフェイスの引数の型と使いたいメソッドの引数の型が同じなら以下のように省略もできます。

FuncUser.samplePrintFunc(Math::sin);

このようにラムダ式は関数を引き渡す強力な文法だと言えます。

Stream API

上で説明したようなラムダ式を使ってコレクション処理を行うツールがStream APIです。まさにラムダ式ありきで設計されたAPIです。よく使うものを例としてまとめてみました。

forEach

forEachだけはstreamメソッドを呼びださないでも使えるようです。

Arrays.asList(new Double[] { 1.0, 2.1, 3.2, 4.3, 5.4 }).forEach(System.out::println);

filter

絞り込み検索を行ってくれるので便利です。いちいちfor文のなかにif文を書かなくていいので楽です。

Arrays.asList(new Double[] { 1.0, 2.1, 3.2, 4.3, 5.4 }).stream().filter(x -> x > 3.0).forEach(System.out::println);

map

listやsetから別のlistとsetを作りたいようなときに使います。

Arrays.asList(new Double[] { 1.0, 2.1, 3.2, 4.3, 5.4 }).stream().map(x -> x * x).forEach(System.out::println);

collect

collectは強力でfor文を書くことがほぼなくなります。

collect(supplier(forの外側に置く変数), accumulator[forの中の処理], combiner[並列処理時のときのsupplierをまとめる処理])を書きます。

Arrays.asList(new Double[] { 1.0, 2.1, 3.2, 4.3, 5.4 }).stream().collect(HashMap::new, (map, x) -> map.put(x, x * x), (map1, map2) -> map1.forEach(map2::put)).entrySet().forEach(System.out::println);

あと、Collectorsを使うのも便利です。よくやる処理がうまく抽象化されてそろっています。

Arrays.asList(new Double[] { 1.0, 2.1, 3.2, 1.0, 5.4 }).stream().collect(Collectors.toSet()).forEach(System.out::println);

mapToDouble etc...

mapTo[数値]のストリームを使うとaverageとかmaxとかminが使えるので重宝しています。

Arrays.asList(new Double[] { 1.0, 2.1, 3.2, 4.3, 5.4 }).stream().mapToDouble(x -> x).average().ifPresent(System.out::println)

標準偏差を求めてみる

標準偏差を求めてみます。

\sigma = \sqrt{\frac{1}{n} \sum_{i = 1}^n (x_i - \mu)^2}

以下の求める部分自体は2行(2,3行目)でかけます。

List<Double> sample = Arrays.asList(new Double[] { 1.0, 2.1, 3.2, 4.3, 5.4 });
double mu = sample.stream().mapToDouble(x -> x).average().getAsDouble();
double siguma = Math.sqrt(sample.stream().map(x -> Math.pow(x - mu, 2.0)).mapToDouble(x -> x).average().getAsDouble());
System.out.println(siguma);