環境
- 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() >= 18
・member -> 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
を作る必要があります。List
のstream()
で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()
メソッドを利用します。
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)
のみの関数型インタフェースです。
今回の場合はT
がMember
になっていると考えてください。つまり、このメソッドは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)
のみの関数型インタフェースです。
今回の場合はT
がMember
・R
がString
になっていると考えてください。つまり、このメソッドは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行ずつ書いていました。
// 元の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行ずつ変数に代入することをおすすめします。理解が進んできたら、メソッドチェーンで書きましょう。もちろん、実務ではメソッドチェーンの利用をおすすめします。