Java のラムダ式と StreamAPI。入門レベルを図解で

Java8で出てきた関数型プログラミングとかラムダ式とか Stream というものについて、概略をまとめてみました。

関数型とはなにか、みたいな難しいことは考えず、とりあえずこんな感じの記述を読みこなせるようになるのが目標です。

- 目次 -

スポンサーリンク

ラムダ式とは

インターフェースに 抽象メソッドを1つだけ 定義したものを 関数型インターフェース と呼びます(Java8で登場した用語)。この関数型インターフェースの実装を、とことん簡潔に記述するのがラムダ式です。

たとえば、こんなインターフェースがあるとします。

無名クラスでの実装はこうなります。

簡単です。簡単ですけど、なんか色々ごちゃついています。もっと簡潔に書けないでしょうか。

簡潔に

簡潔に書く上でポイントとなるのが関数型インターフェースの特徴です。関数型インターフェースには抽象メソッドが1つしかありません。1つだけなら、メソッド名をわざわざ実装側に書かなくてもコンパイラには判るはずです。そして引数の型や戻り値もコンパイラには判るはずです。そういったコンパイラには判るはずの情報を省略して 必要な部分だけ 記述するのがラムダ式です。

で、ラムダ式だとこうなります。

矢印(->)をはさんだ左側が引数で、右側が処理になります。

無名クラスの実装でもラムダ式でもメソッドの呼び出し結果は同じで、こうなります。

処理が複数行の場合

処理が複数行にわたる場合は {} でくくります。

ラムダ式の使いどころ

List の処理を例として、ラムダ式を使うケースを見てみます。

List のデータを処理する場合、ほぼ必ずループが出てきます。

これはこれでいいのですが、Java8から別の書き方ができるようになっています。forEach というメソッドを使う方法で

これで全データに対してカッコ内の処理が行われます。しかも、オプションを指定すればマルチCPU化まで裏でやってくれます。

forEach の引数として ”一件ごとの処理” を渡していますが、これは具体的にどうするのでしょうか。ラムダ式とどんな関係があるのでしょうか。

一件ごとの処理の渡し方

forEach に渡すのは Consumer というインターフェースのオブジェクトです。Consumer には accept というメソッドが定義されており

この accept に一件ごとの処理を記述しておきます。

forEach-Consumer

実際に Consumer を実装してみます。

上で見たとおり、Consumer で実装する抽象メソッドは accept だけです。抽象メソッドがひとつしかないインターフェース(関数型インターフェース)は ラムダ式 で書けるのでした。そうすると、コードはこうなります。かなりスッキリします。

(参考)実行時の forEach の動き

forEach は、各データを引数として Consumer の accept を呼び出してくれます。

forEach-Consumer

実装例として、全データを画面表示させてみましたが、実際のプログラミングはそんな単純なことばかりではなく、条件判定を行ったり計算したり、いろいろあるはずです。そういったいろいろな処理を行うためのメソッドや関数型インターフェースが Java8 では用意されており StreamAPI として提供されています。

Stream

StreamAPI というのは、コレクションや配列、数字の集合、ファイル一覧など、データのかたまりを操作するためのライブラリです。コレクションを StreamAPI で操作するのは非常に簡単で、前項のサンプル list を例にとると

Stream というものが生成され、以降の処理は Stream のメソッドを呼び出すことで実行します。様々なメソッドが Stream には用意されており、データ処理が簡単に行えます。

前項の forEach はラムダ式との組み合わせで動作しましたが、Stream のメソッドもラムダ式と組み合わせて使用するものが多数あります。

中間操作と終端操作

Stream には 中間操作終端操作 という2種類のメソッドがあります。中間操作は Stream の抽出や変換を行い、新たな Stream を生成します。終端操作は Stream に対する最終的な処理(合計算出など)を行います。

ちょっと判りにくいので図で描いてみます。たとえば、ここに性別と年齢のデータがリスト形式であるとします。

ここから男性の平均年齢を計算する場合

  • ① リストから Stream を生成し
  • ② そこから男性だけを抽出して Stream を生成し
  • ③ そこから年齢を取りだして Stream を生成し
  • ④ その平均をもとめる

という流れになり、②と③が中間操作で、④が終端操作に相当します。

Stream を生成し、その Stream を元に新たな Stream を生成するという作業を中間操作でくり返し、終端操作で最終的なゴール(男性の平均年齢)に到達しています。コーディングは次のような形になります。

中間操作、終端操作はすべて Stream のメソッドとして提供されています。次項でいくつか見ていきます。

抽出

条件を指定して抽出を行うのが filter メソッドです。

と書けば抽出後の Stream が filter から返却されます。filter の引数が “抽出ロジック” となっていますが、それはどういうことかというと、filter に渡す引数は Predicate という関数型インターフェースと決まっており、そこに抽出ロジックを記述します。Predicate には test というメソッドがあり

この test メソッドに抽出の条件判定ロジック(true/falseを返す)を記述します。

下記サンプルは、数値を 300 以上という条件で抽出しています。Predicate を 無名クラス で実装したものと ラムダ式 を使ったものと2パターンです。

無名クラスで

ラムダ式で

filter に渡したラムダ式は test メソッド本体として扱われます。一行で書ける場合は return が省略可能 です。

上記サンプルに forEach がでてきます。同名メソッドがコレクションにあるのを上のほうで見ましたが、ここで出てきたのは Stream のメソッドです。処理内容は同じで、データ1件ごとにカッコ内の処理を実行してくれます。
(参考)実行時の filter の動き

filter は、Stream の各データを引数として Predicate の test を呼び出し、true となるデータで Stream を新たに生成し、戻り値とします。

集計

Stream の集計を行うときは reduce というメソッドを使います。しかし、この reduce はちょっと判りにくいので別の方法で実行します。IntStream というものを使うのですが、IntStream とはそもそもなんなのか、それをさらっと説明します。

これまで Stream とひとくちに言ってきましたが、実は Stream には下記の4種類あり、その中のひとつが IntStream です。

  • Stream<T>
  • 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 というメソッドがあり

この applyAsInt メソッドに 元データ → int への変換プログラムを記述します。

下記は合計を求めるサンプルですが ToIntFunction を 無名クラス で実装したものと ラムダ式 を使ったものと2パターンです。

無名クラスで

ラムダ式で

(参考)実行時の mapToInt の動き

mapToInt は Stream<T> の各データを引数として ToIntFunction の applyAsInt を呼び出し、変換された int の値で IntStream を生成し戻り値とします。

filter といっしょに
filter と組み合わせれば、条件を指定して集計できます。300 以上の値を合計してみます。

Stream をリストへ戻す

ここまでは、リストを Stream に変換して操作をする方法を見てきましたが、今度は逆に、Stream をリストに変換する 方法を見てみます。

collect(Collectors.toList()) で filter 後の Stream がリストに変換されています。

スポンサーリンク
その他の記事

コメントはお気軽に