🔖

入門Javaのラムダ式とStream API

に公開

環境

  • JDK 17

はじめに

例えば、こんなMemberレコードがあって、

public record Member(String name, int age) {
}

こんなList<Member>があるとします。

List<Member> memberList = List.of(
  new Member("佐々木久美", 27),
  new Member("金村美玖", 20),
  new Member("髙橋未来虹", 19),
  new Member("正源寺陽子", 16)
);

このmemberListから「18歳以上のメンバーの名前だけのリスト」を作りたい場合、どうしましょうか?

ベタに書くならこんな感じになります。

List<String> resultList = new ArrayList<>();
for (Member member : memberList) {
  if (member.age() >= 18) {
    String name = member.name();
    resultList.add(name);
  }
}

しかし、このコードには次のような問題があります。

  • 何がやりたいのかを理解するには、コードを詳細に読む必要がある
  • forやifの多用はバグを生みやすい

そこで、Stream APIの登場です。Stream APIは、Listなどのコレクションに対して抽出や変換を行うAPIです。Stream APIを使うことで、

  • 何がやりたいのかすぐに理解できる
  • forやifが不要なのでバグを生みにくい

というメリットが得られます。

具体的にはこんな感じです(後で詳細に解説します)。

// ListをStreamに変換する
List<String> resultList = memberList.stream()
  // 18歳以上のメンバーのみ抽出する
  .filter(member -> member.age() >= 18)
  // MemberからString(メンバーの名前)に変換する
  .map(member -> member.name())
  // Listに変換する
  .toList();

.stream().filter(...).map(...).toList()の部分がStream APIです。

そして、filter()map()の引数member -> member.age() >= 18member -> member.name()の部分がラムダ式です。

なので、Stream APIを理解するには、まずはラムダ式を理解する必要があります。

ラムダ式

匿名クラスとは

ラムダ式を理解するには、まずは匿名クラスを理解する必要があります。ちょいちょい使う割にはJava入門書などではあまり説明されていないので、ここで説明します。

次のようなHogeインタフェースがあるとします。

インタフェース
interface Hoge {
  int doSomething(String str);
}

このインタフェースを使いたい場合、通常だと実装クラスを作りますね。

実装クラス
class HogeImpl implements Hoge {
    @Override
    public int doSomething(String str) {
        return str.length();
    }
}
通常の書き方
Hoge hoge = new HogeImpl();
int length = hoge.doSomething("あいうえお");
System.out.println(length);  // "5"と出力される

しかし、このHogeImplクラスは1箇所でしか使わない場合は、クラスを作るのが大げさな感じがします。

そこで、前出のような通常のクラス定義を行わずにHogeインタフェースを使う方法があります。それが匿名クラスです。見た目は違いますが、前出のコードと全く同じことをしています。

匿名クラスを使った書き方
// これが匿名クラス
Hoge hoge = new Hoge() {
  @Override
  public int doSomething(String str) {
    return str.length();
  }
};
int length = hoge.doSomething("あいうえお");
System.out.println(length);  // "5"と出力される

クラス定義をしなくてもいいので楽ですね!

しかし一方で、何となくゴチャゴチャしている感じがします。何故かと言うと、推測できるはずの情報も書いているからです。

  • 右辺のnew Hoge()は、左辺にHogeと書いてあるから推測可能なはず
  • Hogeインタフェースには抽象メソッドが1つしか無いので、メソッド名doSomething・戻り値の型int・引数の型Stringは推測可能なはず

ラムダ式

そこで、ラムダ式の登場です。ラムダ式は、匿名クラスの推測可能な部分を省略した書き方です。

文法的に正確に言うとラムダ式は匿名クラスの省略記法ではないのですが、使う上では省略記法と思っても大丈夫です。

(再掲)匿名クラスを使った書き方
// これが匿名クラス
Hoge hoge = new Hoge() {
  @Override
  public int doSomething(String str) {
    return str.length();
  }
};
int length = hoge.doSomething("あいうえお");
System.out.println(length);  // "5"と出力される
ラムダ式を使った書き方
// これがラムダ式。やっていることは匿名クラスの場合と全く同じです。
Hoge hoge = (str) -> {
  return str.length();
};
int length = hoge.doSomething("あいうえお");
System.out.println(length);  // "5"と出力される

