前提
以降の説明で表示するソースは全て 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
コレクションや配列、数字の集合、ファイル一覧など、データのかたまりを操作するためのライブラリ。