JavaJava関連

【Java】Stream APIとラムダ式|コレクション操作

Java StreamAPIとラムダ式解説ページのアイキャッチ画像 Java

この記事では、Java 8で導入されたStream APIの基本、適切な使用場面、ラムダ式を活用した様々なStream操作方法、そして並列処理の方法まで、幅広くカバーしています。特に、filter()、map()、reduce()、collect()などの主要なStream操作について解説しています。

Stream APIの基本

Stream APIは、Java 8以降で提供される、コレクションなどのデータ操作を簡潔かつ効率的に行うための仕組みです。これは、リストや配列といったデータの集まりに対して一連の処理(フィルタリング、変換、集計など)を行いやすくするAPIです。従来のループ処理に比べて、コードがシンプルで可読性が高くなるのが大きな特徴です。

Stream APIの基本的な特徴

  1. 遅延評価:必要になるまで実際の処理を行わないため、効率的な処理が可能。
  2. パイプライン処理:複数の処理を連結して一連の処理として実行できる。例えば、フィルタリングや変換を一度に連結できます。
  3. 並列処理:大量データの処理を並列に行うことで、パフォーマンスを向上させることができる。

使用場面

Stream APIの利用はさまざまな場面で便利です。具体的な使用例を以下に挙げます。

  • フィルタリング:特定の条件に合ったデータのみを抽出する。
  • 変換:データを別の形式に変換(例:文字列リストから大文字に変換)。
  • 集計:合計や平均など、数値データの統計的処理を行う。
  • 重複の削除:データ内の重複を取り除く。
  • ソート:データを指定の順序で並べ替える。
  • マッチング:すべて、いずれか、または一部の要素が条件に合うか確認する。

ラムダ式を使用したStream操作

filter()

filter()メソッドは、特定の条件に合う要素だけを抽出するための便利なメソッドです。このメソッドは、Stream APIの中でよく使われるもので、コレクション内の要素をフィルタリングする(選別する)ために利用されます。条件を満たすものだけを返し、それ以外のものは除外するため、結果が非常に分かりやすくなります。

次のコードはリスト内の数値のうち、偶数だけを抽出する方法を示しています。

Java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
numbers.stream()
       .filter(n -> n % 2 == 0)  // 条件:偶数のみ
       .forEach(System.out::println);

実行結果:

2
4
6
8
10
  1. .stream()
    ListArrayなどのコレクションに対してStreamを作成します。これで、リスト内のすべての要素に対して処理が行えるようになります。
  2. .filter(n -> n % 2 == 0)
    ここがフィルタリング条件です。この例では「偶数だけを残す」という条件にしています。n -> n % 2 == 0ラムダ式と呼ばれ、nがリストの要素を1つずつ受け取り、n % 2 == 0の条件に合致する場合に、その要素を次の処理に渡します。
  3. .forEach(System.out::println)
    最後にforEachを使って、条件に合致した要素を1つずつ画面に出力しています。

map()

map()メソッドは、各要素を特定の方法で変換するためのメソッドです。リストや配列の各要素に対して変換処理を行い、新しい値に置き換えたStreamを返すため、データの加工や変換が非常に便利です。

例えば、「名前を大文字に変換し、各名前の先頭に”Hello “を追加する」という処理を行う場合、map()を使うと簡潔に記述できます。

Java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
     .map(name -> "Hello " + name.toUpperCase())  // ラムダ式で複数の操作を実行
     .forEach(System.out::println);

実行結果:

Hello ALICE
Hello BOB
Hello CHARLIE
  1. .stream()
    namesリストに対してStreamを作成します。これでリスト内のすべての要素に対して処理を行えるようになります。
  2. .map(name -> "Hello " + name.toUpperCase())
    ここが変換処理の部分です。map()メソッドを使うと、各要素に対して変換処理を適用し、変換後の新しいStreamを返します。この例では、name -> "Hello " + name.toUpperCase()というラムダ式を使っています。
    • ラムダ式name -> "Hello " + name.toUpperCase()では、各要素nameが順番に渡されます。
    • それぞれの名前に対してtoUpperCase()メソッドで大文字に変換し、さらに"Hello "を先頭に追加する操作を行います。
    • 変換された要素は、次の処理に渡されるStreamに格納されます。
  3. .forEach(System.out::println)
    最後にforEachメソッドを使って、Stream内の各要素を1つずつ出力します。ここではSystem.out::printlnを使って変換後の名前を表示しています。