引数名は任意です。つまり、今回だとstrでなくても構いません。好きな名前を付けてOKです。

ただし、全ての匿名クラスをラムダ式で書き換えられる訳ではありません。ラムダ式で書き換えられるのは、「関数型インタフェースの匿名クラスのみ」です。

関数型インタフェースとは、平たく言うと「抽象メソッドを1つだけ持つインタフェース」です(抽象メソッドの他に、defaultメソッドやstaticメソッドがあっても構いません)。

関数型インタフェースには@FunctionalInterfaceアノテーションを付加することができます。

@FunctionalInterface
interface Hoge {
  int doSomething(String str);
}

@FunctionalInterfaceアノテーションを付加しているのに抽象メソッドが2つ以上あった場合などは、コンパイルエラーになります。

ラムダ式の省略

ラムダ式には、更なる省略記法があります。

  • 引数が1つだけの場合、()は省略できる
    • 引数が2個以上や0個の場合は省略不可
  • {}内の処理が1行だけの場合、{}は省略できる
    • 処理が2行以上の場合は省略不可
  • 処理がreturn ○;だけの場合、return;は省略できる

ということで、省略前と省略後のラムダ式を並べるとこんな感じです。

省略前のラムダ式
Hoge hoge = (str) -> {
  return str.length();
};
省略後のラムダ式
Hoge hoge = str -> str.length();

一番よく使う書き方は、このような省略後の書き方です。少しずつ練習していきましょう。

メソッド参照

実は、ラムダ式には更なる省略記法のようなものがあります。それがメソッド参照です。

ラムダ式
Hoge hoge = str -> str.length();
メソッド参照
Hoge hoge = String::length;

ただし、個人的には分かりづらいなと思うので、あまりメソッド参照は使いません。

Stream API

お待たせしました。ようやっとStream APIの登場です。Stream APIは、Listなどのコレクションに対して抽出や変換を行うAPIです。

Streamの生成

Stream APIの主役はjava.util.stream.Streamインタフェースが主役です。このインタフェースに、値を抽出・変換したり、最終結果をListなどに変換するメソッドが定義されています。

なので、まずはListからStreamを作る必要があります。Liststream()Streamを生成できます。

ListからStreamを生成
List<Member> memberList = List.of(
  new Member("佐々木久美", 27),
  new Member("金村美玖", 20),
  new Member("髙橋未来虹", 19),
  new Member("正源寺陽子", 16)
);

// Streamを生成
Stream<Member> stream = memberList.stream();

配列からStreamを作る場合は、java.util.Arraysクラスのstream()メソッドを利用します。

配列からStreamを生成
Member[] members = {
  new Member("佐々木久美", 27),
  new Member("金村美玖", 20),
  new Member("髙橋未来虹", 19),
  new Member("正源寺陽子", 16)
};

// Streamを生成
Stream<Member> stream = Arrays.stream(members);

中間操作

中間操作は、生成したStreamに対する要素の抽出や変換を行うメソッドです。中間操作は、0回以上行うことができます。

特によく使う中間操作は、filter()map()です。

要素の抽出

要素の抽出を行えるのはfilter()メソッドです。

// 18歳以上のメンバーのみを抽出したStreamを作成
Stream<Member> filterredStream = stream.filter(member -> member.age() >= 18);

filter()の引数はラムダ式になっているので、これは関数型インタフェースのはずです。

filter()のメソッド定義を見ると、引数の型はjava.util.function.Predicateになっています。これは、抽象メソッドがboolean test(T t)のみの関数型インタフェースです。

今回の場合はTMemberになっていると考えてください。つまり、このメソッドはMemberを引数にとってbooleanを返します。今回ではmember.age() >= 18(つまり年齢が18歳以上ならtrue)を返しています。

要素の型変換

要素の型変換を行えるのはmap()メソッドです。

