Java 8のストリーム – Javaストリーム
Java 8 Stream APIチュートリアルへようこそ。過去数回のJava 8の投稿では、Java 8のインターフェースの変更や関数インターフェースとラムダ式について見てきました。今日は、Java 8で導入された主要なAPIの1つであるJava Streamを見ていきます。
Java 8のストリーム
-
- Java 8のストリーム
-
- コレクションとJavaストリーム
-
- Java 8のストリームにおける関数的インターフェース
関数とバイ関数
述語とバイ述語
コンシューマーとバイコンシューマー
サプライヤー
java.util.Optional
java.util.Spliterator
Javaストリームの中間および終端操作
Javaストリームのショートサーキット操作
Javaストリームの例
Javaストリームの作成
Javaストリームをコレクションや配列に変換
Javaストリームの中間操作
Javaストリームの終端操作
Java 8ストリームAPIの制限
Javaストリーム
Java Stream APIの例を調べる前に、なぜそれが必要なのかを見てみましょう。整数のリストを繰り返し処理し、10より大きい整数の合計を求めたいとします。Java 8より前の場合、それを行うアプローチは次のようになります。
private static int sumIterator(List<Integer> list) {
Iterator<Integer> it = list.iterator();
int sum = 0;
while (it.hasNext()) {
int num = it.next();
if (num > 10) {
sum += num;
}
}
return sum;
}
上記のアプローチには3つの主要な問題があります。 (Jōki no apurōchi ni wa mitsu no shuyōna mondai ga arimasu.)
-
- 私たちは整数の合計を知りたいだけですが、イテレーションの方法も提供しなければなりません。これは外部イテレーションとも呼ばれます。クライアントプログラムがリスト上を繰り返すアルゴリズムを処理しています。
-
- このプログラムは順次的なものであり、簡単に並行して行う方法はありません。
- 単純なタスクでも多くのコードが必要です。
上記のすべての不具合を解決するために、Java 8 Stream APIが導入されました。Java Stream APIを使用することで、内部反復を実装できます。これは、Javaフレームワークが反復を制御しているため、より優れています。内部反復には、シーケンシャルおよび並列実行、指定された条件に基づくフィルタリング、マッピングなどのいくつかの機能が備わっています。 Java 8 Stream APIのほとんどのメソッド引数は、関数インターフェースですので、ラムダ式を使うことができます。Java Streamsを使用して、上記のロジックを一行の文でどのように書くことができるかを見てみましょう。
private static int sumStream(List<Integer> list) {
return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}
上記のプログラムは、Javaのフレームワークの反復戦略、フィルタリング、マッピングのメソッドを利用しており、効率を向上させることができます。まず最初に、Java 8 Stream APIの基本的な概念について見ていきます。そして、最もよく使われるメソッドを理解するために、いくつかの例を紹介します。
コレクションとJavaストリーム
コレクションは、値を保持するためのメモリ上のデータ構造であり、コレクションを使用する前に、すべての値が設定されている必要があります。一方、Javaのストリームは、必要に応じて計算されるデータ構造です。Javaのストリームはデータを格納しないため、ソースデータ構造(コレクションや配列)上で操作を行い、パイプライン化されたデータを生成して特定の操作を行うことができます。たとえば、リストからストリームを作成し、条件に基づいてフィルタリングすることができます。Javaのストリーム操作は、ラムダ式を使用した関数型プログラミングに非常に適しているため、コードを読みやすく短くすることができます。Java 8のストリームの内部イテレーションの原則により、いくつかのストリーム操作で遅延シークが実現されます。たとえば、フィルタリング、マッピング、重複の削除などは、遅延的に実装でき、高いパフォーマンスと最適化の余地があります。Javaのストリームは消費可能なので、将来の使用のためにストリームへの参照を作成する方法はありません。データが必要とされるため、同じストリームを複数回再利用することはできません。Java 8のストリームは、シーケンシャル処理と並列処理の両方をサポートしており、大規模なコレクションで高いパフォーマンスを実現するのに非常に役立ちます。JavaのストリームAPIのインターフェースとクラスはすべて、java.util.streamパッケージにあります。また、オートボクシングを使用してコレクション内でintやlongなどのプリミティブデータ型を使用することができ、これらの操作には多くの時間がかかることがあるため、プリミティブ型のための特定のクラスであるIntStream、LongStream、DoubleStreamも利用できます。
Java 8 Streamにおける機能的インタフェース
Java 8 Stream API メソッドでよく使用される関数インターフェースには、次のようなものがあります。
-
- FunctionとBiFunction: Functionは、一つのタイプの引数を受け取り、別のタイプの引数を返す関数を表します。Function<T, R>は、Tが関数の入力のタイプであり、Rが関数の結果のタイプである、ジェネリック形式です。プリミティブ型の処理には、特定のFunctionインターフェースがあります – ToIntFunction、ToLongFunction、ToDoubleFunction、ToIntBiFunction、ToLongBiFunction、ToDoubleBiFunction、LongToIntFunction、LongToDoubleFunction、IntToLongFunction、IntToDoubleFunctionなどです。Functionまたはそのプリミティブの特殊化が使用されるStreamメソッドのいくつかは以下の通りです:
Stream map(Function<? super T, ? extends R> mapper)
IntStream mapToInt(ToIntFunction<? super T> mapper) – longおよびdoubleを返すプリミティブ特化ストリームも同様です。
IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) – longおよびdoubleも同様です
A[] toArray(IntFunction<A[]> generator)
U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator combiner)
PredicateとBiPredicate: ストリームの要素に対してテストされる述語を表します。これは、Javaのストリームから要素をフィルタリングするために使用されます。Functionと同様に、int、long、doubleのためのプリミティブ専用のインターフェースがあります。PredicateまたはBiPredicateの特殊化が使用されるStreamメソッドの一部は以下の通りです:
Stream filter(Predicate<? super T> predicate)
boolean anyMatch(Predicate<? super T> predicate)
boolean allMatch(Predicate<? super T> predicate)
boolean noneMatch(Predicate<? super T> predicate)
ConsumerとBiConsumer: これは、単一の入力引数を受け入れ、結果を返さない操作を表します。これは、Javaのストリームのすべての要素に対して何らかのアクションを実行するために使用することができます。Consumer、BiConsumer、またはそのプリミティブの特殊化インターフェースが使用されるJava 8のストリームメソッドの一部は以下の通りです:
Stream peek(Consumer<? super T> action)
void forEach(Consumer<? super T> action)
void forEachOrdered(Consumer<? super T> action)
Supplier: Supplierは、ストリーム内で新しい値を生成するために使用できる操作を表します。Supplier引数を取るStreamの一部のメソッドは以下の通りです:
public static Stream generate(Supplier s)
R collect(Supplier supplier,BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner)
java.util.Optionalの日本語の同義語を提供します:
– オプション型
– ヌル値を許容する型
– 空値を扱うための型
– 存在しないかもしれない値を扱う型
Java Optionalは、nullでない値を含むかもしれないコンテナオブジェクトです。値が存在する場合、isPresent()はtrueを返し、get()はその値を返します。ストリームの終端操作はOptionalオブジェクトを返します。これらのメソッドのいくつかは以下の通りです。
- Optional reduce(BinaryOperator accumulator)
- Optional min(Comparator<? super T> comparator)
- Optional max(Comparator<? super T> comparator)
- Optional findFirst()
- Optional findAny()
java.util.Spliteratorを日本語で表現すると、「ジャバドットユーティルドットスプリッタレータ」となります。
Java 8のStream APIで並列実行をサポートするために、Spliteratorインターフェースが使用されます。SpliteratorのtrySplitメソッドは、元のSpliteratorの一部の要素を管理する新しいSpliteratorを返します。
Javaのストリームの中間操作と終端操作
新しいStreamを返すJava Stream APIの操作は、中間操作と呼ばれています。これらの操作のほとんどは遅延評価であり、新しいストリーム要素を生成し、次の操作に送信します。中間操作は最終的な結果を生成する操作ではありません。よく使用される中間操作は、filterやmapです。Java 8 Stream APIの操作は、結果を返すか副作用を生成する操作です。ストリームに対して終端メソッドが呼び出されると、ストリームが消費され、以降ストリームを使用することはできません。終端操作は積極的な性質を持っています。つまり、結果を返す前にストリーム内のすべての要素を処理します。よく使用される終端メソッドは、forEach、toArray、min、max、findFirst、anyMatch、allMatchなどです。終端メソッドは戻り値の型から特定することができますが、それらは決してStreamを返しません。
Javaのストリームのショートサーキット操作
中間操作がショートサーキューティングと呼ばれるのは、無限のストリームに対して有限のストリームを生成する可能性がある場合です。例えば、limit()やskip()は2つのショートサーキューティングな中間操作です。端末操作がショートサーキューティングと呼ばれるのは、無限ストリームに対して有限時間で終了する可能性がある場合です。例えば、anyMatch、allMatch、noneMatch、findFirst、findAnyはショートサーキューティングな端末操作です。
Javaのストリームの例
私はJava 8 Stream APIのほぼすべての重要な部分を網羅しました。この新しいAPIの機能を使うのは興奮しますし、いくつかのJavaストリームの例を見てみましょう。
Java ストリームの作成
配列やコレクションからJavaストリームを作成するためのいくつかの方法があります。これらを簡単な例で見てみましょう。
-
- 似た種類のデータからストリームを作成するためには、Stream.of()を使用することができます。例えば、intやIntegerオブジェクトのグループからJava Streamの整数ストリームを作成することができます。
-
- Stream stream = Stream.of(1,2,3,4);
配列のオブジェクトを使用してStream.of()を利用することもできます。ただし、オートボクシングはサポートされていないため、プリミティブの配列を渡すことはできません。
Stream stream = Stream.of(new Integer[]{1,2,3,4});
//うまく動作します
Stream stream1 = Stream.of(new int[]{1,2,3,4});
//コンパイルエラー、型の不一致: Stream<int[]>をStreamに変換できません
Collectionのstream()メソッドを使用して順次のストリームを作成し、parallelStream()を使用して並列ストリームを作成することができます。
List myList = new ArrayList<>();
for(int i=0; i<100; i++) myList.add(i);
//順次のストリーム
Stream sequentialStream = myList.stream();
//並列ストリーム
Stream parallelStream = myList.parallelStream();
Stream.generate()メソッドとStream.iterate()メソッドを使用してストリームを作成することができます。
Stream stream1 = Stream.generate(() -> {return “abc”;});
Stream stream2 = Stream.iterate(“abc”, (i) -> i);
Arrays.stream()メソッドとString.chars()メソッドを使用することもできます。
LongStream is = Arrays.stream(new long[]{1,2,3,4});
IntStream is2 = “abc”.chars();
Java Streamをコレクションまたは配列に変換する
Java Streamからコレクションや配列を取得するためのいくつかの方法があります。
-
- JavaのStreamクラスのcollect()メソッドを使用することで、ストリームからList、Map、またはSetを取得することができます。
-
- Stream intStream = Stream.of(1, 2, 3, 4);
-
- List intList = intStream.collect(Collectors.toList());
-
- System.out.println(intList); // [1, 2, 3, 4]が出力されます。
intStream = Stream.of(1, 2, 3, 4); // ストリームを閉じたため、再び作成する必要があります
Map<Integer, Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i + 10));
System.out.println(intMap); // {1=11, 2=12, 3=13, 4=14}が出力されます。
ストリームから配列を作成するために、streamのtoArray()メソッドを使用することもできます。
Stream intStream = Stream.of(1, 2, 3, 4);
Integer[] intArray = intStream.toArray(Integer[]::new);
System.out.println(Arrays.toString(intArray)); // [1, 2, 3, 4]が出力されます。
Java Streamの中間操作
(Java Streamの)中間操作
よく使われるJava Streamの中間操作の例を見てみましょう。
-
- ストリームのfilter()メソッドの例:filter()メソッドを使用して、ストリームの要素を条件にテストし、フィルターされたリストを生成することができます。
-
- List myList = new ArrayList<>();
-
- for(int i=0; i<100; i++) myList.add(i);
-
- Stream sequentialStream = myList.stream();
Stream highNums = sequentialStream.filter(p -> p > 90); //90より大きい数字をフィルタリング
System.out.print(“High Nums greater than 90=”);
highNums.forEach(p -> System.out.print(p+” “));
//結果:”High Nums greater than 90=91 92 93 94 95 96 97 98 99 ”
ストリームのmap()メソッドの例:map()メソッドを使用して、関数をストリームに適用することができます。文字列のリストに大文字の関数を適用する方法を見てみましょう。
Stream names = Stream.of(“aBc”, “d”, “ef”);
System.out.println(names.map(s -> {
return s.toUpperCase();
}).collect(Collectors.toList()));
//結果:[ABC, D, EF]
ストリームのsorted()メソッドの例:sorted()メソッドを使用して、ストリームの要素をComparator引数でソートすることができます。
Stream names2 = Stream.of(“aBc”, “d”, “ef”, “123456”);
List reverseSorted = names2.sorted(Comparator.reverseOrder()).collect(Collectors.toList());
System.out.println(reverseSorted); // [ef, d, aBc, 123456]
Stream names3 = Stream.of(“aBc”, “d”, “ef”, “123456”);
List naturalSorted = names3.sorted().collect(Collectors.toList());
System.out.println(naturalSorted); //[123456, aBc, d, ef]
ストリームのflatMap()メソッドの例:flatMap()メソッドを使用して、リストのストリームからストリームを作成することができます。この疑問を解消するための簡単な例を見てみましょう。
Stream<List> namesOriginalList = Stream.of(
Arrays.asList(“Pankaj”),
Arrays.asList(“David”, “Lisa”),
Arrays.asList(“Amit”));
//ListからStringのストリームにフラット化する
Stream flatStream = namesOriginalList
.flatMap(strList -> strList.stream());
flatStream.forEach(System.out::println);
Javaのストリーム終端操作
Javaのストリームの端末操作の例を見てみましょう。
-
- ストリームreduce()の例:私たちはreduce()を使用して、ストリームの要素に減少を行い、連想積算関数を使用してOptionalを返すことができます。ストリーム内の整数を乗算するためにどのように使用するかを見てみましょう。
-
- Stream numbers = Stream.of(1、2、3、4、5);
Optional intOptional = numbers.reduce((i、j)→ {return i*j;});
if(intOptional.isPresent())System.out.println(”Multiplication = “+ intOptional.get()); //120
ストリームcount()の例:この終端操作を使用して、ストリーム内のアイテムの数を数えることができます。
Stream numbers1 = Stream.of(1、2、3、4、5);
System.out.println(”Number of elements in stream =”+ numbers1.count()); //5
Stream forEach()の例:これはストリーム上で反復処理するために使用できます。イテレーターの代わりにこれを使用できます。ストリームのすべての要素を印刷するためにどのように使用するかを見てみましょう。
Stream numbers2 = Stream.of(1、2、3、4、5);
numbers2.forEach(i→ System.out.print(i + “,”)); //1,2,3,4,5,
ストリームmatch()の例:Stream APIの一致メソッドのいくつかの例を見てみましょう。
Stream numbers3 = Stream.of(1、2、3、4、5);
System.out.println(”Stream contains 4? “+ numbers3.anyMatch(i→ i == 4));
// Stream contains 4? true
Stream numbers4 = Stream.of(1、2、3、4、5);
System.out.println(”Stream contains all elements less than 10? “+ numbers4.allMatch(i→ i < 10));
// Stream contains all elements less than 10? true
Stream numbers5 = Stream.of(1、2、3、4、5);
System.out.println(”Stream doesn’t contain 10? “+ numbers5.noneMatch(i→ i == 10));
// Stream doesn’t contain 10? true
findFirst()の例:これはショートサーキット終端操作です。ストリームから最初の文字列を探すためにどのように使用するかを見てみましょう。 Dで始まるストリーム。
Stream names4 = Stream.of(”Pankaj”、”Amit”、”David”、”Lisa”);
Optional firstNameWithD = names4.filter(i→ i.startsWith(”D”)).findFirst();
if(firstNameWithD.isPresent()){
System.out.println(”First Name starting with D =”+ firstNameWithD.get()); //David
}
Java 8 ストリーム API の制約
Java 8のStream APIは、リストや配列に多くの新しい機能をもたらしますが、いくつかの制限もあります。
-
- ステートレスなラムダ式: パラレルストリームを使用している場合、ステートフルなラムダ式を使用するとランダムな応答が生じる可能性があります。簡単なプログラムで確認してみましょう。StatefulParallelStream.java
package com.scdev.java8.stream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StatefulParallelStream {
public static void main(String[] args) {
List ss = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);
List result = new ArrayList();
Stream stream = ss.parallelStream();
stream.map(s -> {
synchronized (result) {
if (result.size() < 10) { result.add(s); } } return s; }).forEach( e -> {});
System.out.println(result);
}
}
上記のプログラムを実行すると、異なる結果が得られます。これはストリームの反復方法に依存し、並列処理の順序が定義されていないためです。逐次的なストリームを使用する場合、この問題は発生しません。
ストリームが消費されると、後で使用することはできません。上記の例では、毎回ストリームを作成していることがわかります。
Stream APIには多くのメソッドがあり、最も混乱する部分はオーバーロードされたメソッドです。これにより、学習曲線が時間を取ることになります。
これでJava 8のStreamの例のチュートリアルは終わりです。この機能を使って、並列処理を通じてコードをより読みやすくし、パフォーマンスを向上させることを楽しみにしています。参考文献:Java Stream APIドキュメント。