LoginSignup
2
1

More than 1 year has passed since last update.

Javaのラムダ式とStreamAPIとは

Last updated at Posted at 2021-12-02
1 / 30

前提

以降の説明で表示するソースは全て GitHub に上げてあります。


本記事の目的

Java8 で追加された機能(関数型プログラミング・ラムダ式・Stream)というものについて、
難しい説明を極力省き、下記のような記述を読みこなせるようになるのが目標です。

persons.stream()
       .filter(p -> p.getAge() >= 30)
       .forEach(p -> System.out.println(p.getName()));

ラムダ式とは

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


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

Sample01.java
interface Iface1 {
  void method(int i, String s);
}

通常クラスの場合

Face1.java
class Face1 extends Iface1 {
  public void method(int i, String s) {
    System.out.println(i + s);
  }
}
Iface1 if1 = new Face1();

無名クラスの場合

無名クラスとは、一時的に利用するために作られる名前の無いクラスです。
インスタンスの生成と同時にクラス定義も指定します。

Sample01.java
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);

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

image.png


処理が複数行ある場合

処理が複数行に渡る場合は {} で括ります。

Sample01.java
Iface1 if1 = (i, s) -> {
  System.out.println(i);
  System.out.println(s);
};
if1.method(123, "xyz"); 
// => 123
// => xyz

ラムダ式の使いどころ

List を例として、ラムダ式を使うケースを見てみます。
List のデータを処理する場合、ほぼ必ずループが出てきます。

Sample02.java
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 を実装してみます。

Sample02.java
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つしかないインターフェース(関数型インターフェース)は ラムダ式 で書けます。
そうするとコードは下記のようになり、かなりスッキリします。

Sample02.java
List<Integer> list = Arrays.asList(100, 200, 300, 400, 500);
list.forEach(i -> System.out.println(i));
// => 100 200 300 400 500 (改行あり)

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

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


StreamAPI

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

list.stream();

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

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


中間操作と終端操作

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


文字だけでは分かりにくいので図で描いてみます。
たとえば、ここに性別と年齢のデータがリスト形式であるとします。
image.png
ここから男性の合計年齢を計算する場合

 ①リストから Stream を生成
 ②そこから男性だけを抽出して Stream を生成
 ③そこから年齢だけを取り出して Stream を生成
 ④その合計を求める

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


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

// コレクション.stream().中間操作().中間操作().終端操作()
// (中間操作の戻り値は Stream なので処理を次々つないでいける。)
list.stream().filter((x) -> x.getGender() == Gender.MALE).mapToInt((x) -> x.getAge()).sum();

中間操作、終端操作はすべて Stream のメソッドとして提供されています。


抽出

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

コレクション.stream().filter (抽出ロジック)

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

public interface Predicate<T> {
  boolean test(T t);

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


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

無名クラス

Sample03.java
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 (改行される)

ラムダ式

Sample03.java
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 の動き

filterStream の各データを引数として Predicatetest を呼び出し、true となるデータで Stream を新たに生成し、戻り値とします。
image.png


集計

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 を生成し
 ③ IntStreamsum メソッドで合計を求める

となります。
image.png


mapToInt

mapToInt には ToIntFunction という関数型インターフェースを渡します。
ToIntFunction には applyAsInt というメソッドがあり

public interface ToIntFunction<T> {
  int applyAsInt(T value);
}

この applyAsInt メソッドに 元データ → int への変換プログラムを記述します。
下記は合計を求めるサンプルですが ToIntFunction無名クラス で実装したものと ラムダ式 を使ったものと2パターンです。

無名クラス

Sample04.java
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

ラムダ式

Sample04.java
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 の動き

mapToIntStream<T> の各データを引数として ToIntFunctionapplyAsInt を呼び出し、変換された int の値で IntStream を生成し戻り値とします。
image.png

filter と一緒に

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

Sample05.java
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 をリストに変換する方法を見てみます。

Sample05.java
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

コレクションや配列、数字の集合、ファイル一覧など、データのかたまりを操作するためのライブラリ。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1