// メンバー(Member)のStreamから、名前(String)のStreamに変換
Stream<String> mappedStream = filterredStream.map(member -> member.name());

map()の引数はラムダ式になっているので、これは関数型インタフェースのはずです。

map()のメソッド定義を見ると、引数の型はjava.util.function.Functionになっています。これは、抽象メソッドがR apply(T t)のみの関数型インタフェースです。

今回の場合はTMemberRStringになっていると考えてください。つまり、このメソッドはMemberを引数にとってStringを返します。今回ではmember.name()(つまりメンバーの名前)を返しています。

その他の中間操作

中間操作は他にもたくさんあります。詳細はStreamのJavadocをご確認ください。

終端操作

終端操作は、Streamを最終目的となる型(Listなど)に変換するメソッドです。終端操作は、1回のみ行うことができます。

Listへの変換

StreamからListへの型変換を行えるのはtoList()メソッドです。

Stream<String> mappedStream = ...;
// Listに変換する終端操作
List<String> resultList = mappedStream.toList();

ちなみに、このresultListはイミュータブル(不変)です。例えば、この後にresultList.add("平尾帆夏")とすると例外がスローされます。

toList()メソッドはJava 16で導入されました。それより前のバージョンのJavaを使っている場合はmappedStream.collect(Collectors.toList())と書く必要があります。ただしこの場合、戻り値のListがミュータブル(可変)になります。

その他の終端操作

終端操作は他にもたくさんあります。詳細はStreamのJavadocをご確認ください。

メソッドチェーンでの書き方

ここまでは、Streamの生成・要素の抽出・要素の変換・Listへの変換をすべて1行ずつ書いていました。

1行ずつ書く場合
// 元のList
List<Member> memberList = ...;
// Streamを生成
Stream<Member> stream = memberList.stream();
// 要素を抽出
Stream<Member> filterredStream = stream.filter(member -> member.age() >= 18);
// 要素を変換
Stream<String> mappedStream = filterredStream.map(member -> member.name());
// List<String>に変換
List<String> resultList = mappedStream.toList();

しかし、実際によく使うのはメソッドチェーンでの書き方です。すなわち、メソッドの戻り値をいちいち変数に代入せずに、.でつなぎます。

メソッドチェーンで書く場合
// 元のList
List<Member> memberList = ...;
// メソッドチェーンで記述
List<String> resultList = memberList.stream()  // Streamを生成
  .filter(member -> member.age() >= 18)  // 要素を抽出
  .map(member -> member.name())  // 要素を変換
  .toList();  // List<String>に変換

最初に学習する段階では、List<Member>Stream<Member>Stream<String>List<String>の型変換が分かりやすいように、1行ずつ変数に代入することをおすすめします。理解が進んできたら、メソッドチェーンで書きましょう。もちろん、実務ではメソッドチェーンの利用をおすすめします。

更に勉強したい人へ

<iframe src="https://www.slideshare.net/slideshow/embed_code/key/nnHz05aMkle7hh?startSlide=1" width="597" height="486" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px;max-width: 100%;" allowfullscreen></iframe><div style="margin-bottom:5px"><strong><a href="https://www.slideshare.net/bitter_fox/java8-launch" title="徹底解説!Project Lambdaのすべて リターンズ[祝Java8Launch #jjug]" target="_blank">徹底解説!Project Lambdaのすべて リターンズ[祝Java8Launch #jjug]</a></strong> from <strong><a href="https://www.slideshare.net/bitter_fox" target="_blank">bitter_fox</a></strong></div>

<iframe src="https://www.slideshare.net/slideshow/embed_code/key/9P5mqlYiLUdIgy?startSlide=1" width="597" height="486" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px;max-width: 100%;" allowfullscreen></iframe><div style="margin-bottom:5px"><strong><a href="https://www.slideshare.net/skrb/stream-api-125945709" title="今こそStream API入門" target="_blank">今こそStream API入門</a></strong> from <strong><a href="https://www.slideshare.net/skrb" target="_blank">Yuichi Sakuraba</a></strong></div>

Discussion