前提
以降の説明で表示するソースは全て GitHub に上げてあります。
本記事の目的
Java8 で追加された機能(関数型プログラミング・ラムダ式・Stream)というものについて、
難しい説明を極力省き、下記のような記述を読みこなせるようになるのが目標です。
persons.stream()
.filter(p -> p.getAge() >= 30)
.forEach(p -> System.out.println(p.getName()));
ラムダ式とは
インターフェースに 抽象メソッド を1つだけ定義したものを 関数型インターフェース と呼びます。
この 関数型インターフェース の実装を、とことん簡潔に記述するのがラムダ式です。
たとえば、こんなインターフェースがあるとします。
interface Iface1 {
void method(int i, String s);
}
通常クラスの場合
class Face1 extends Iface1 {
public void method(int i, String s) {
System.out.println(i + s);
}
}
Iface1 if1 = new Face1();
無名クラスの場合
無名クラスとは、一時的に利用するために作られる名前の無いクラスです。
インスタンスの生成と同時にクラス定義も指定します。
Iface1 if1 = new Iface1() {
public void method(int i, String s) {
System.out.println(i + s);
}
};
簡単です。簡単ですけど
まだまだ冗長です
もっと簡潔に書けないでしょうか?
簡潔に書く上でポイントとなるのが 関数型インターフェース の特徴です。
関数型インターフェース には 抽象メソッド が1つしかありません。
1つだけならメソッド名をわざわざ実装側に書かなくてもコンパイラには判るはずです。
同様に引数の型や戻り値もコンパイラには判るはず。
そういったコンパイラには判るはずの情報を省略して必要な部分だけ記述するのが ラムダ式 です。
ラムダ式の場合
Iface1 if1 = (i, s) -> System.out.println(i + s);
矢印(->)をはさんだ左側が引数で、右側が処理になります。
処理が複数行ある場合
処理が複数行に渡る場合は {} で括ります。
Iface1 if1 = (i, s) -> {
System.out.println(i);
System.out.println(s);
};
if1.method(123, "xyz");
// => 123
// => xyz
ラムダ式の使いどころ
List を例として、ラムダ式を使うケースを見てみます。
List のデータを処理する場合、ほぼ必ずループが出てきます。
List<Integer> list = Arrays.asList(100, 200, 300, 400, 500);
for (Integer i: list) {
// 1件ごとの処理
}
これはこれでいいのですが、Java8からは forEach メソッドを使うことでより簡潔に書くことができます。
list.forEach(/*1件ごとの処理*/)
上記で全データに対してカッコ内の処理が行われます。
forEach の引数として 1件ごとの処理 を渡していますが、これは具体的に何を指定するのか。
ラムダ式とどんな関係があるのか見ていきましょう。
一件ごとの処理の渡し方
forEach に渡すのは Consumer というインターフェースのオブジェクトです。
Consumer には accept というメソッドが定義されており
public interface Consumer<T> {
void accept(T t);
この accept に1件ごとの処理を記述しておきます。
実際に Consumer を実装してみます。
List<Integer> list = Arrays.asList(100, 200, 300, 400, 500);
list.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer i) {
System.out.println(i);
}
});
// => 100 200 300 400 500 (改行あり)
上記で見たとおり Consumer で実装する抽象メソッドは accept だけです。
抽象メソッドが1つしかないインターフェース(関数型インターフェース)は ラムダ式 で書けます。
そうするとコードは下記のようになり、かなりスッキリします。
List<Integer> list = Arrays.asList(100, 200, 300, 400, 500);
list.forEach(i -> System.out.println(i));
// => 100 200 300 400 500 (改行あり)
(参考)実行時の forEach の動き
forEach は、各データを引数として Consumer の accept を呼び出してくれます。

実装例として、全データを画面表示させてみましたが、実際のプログラミングはそんな単純なことばかりではなく、条件判定を行ったり計算したり、いろいろあるはずです。
そういったいろいろな処理を行うためのメソッドや関数型インターフェースが Java8 では用意されており StreamAPI として提供されています。
StreamAPI
StreamAPI というのは、コレクションや配列、数字の集合、ファイル一覧など、データのかたまりを操作するためのライブラリです。
コレクションを StreamAPI で操作するのは非常に簡単で、前項のサンプル list を例にとると
list.stream();
で Stream が生成され、以降の処理は Stream のメソッドを呼び出すことで実行します。
様々なメソッドが Stream には用意されており、データ処理が簡単に行えます。
前項の forEach はラムダ式との組み合わせで動作しましたが、
Stream のメソッドもラムダ式と組み合わせて使用するものが多数あります。
中間操作と終端操作
Stream には 中間操作 と 終端操作 という2種類のメソッドがあります。
中間操作 は Stream の抽出や変換を行い、新たな Stream を生成します。
終端操作 は Stream に対する最終的な処理(合計算出やリスト化など)を行います。
文字だけでは分かりにくいので図で描いてみます。
たとえば、ここに性別と年齢のデータがリスト形式であるとします。

ここから男性の合計年齢を計算する場合
①リストから Stream を生成
②そこから男性だけを抽出して Stream を生成
③そこから年齢だけを取り出して Stream を生成
④その合計を求める
という流れになり、②と③が中間操作で④が終端操作に相当します。