reduce()

reduce()メソッドは、コレクションの要素を集約して1つの結果を得るために使われます。リストや配列の複数の要素を1つの値にまとめる際に便利で、合計や積、文字列の結合など、1つの結果を生成する処理を行う際に特に役立ちます。

以下のコードは、整数リスト内のすべての要素を足し合わせ、合計を出力する例です。

Java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                 .reduce(0, (a, b) -> a + b);
System.out.println("Sum: " + sum);

実行結果:

Sum: 15

このコードでは、リスト内の数字をすべて足し合わせ、最終的な合計を計算しています。それぞれの部分を解説していきます。

  1. .stream()
    numbersリストに対してStreamを作成します。これにより、リスト内の各要素に対して集約処理を実行できるようになります。
  2. .reduce(0, (a, b) -> a + b)
    ここが集約処理の部分です。reduce()メソッドは、2つの引数を取ります。
    • 初期値 (0)
      reduce()メソッドの最初の引数は、計算を始める際の初期値です。この例では、合計の計算を行うため、初期値に0を指定しています。
    • ラムダ式 (a, b) -> a + b
      2番目の引数は、2つの引数(ab)を受け取り、1つの結果を返す関数(ラムダ式)です。
      • ここで、aは「これまでの合計」または「前の計算結果」を表し、bは「現在の要素」を表しています。
      • ラムダ式のa + bは、「これまでの合計」に「現在の要素」を加える計算です。
      • reduce()メソッドは、最初の要素から順番に、この計算を繰り返して最終的な合計を求めます。
  3. 計算結果の出力
    最終的な合計(sum)が計算され、System.out.println("Sum: " + sum);で出力されています。

collect()

collect()メソッドは、ストリームの処理結果を新しいコレクション(リストやセットなど)に集約するためのメソッドです。これにより、ストリームを使って変換したデータを最終的にコレクションとして保持することができます。

以下のコードは、名前のリストから4文字以上の名前をフィルタリングし、その結果を新しいリストに収集する例です。

Java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> longNames = names.stream()
                              .filter(name -> name.length() > 4)  // 4文字以上の名前をフィルタリング
                              .collect(Collectors.toList());      // 結果をリストに収集
System.out.println(longNames);

実行結果:

[Alice, Charlie, David]

このコードは、リスト内の名前をフィルタリングして、新しいリストを作成しています。それぞれの部分を解説していきます。

  1. .stream()
    namesリストに対してStreamを作成します。これにより、リスト内の各要素に対して操作を実行できるようになります。
  2. .filter(name -> name.length() > 4)
    この部分は、フィルタリング処理を行います。filter()メソッドを使って、条件に合った要素のみを選択します。
    • ラムダ式name -> name.length() > 4は、各要素(name)の長さが4より大きいかどうかをチェックしています。
    • この条件を満たす名前だけが次の処理に渡されます。
  3. .collect(Collectors.toList())
    collect()メソッドは、ストリームの結果を新しいコレクションに収集するための部分です。
    • Collectors.toList()は、結果をListとして収集するためのメソッドです。この部分でフィルタリングされた名前が新しいリスト(longNames)に格納されます。
  4. 結果の出力
    System.out.println(longNames);で、新しいリストに収集された名前を出力します。

並列ストリーム処理

並列ストリームを使用すると、マルチコアプロセッサの能力を活用して処理を高速化することができます。これにより、データの処理を複数のスレッドで並行して実行できるため、大量のデータを扱う際に特に有効です。

以下のコード例では、リスト内の整数の合計を計算するだけでなく、合計が10より大きい場合は特定のメッセージを表示するようにします。

Java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.parallelStream()
                 .reduce(0, (a, b) -> {
                     int result = a + b;  // 合計を計算
                     if (result > 10) {
                         System.out.println("Current sum exceeds 10: " + result);
                     }
                     return result;  // 合計を返す
                 });

System.out.println("Parallel Sum: " + sum);

実行結果:

