Java 11 Gold 取得に向けた学習記録
ストリームAPI
コレクションの各要素などをストリームという概念で扱うためのクラスがjava.util.streamパッケージには用意されている。
ストリーム生成元のコレクションなどをソースと表現する。基本的にはストリームを利用して各要素に操作を行ったとしても、ソースには影響が無い(干渉、副作用)。
ストリームを取得するには、いくつかの方法がある。
List<String> list = List.of("E", "B", "D", "C", "A");
Stream<String> stream1 = list.stream();
String[] array = new String[] {"E", "B", "D", "C", "A"};
Stream<String> stream2 = Arrays.stream(array);
ストリームパイプライン
ストリームに対してメソッドチェーンを利用して複数の処理を実行する形式をストリームパイプラインと言う。ストリームパイプラインは1つのソース、複数の中間操作、1つの終端操作から構成される。
メソッドチェーンは中間操作メソッドが実行元のインスタンスとは別の新たなストリームインスタンスを返すことによって実現している。
Stream<String> stream = List.of("E", "B", "D", "C", "A").stream();
Stream<String> newStream = stream.sorted();
System.out.println(stream == newStream);
// >> false
中間操作と終端操作はjava.util.stream.Streamインターフェースから提供される。
中間操作
| 戻り値の型 | メソッド名 | 概要 |
|---|---|---|
Stream<T> |
distinct() |
重複を排除する(equals()で判断) |
Stream<T> |
filter(Predicate<? super T> predicate) |
trueを返すもの要素だけに絞り込む |
Stream<T> |
limit(long maxSize) |
要素数をmaxSizeに制限 |
<R> Stream<R> |
map(Function<? super T, ? extends R> mapper) |
各要素に指定された関数を実行 |
Strema<T> |
peek(Consumer<? super T> action) |
新しいストリームを返すとともに、各要素に指定されたアクションを実行。ログ出力などやデバッグで利用。 |
Stream<T> |
skip(long n) |
ストリームから最初のn個を破棄 |
Stream<T> |
sorted(Comparator>? super T> comparator) |
comparatorを使って並び替える |
終端操作
| 戻り値の型 | メソッド名 | 概要 |
|---|---|---|
boolean |
allMatch(Predicate<? super T> predicate) |
全ての要素がpredicateの結果がtrueになるかどうか |
boolean |
anyMatch(Predicate<? super T> predicate) |
いずれかの要素がpredicateの結果がtrueになるかどうか |
<R, A> R |
collect(Collector<? super T,A,R> collector) |
collectorを使って各要素に対する可変リダクション操作を実行 コレクションを作るために使用する |
long |
count() |
ストリームの要素数を返す |
Optional<T> |
findAny() |
ストリーム内に要素が残っているかどうかの結果をラップしたOptionalを返す |
Optional<T> |
findFirst() |
ストリーム内の最初の要素をラップしたOptionalを返す |
void |
forEach(Consumer<? super T> action) |
各要素にactionを実行 |
Optional<T> |
max(Comparator<? super T> omparator) |
comparatorを使って最大の要素を返す |
Optional<T> |
min(Comparator<? super T> omparator) |
comparatorを使って最大の要素を返す |
boolean |
noneMatch(Predicate<? super T> predicate) |
predicateがtrueを返す要素がないかどうか |
Optional<T> |
reduce(BinaryOperator<T> accumulator) |
各要素にリダクション操作を実行し、累積的に結合された結果をOptionalでラップして返す(引数で初期値を指定した場合はラップされない) |
Object[] |
toArray() |
ストリームを配列にする |
<A> A[] |
toArray(IntFunction<A[]> generator) |
generatorを使用してストリームを配列にする |
IllegalStateException
Streamが継承するBaseStraemにはclose()メソッドが定義されていて、一度使用したStreamオブジェクトはこのclose()メソッドによって閉じられることになっている。
もしも1度閉じられたストリームを再利用しようとした場合、IllegalStateExceptionがthrowされる。
Stream<String> usedStream = List.of("E", "B", "D", "C", "A").stream();
// この時点で usedStream は閉じられる
Stream<String> newStream1 = usedStream.sorted();
// 閉じられたストリームをもう一度使用した場合、IllegalStateException が発生する
Stream<String> newStream2 = usedStream.map((alphabet) -> {
return "word:" + alphabet;
});
干渉
interference
ストリームパイプラインが元のソースに変更を加えることを干渉と言う。
基本的にストリームパイプラインにおいては干渉は避けるべきとされている。
// ソース
List<String> list = List.of("E", "B", "D", "C", "A");
// ストリーム
Stream<String> stream = list.stream();
stream.map((alphabet) -> {
// ソースに変更を加える処理(干渉)
list.remove(alphabet);
return alphabet;
});
順次・並列実行
sequential=順次
parallel=並列
ストリームパイプラインは順次、並列のいずれかで実行することができる。
例えば、Collectionインターフェースのstream()で生成したストリームは順次実行となり、parallelStream()で生成したストリームは並列実行になる。
基本的には明示的に並列処理用のストリームを生成しない限りは、順次実行用のストリームになる。
List<String> list = List.of("E", "B", "D", "C", "A");
// 順次実行のストリーム
Stream<String> stream = list.stream();
// 並列実行のストリーム
Stream<String> parallelStream = list.parallelStream();
ステートレス操作・ステートフル操作
中間操作はさらに、ステートレス操作とステートフル操作に分類される。
ステートレス操作は、各要素を処理するために他の要素の情報を必要としない。map()、filter()、flatMap()、peek()などが該当する。ステートレス操作は操作の結果がストリームの各要素に対して独立しているため、並列処理に適している。
ステートフル操作は、各要素を処理するために他の要素の情報を必要とする。sorted()、distinct()、limit()、skip()などが該当する。例えばsorted()では並び替えのために全体の要素数が確定した状態でしか実行できないし、distinct()は重複排除のために全体の要素を使用する必要がある。つまり、ステートフル操作は全体の要素の状態を把握する必要があり、各要素に対して独立して操作を実行することができないため、並列処理に向かない。
ステートレス操作とステートフル操作は実行のされ方も異なる。
ステートレス操作は各要素を独立して処理するため、ある要素の処理が他の要素の処理に影響を与えない。そのため一つの要素に対する処理が完了すると、次の要素への処理を待たず、処理が完了した要素を即座に次のステップに送信する。
一方、ステートフル操作は全要素を収集してから実行されるため、要素のうちの一つに対する処理が完了したとしても、即座に次のステップには進まず次の要素に対する処理を続けて実行する。
List.of("E", "B", "D", "C", "A")
.stream()
.peek((alphabet) -> {
System.out.println(alphabet);
// ステートフル操作
}).sorted((a, b) -> {
System.out.println("sorting: " + a + ", " + b);
return a.compareTo(b);
// ステートレス操作
}).peek((alphabet) -> {
System.out.println("sorted: " + alphabet);
}).map((alphabet) -> {
System.out.println("mapping: " + alphabet);
return "word:" + alphabet;
}).peek((alphabet) -> {
System.out.println("mapped: " + alphabet);
}).forEach((alphabet) -> {
System.out.println(alphabet);
});
// >> E
// >> B
// >> D
// >> C
// >> A
// ステートフル操作
// >> sorting: B, E
// >> sorting: D, B
// >> sorting: D, E
// >> sorting: D, B
// >> sorting: C, D
// >> sorting: C, B
// >> sorting: A, D
// >> sorting: A, C
// >> sorting: A, B
// ステートレス操作
// >> sorted: A
// >> mapping: A
// >> mapped: word:A
// >> word:A
// >> sorted: B
// >> mapping: B
// >> mapped: word:B
// >> word:B
// >> sorted: C
// >> mapping: C
// >> mapped: word:C
// >> word:C
// >> sorted: D
// >> mapping: D
// >> mapped: word:D
// >> word:D
// >> sorted: E
// >> mapping: E
// >> mapped: word:E
// >> word:E
遅延実行
中間操作のためのメソッドの多くは関数型インターフェースを受け取るが、渡した「処理」(多くの場合、ラムダ式。もしくはメソッド参照)が内部で実行されるのは、終端操作の実行時になる。この特徴を遅延実行と言う。
System.out.println("ストリームを生成");
Stream<String> stream = List.of("E", "B", "D", "C", "A").stream();
System.out.println("中間操作の実行");
Stream<String> stream2 = stream.sorted().map((alphabet) -> {
System.out.println("map()に渡した処理の実行タイミングはここ");
return "word:" + alphabet;
});
System.out.println("終端操作の実行");
String[] result = stream2.toArray(String[]::new);
for(String word : result){
System.out.println(word);
}
// >> ストリームを生成
// >> 中間操作の実行
// >> 終端操作の実行
// >> map()に渡した処理の実行タイミングはここ
// >> map()に渡した処理の実行タイミングはここ
// >> map()に渡した処理の実行タイミングはここ
// >> map()に渡した処理の実行タイミングはここ
// >> map()に渡した処理の実行タイミングはここ
// >> word:A
// >> word:B
// >> word:C
// >> word:D
// >> word:E
実行結果から、中間操作が終端操作の開始をきっかけに開始されていることがわかる。
中間操作は終端操作が呼び出された時点で開始される
副作用
ストリームパイプラインが処理過程でパイプライン外部の状態を変更したり、外部と相互作用することを副作用と言う。
副作用は関数型プログラミングにおける純粋な関数が持つべき「副作用を持たない」と言う特徴に反するものであり、ストリームが本来のデータ処理の目的以外に影響を及ぼすことになる。またスレッドセーフにおける安全性に悪影響を与えることもある。
またストリームAPIには遅延実行という特徴があることからも、副作用を持つ処理は避けるべきとされる。
List<String> list = List.of("E", "B", "D", "C", "A");
Stream<String> stream = list.stream();
stream.map((alphabet) -> {
return list.add("word:" + alphabet);
});
List<String> list = List.of("E", "B", "D", "C", "A");
Stream<String> stream = list.stream();
List<String> result = stream.map((alphabet) -> {
return "ward:" + alphabet;
}).collect(Collectors.toList());
副作用はリダクションを利用することでも回避できる場合がある。
リダクション
Reduction
reduce()に代表される、ストリームの各要素を集約して単一の結果にまとめるような操作をリダクション操作と言う。
リダクション操作は元のソースに対して変更を加えないため、干渉を避けることができる。
reduce()
Optional<T> reduce(BinaryOperator<T> accumulator)
List<String> list = List.of("E", "B", "D", "C", "A");
Optional<String> result = list.stream().reduce( (sum, element) -> sum + element);
初期値(identity)を指定した場合、戻り値は初期値と同じ型になる。
T reduce(T identity, BinaryOperator<T> accumulator)
List<String> list = List.of("E", "B", "D", "C", "A");
String result = list.stream().reduce("", (sum, element) -> sum + element);
上の例ではメソッド参照を利用できる
String result = list.stream().reduce("", String::concat);
可変リダクション
Mutable Reduction
collect()に代表される、ストリームの内部に存在するCollectionやStringBuilderなどの可変結果コンテナがストリームの要素を逐次的に収集しながらも、途中でそのコンテナの中身を変更(更新)することができ、最終的には目的とする表現に変換することができるリダクション操作を可変リダクション操作と言う。
ここで、可変結果コンテナとは、ストリームの要素を収集するために使用されるList、Set、StringBuilderなどの変更可能なオブジェクトのことを指す。収集の過程でこのオブジェクトには新たな要素が追加され、コンテナとしての更新が行われている。
collect()
CollectionやStringBuilderなどの可変結果コンテナ内に必要な要素を集める(collect)するところからcollect()と言う名前になっている。
collect()を理解するには、Collectorインターフェース、Collectorsクラスについても理解する必要がある。
<R, A> R collect(Collector<? super T, A, R> collector)
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)
Collectorインターフェース
(個人的には最初、Collectorは関数型インタフェースだと勘違いしていた。ストリームパイプラインの中間操作メソッドの多くは関数型インターフェースの実装クラスを引数に取るが、collect()メソッドは関数型インターフェースの実装ではなく、普通のインターフェースCollectorの実装クラスを引数に受けとる。)
java.util.stream.Collectorインターフェースは、リダクション操作を定義するために使用される。
Collectorインターフェースは、collect()の引数で利用されている。
<R,A> R collect(Collector<? super T,A,R> collector)
Collectorインターフェース自体の型引数(ジェネリクス)は次のような定義になっている。
public interface Collector<T, A, R>
T : 入力要素の型
A : 中間操作のaccumulatorの型(関数型インターフェース)
R : 集約結果の型
Collectorインターフェースで定義された抽象メソッドは、主に次の4つ。これらは関数型インターフェースの実装を返すメソッドで、関数型インターフェースの実装部分がリダクション操作に当たる。
Supplier<A> supplier()BiConsumer<A, T> accumulator()BinaryOperator<A> combiner()Function<A, R> finisher()
Supplier<A> supplier()
「新しい結果コンテナを作成するSupplierインターフェースの実装」を返す。
BiConsumer<A, T> accumulator()
「結果コンテナへ新しいデータ要素を追加するBiConsumerインターフェースの実装」を返す。
BinaryOperator<A> combiner()
「2つの結果コンテナを1つに結合するBinaryOperatorインターフェースの実装」を返す。
Function<A, R> finisher()
(オプション)「コンテナに対する最終的な変換を行うFunctionインターフェースの実装」を返す。
関数型インターフェースの実装を返す関数
Collectorインターフェースに定義された各メソッドは、次のように関数型インターフェースの実装、すなわちラムダ式もしくはメソッド参照を返す。
interface Supplier<T> { T get(); }
Supplier<String> getSupplier() {
return () -> {
return "hello world";
};
}
Collectorsクラス
java.util.stream.Collectorsクラスには、前述のCollectorインターフェースの実装を取得するためのファクトリメソッドが定義されている。
Collectorsクラスのメソッドを使用することで、自前でCollectorインターフェースを実装する手間を省くことができる。
collect()、Collector、Collectorsの関係性
collect()メソッドは、ストリーム要素を収集し、可変結果コンテナに格納する。
Collectorインターフェースは、リダクション操作を取得するための関数を提供する。
Collectorsクラスは、一般的なリダクション操作の実装を提供する。
collect()メソッドにはオーバーロードが2つあり、Collectorsクラスが提供するCollectorインターフェースの実装オブジェクトを渡すもの以外にも、自分で実装を指定する方法も用意されている。
用意されたものを使う場合は、
<R,A> R collect(Collector<? super T,A,R> collector)
を利用し、自前で実装する場合は
<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
を利用する。
collect()を使用する
List<String> list = List.of("E", "B", "D", "C", "A");
Stream<String> stream = list.stream();
// リストをセットに変換
Set<String> result = stream.collect(Collectors.toSet());
参考
・徹底攻略Java SE 11 Gold問題集[1Z0-816]対応
・徹底解説!Project Lambdaのすべて リターンズ