Stream を生成し、その Stream を元に新たな Stream を生成するという作業を中間操作でくり返し、
終端操作で最終的なゴール(男性の合計年齢)に到達しています。コーディングは次のような形になります。
// コレクション.stream().中間操作().中間操作().終端操作()
// (中間操作の戻り値は Stream なので処理を次々つないでいける。)
list.stream().filter((x) -> x.getGender() == Gender.MALE).mapToInt((x) -> x.getAge()).sum();
中間操作、終端操作はすべて Stream のメソッドとして提供されています。
抽出
条件を指定して抽出を行うのが filter メソッドです。
コレクション.stream().filter (抽出ロジック)
と書けば抽出後の Stream が filter から返却されます。
filter の引数が 抽出ロジック となっていますが、それはどういうことかというと、filter に渡す引数は Predicate という関数型インターフェースと決まっており、そこに抽出ロジックを記述します。
Predicate には test というメソッドがあり
public interface Predicate<T> {
boolean test(T t);
この test メソッドに抽出の条件判定ロジック(true/falseを返す)を記述します。
下記サンプルは、数値を 300 以上という条件で抽出しています。
Predicate を 無名クラス で実装したものと ラムダ式 を使ったものと2パターンです。
無名クラス
List<Integer> list = Arrays.asList(100, 200, 300, 400, 500);
list.stream().filter(new Predicate<Integer>() {
@Override
public boolean test(Integer i) {
return i >= 300;
}
}).forEach(i -> System.out.println(i));
// => 300 400 500 (改行される)
ラムダ式
List<Integer> list = Arrays.asList(100, 200, 300, 400, 500);
list.stream().filter(i -> i >= 300)
.forEach(i -> System.out.println(i));
// => 300 400 500 (改行される)
filter に渡したラムダ式は test メソッド本体として扱われます。
一行で書ける場合は return が省略可能 です。
(参考)実行時の filter の動き
filter は Stream の各データを引数として Predicate の test を呼び出し、true となるデータで Stream を新たに生成し、戻り値とします。

集計
Stream の集計を行うときは reduce というメソッドを使います。
しかし、この reduce はちょっと分かりにくいので別の方法で実行します。
IntStream というものを使うのですが、IntStream とはそもそもなんなのか、それをさらっと説明します。
これまで Stream とひとくちに言ってきましたが、実は Stream には下記の4種類あり、その中のひとつが IntStream です。
- Stream
- IntStream
- LongStream
- DoubleStream
前項のサンプル list.stream() で生成されるのは Stream<T> です。
Stream<T> は言わば汎用的な Stream で、それに比べて IntStream , LongStream , DoubleStream は、数値(int、long、double)専用の Stream です。
平均を求める average や 合計を求める sum といったメソッドが用意されており、集計を非常に簡単に行えます。
使い方の手順として、いったん Stream<T> を生成し、それを IntStream に変換します。
変換には mapToInt というメソッドを使います。
たとえば、元データとして数値文字列のリストがあるとします。集計をとる際の流れは
① list.stream() で Stream<String> を生成し
②そのインスタンスの mapToInt メソッドで IntStream を生成し
③ IntStream の sum メソッドで合計を求める
mapToInt
mapToInt には ToIntFunction という関数型インターフェースを渡します。
ToIntFunction には applyAsInt というメソッドがあり
public interface ToIntFunction<T> {
int applyAsInt(T value);
}
この applyAsInt メソッドに 元データ → int への変換プログラムを記述します。
下記は合計を求めるサンプルですが ToIntFunction を 無名クラス で実装したものと ラムダ式 を使ったものと2パターンです。
無名クラス
List<String> list = Arrays.asList("100", "200", "300", "400");
int sum = list.stream().mapToInt(new ToIntFunction<String>() {
@Override
public int applyAsInt(String s) {
return Integer.parseInt(s); // 変換
}
}).sum();
System.out.println(sum);
// => 1000
ラムダ式
List<String> list = Arrays.asList("100", "200", "300", "400");
int sum = list.stream()
.mapToInt(s -> Integer.parseInt(s))
.sum();
System.out.println(sum);
// => 1000
(参考)実行時の mapToInt の動き
mapToInt は Stream<T> の各データを引数として ToIntFunction の applyAsInt を呼び出し、変換された int の値で IntStream を生成し戻り値とします。

filter と一緒に
filter と組み合わせれば、条件を指定して集計できます。 300 以上の値を合計してみます。
List<String> list = Arrays.asList("100", "200", "300", "400");
int sum = list.stream()
.mapToInt(s -> Integer.parseInt(s))
.filter(i -> i >= 300)
.sum();
System.out.println(sum);
// => 700
Stream をリストへ戻す
ここまでは、リストを Stream に変換して操作をする方法を見てきましたが、今度は逆に Stream をリストに変換する方法を見てみます。
List<Integer> list = Arrays.asList(100, 200, 300, 400, 500);
List<Integer> list2 = list.stream()
.filter(i -> i >= 300)
.collect(Collectors.toList());
for (Integer i : list2) {
System.out.println(i);
}
// 300 400 500 (改行あり)
collect(Collectors.toList()) で filter 後の Stream がリストに変換されています。
まとめ
ラムダ式
インターフェースに 抽象メソッド を1つだけ定義したものを 関数型インターフェース と呼ぶ。
この 関数型インターフェース の実装を、とことん簡潔に記述するのがラムダ式。
StreamAPI
コレクションや配列、数字の集合、ファイル一覧など、データのかたまりを操作するためのライブラリ。