Current sum exceeds 10: 11
Current sum exceeds 10: 12
...
Parallel Sum: 55

このコードでは、以下のように複雑な処理を加えています。

  1. .parallelStream()
    numbersリストから並列ストリームを生成します。これにより、複数のスレッドでデータを同時に処理します。
  2. .reduce(0, (a, b) -> { ... })
    合計を計算する部分です。ここでは、ラムダ式の中に条件分岐を追加しました。
    • int result = a + b;
      まず、現在の合計と新しい要素を加えた結果をresultに保存します。
    • if (result > 10) { ... }
      合計が10を超える場合に、現在の合計を表示します。このように、計算途中での条件分岐を加えることで、より複雑な処理を実現しています。
    • return result;
      最後に、新しい合計を返します。
  3. 結果の出力
    最終的な合計が計算された後、System.out.println("Parallel Sum: " + sum);で出力されます。

ラムダ式を使用した複雑なStream操作の例

このコード例では、JavaのStream APIを使って、リスト内の単語を処理し、特定の条件に基づいてグループ化しています。

Java
List<String> words = Arrays.asList("Java", "Stream", "Lambda", "API", "Functional");
Map<Integer, List<String>> groupedByLength = words.stream()
    .filter(word -> word.length() > 3)
    .map(String::toLowerCase)
    .collect(Collectors.groupingBy(String::length));

System.out.println(groupedByLength);

実行結果:

{4=[java], 6=[stream, lambda], 10=[functional]}
  • 結果は、長さごとに単語をグループ化したMap形式で表示されます。キーは単語の長さで、値はその長さに対応する単語のリストです。

コード解説


リストの作成:

Java
List<String> words = Arrays.asList("Java", "Stream", "Lambda", "API", "Functional");
  • wordsというリストを作成し、5つの単語を格納しています。このリストは、後で処理するデータソースとなります。

Streamの生成:

Java
words.stream()
  • wordsリストからストリームを生成します。これにより、リスト内の各要素に対してストリーム操作を行うことができます。

フィルタリング:

Java
.filter(word -> word.length() > 3)
  • filterメソッドを使用して、単語の長さが3より大きい場合のみを選択します。ラムダ式word -> word.length() > 3が、条件を判定するために使用されています。このステップにより、短い単語は除外されます。

変換:

Java
.map(String::toLowerCase)
  • mapメソッドを使って、残った単語をすべて小文字に変換します。String::toLowerCaseというメソッド参照が使用されています。これにより、結果として大文字と小文字の違いがなくなります。

集約:

Java
.collect(Collectors.groupingBy(String::length))
  • 最後に、collectメソッドを使って、結果を新しいコレクションに集約します。ここでは、Collectors.groupingBy(String::length)を使用して、単語の長さごとにグループ化しています。長さをキーとして、同じ長さの単語をリストにまとめます

まとめ

  • Stream APIの基本概念: Stream APIの特徴と、従来のコレクション操作との違い
  • 使用場面の把握: Stream APIが特に効果的な場面や、適切な使用方法
  • ラムダ式との連携: Stream APIとラムダ式を組み合わせた簡潔なコーディング方法
  • 主要操作の理解: filter()、map()、reduce()、collect()などの基本的なStream操作
  • 並列処理の実現: Stream APIを用いた効率的な並列処理
  • 複雑な操作の習得: 複数のStream操作を組み合わせた高度な処理

この記事では、Stream APIの基本的な概念から主要なStream操作ラムダ式との組み合わせてコードを簡素化して解説しました。

Stream APIの適切な使用は、コードの可読性向上、処理の効率化、そして大量データの扱いの簡素化につながります。これらの知識は、データ処理、バッチ処理、大規模アプリケーションの開発など、様々な場面で活用できる重要なスキルセットとなります。

未経験でもWEBスキルを取得したい方
  • Javaコース
    Javaプログラミングを基礎から応用まで、体系的に学べる実践的なカリキュラムが特徴です。「マインクラフト」を通じてJavaの基本文法とフレームワークの概念を学びます。また、実際のプロジェクト開発を通じて、データベース連携やWebアプリケーション開発のスキルを身につけることができます。

コメント

タイトルとURLをコピーしました