Java Stream API まとめ
皆さん、こんにちは。LP開発グループのn-ozawanです。
十数年ぶりにPCを自作しました。昔と今とでは様相が異なり浦島太郎状態でした。側面と前面をガラスで中身がスケスケなピラーレスPCケースが最近のトレンドのようです。
本題です。
多くの言語では配列などの情報を反復処理するためのメソッドが事前に用意されていることが多いです。Javaも例外ではなく、Java 8 からStream APIが追加され、ラムダ式を使った反復処理ができるようになりました。今回はJavaのStream APIで何ができるのかをまとめたいと思います。
目次
Stream API
はじめに
JavaのStream APIは、コレクションや配列などのデータを宣言的に処理するための機能で、従来であればfor文で記述したコードを、より直感的で可読性高くコーディングすることができます。
例えば「藤」が付く名前を抽出したい場合、従来のfor文では以下のように記述します。
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
List<String> filteredNames = new ArrayList<>();
for (String name : names) {
if (name.contains("藤")) {
filteredNames.add(name);
}
}
IO.println(filteredNames); // [佐藤, 伊藤]Stream APIを使うと以下のようになります。従来のコードでは、何をしたい処理なのかを理解するのに、コードを読み込む必要がありました。しかし、Stream APIではfilterやcollectなどの言葉から、何をしたい処理なのかがはっきりと分かるようになります。
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
List<String> filteredNames = names.stream()
.filter(name -> name.contains("藤"))
.collect(Collectors.toList());
IO.println(filteredNames); // [佐藤, 伊藤]Stream API は大きく3つのフェーズに分かれます。
- Streamの開始
- 中間操作
- 終端操作
Streamの開始
Streamを開始するには、Streamインスタンスを作成する必要があります。ListやSetなどであればstream()メソッドを呼び出すことでStreamを開始することができます。MapはCollectionではありませんので、直接stream()メソッドを呼び出すことはできませんが、entrySet()メソッドでSetを取得することでStreamを開始することができます。配列の場合はArrays.stream()を使用します。
// Listの場合
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
Stream<String> nameStream = names.stream();
// Setの場合
Set<String> nameSet = Set.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
Stream<String> nameStreamFromSet = nameSet.stream();
// Mapの場合
Map<String, Integer> nameAgeMap = Map.ofEntries(
Map.entry("佐藤", 30),
Map.entry("鈴木", 25),
Map.entry("高橋", 28),
Map.entry("田中", 22),
Map.entry("伊藤", 35)
);
Stream<Map.Entry<String, Integer>> nameAgeStream = nameAgeMap.entrySet().stream();
// 配列の場合
String[] nameArray = {"佐藤", "鈴木", "高橋", "田中", "伊藤"};
Stream<String> nameStreamFromArray = Arrays.stream(nameArray);
中間操作
中間操作はStreamに対して加工などを行い、新しいStreamを生成します。いくつか用意されていますが、ここでは主な中間操作を紹介します。
filter()
特定の条件に合致する要素だけを抽出します。
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
List<String> filteredNames = names.stream()
.filter(name -> name.contains("藤"))
.collect(Collectors.toList());
IO.println(filteredNames); // [佐藤, 伊藤]map() / flagMap()
値もしくは型を変更します。
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
List<String> uppercasedNames = names.stream()
.map(name -> name + "さん")
.collect(Collectors.toList());
IO.println(uppercasedNames); // [佐藤さん, 鈴木さん, 高橋さん, 田中さん, 伊藤さん]distinct()
重複を削除します。
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤", "佐藤", "伊藤");
List<String> distinctNames = names.stream()
.distinct()
.collect(Collectors.toList());
IO.println(distinctNames); // [佐藤, 鈴木, 高橋, 田中, 伊藤]sorted()
ソートします。
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
List<String> sortedNames = names.stream()
.sorted()
.collect(Collectors.toList());
IO.println(sortedNames); // [伊藤, 佐藤, 鈴木, 高橋, 田中]終端操作
終端操作はStreamの最後に行われる操作です。
forEach()
Streamの先頭から末端までを順次処理します。
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
names.stream().forEach(name -> IO.println(name));
// 佐藤
// 鈴木
// 高橋
// 田中
// 伊藤collect()
Streamの内容をCollection型(ListやSet)に変換します。
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
List<String> filteredNames = names.stream()
.filter(name -> name.contains("藤"))
.collect(Collectors.toList());
IO.println(filteredNames); // [佐藤, 伊藤]他にもグルーピングもできます。以下は名前の文字数ごとにグルーピングしています。
List<String> names02 = List.of("佐藤", "鈴木一", "高橋", "田中恵", "伊藤", "山", "小林", "山田太郎");
Map<Integer, List<String>> groupedByLength = names02.stream()
.collect(Collectors.groupingBy(String::length));
IO.println(groupedByLength); // {1=[山], 2=[佐藤, 高橋, 伊藤, 小林], 3=[鈴木一, 田中恵], 4=[山田太郎]}count()
要素数を返却します。
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
long count = names.stream()
.filter(name -> name.contains("藤"))
.count();
IO.println(count); // 2reduce()
1つの情報に集約します。
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum);
IO.println(sum); // 15もちろん、リストに格納された数値を合算するだけであれば、以下でもできます。
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum2 = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
IO.println(sum2); // 15anyMatch() / allMatch() / noneMatch()
allMatch()は全ての要素が特定の条件に合致すればtrue、合致しなければfalseを返却します。anyMatch()はどれが1つでも特定の条件に合致すればtrue、合致しなければfalseを返却します。noneMatch()は!anyMatch()と同じです。
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
// anyMatch
boolean hasSuzuki = names.stream()
.anyMatch(name -> name.equals("鈴木"));
IO.println(hasSuzuki); // true
// allMatch
boolean allHaveTo = names.stream()
.allMatch(name -> name.contains("田"));
IO.println(allHaveTo); // false
// noneMatch
boolean noneHaveYamamoto = names.stream()
.noneMatch(name -> name.equals("山本"));
IO.println(noneHaveYamamoto); // true並列処理
Stream APIでは並列処理を行うことができます。並列処理したい場合は、parallelStream()でStreamを開始します。
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
names.parallelStream()
.filter(name -> {
IO.println("フィルタ中: " + name + " - " + Thread.currentThread().getName());
return name.contains("藤");
})
.map(name -> {
IO.println("変換中: " + name + " - " + Thread.currentThread().getName());
return "こんにちは、" + name + "さん";
})
.forEach(name -> IO.println(name));結果は以下になります。各要素が異なるスレッドで並列処理されていることが分かります。
フィルタ中: 伊藤 - ForkJoinPool.commonPool-worker-3
フィルタ中: 佐藤 - ForkJoinPool.commonPool-worker-2
フィルタ中: 田中 - ForkJoinPool.commonPool-worker-4
変換中: 伊藤 - ForkJoinPool.commonPool-worker-3
変換中: 佐藤 - ForkJoinPool.commonPool-worker-2
フィルタ中: 高橋 - main
フィルタ中: 鈴木 - ForkJoinPool.commonPool-worker-1
こんにちは、佐藤さん
こんにちは、伊藤さんおわりに
「JavaScriptでの反復処理まとめ」でも書きましたが、Stream APIも内部ではfor文によるループ処理が行われています。よって無駄なメソッドチェーンは非効率な処理になりますので、使用する際はその辺りをよく考えて実装すると良いでしょう。
ではたま。
