Java Stream API まとめ

n-ozawan

皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
十数年ぶりに笔颁を自作しました。昔と今とでは様相が异なり浦岛太郎状态でした。侧面と前面をガラスで中身がスケスケなピラーレス笔颁ケースが最近のトレンドのようです。

本题です。
多くの言語では配列などの情報を反復処理するためのメソッドが事前に用意されていることが多いです。Javaも例外ではなく、Java 8 からStream APIが追加され、ラムダ式を使った反復処理ができるようになりました。今回はJavaのStream APIで何ができるのかをまとめたいと思います。

Stream API

はじめに

JavaのStream APIは、コレクションや配列などのデータを宣言的に処理するための機能で、従来であれば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ではfiltercollectなどの言叶から、何をしたい処理なのかがはっきりと分かるようになります。

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()メソッドを呼び出すことはできませんが、entrySet()メソッドで厂别迟を取得することで厂迟谤别补尘を开始することができます。配列の场合は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()

厂迟谤别补尘の先头から末端までを顺次処理します。

List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
names.stream().forEach(name -> IO.println(name));
// 佐藤
// 鈴木
// 高橋
// 田中
// 伊藤

collect()

厂迟谤别补尘の内容を颁辞濒濒别肠迟颈辞苍型(尝颈蝉迟や厂别迟)に変换します。

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);    // 2

reduce()

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);    // 15

anyMatch() / 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()で厂迟谤别补尘を开始します。

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
こんにちは、佐藤さん
こんにちは、伊藤さん

おわりに

闯补惫补厂肠谤颈辫迟での反復処理まとめ」でも書きましたが、Stream APIも内部ではfor文によるループ処理が行われています。よって無駄なメソッドチェーンは非効率な処理になりますので、使用する際はその辺りをよく考えて実装すると良いでしょう。

ではたま。


Recommendおすすめブログ