Streamの終端処理はforEachメソッドだけあれば実現できる。
実際にはそれだけだと便利ではないので、他にもメソッドが色々用意されているわけだが。
forEachは戻り値を返さないので、すなわち副作用を起こすためのメソッドである。
副作用を起こす処理とは、メソッドの外部に変化を起こすような処理のこと。
例えばコンソールに値を出力(表示)するとか、外部の変数に値を代入するとか。
→Scalaのforeach()
import java.util.stream.Stream;
Stream<String> stream = Stream.of("abc", "def");
ラムダ式を使う方法 |
stream.forEach(t -> System.out.println(t)); |
メソッド参照を使う方法 |
stream.forEach(System.out
|
処理イメージ |
for (Iterator<String> i = stream.iterator(); i.hasNext();) { String t = i.next(); System.out.println(t); } |
StreamをListに変換するにはCollectors.toList()を使うのが常套手段だが、forEachで書くと以下のようになる。
import java.util.ArrayList; import java.util.List; import java.util.stream.Stream;
Stream<String> stream = Stream.of("abc", "def"); // List<String> list = stream.collect(Collectors.toList()); List<String> list = new ArrayList<>(); stream.forEach(t -> list.add(t)); System.out.println(list);
→collectメソッドの引数を自作してListへ変換する例
→Collectorオブジェクトを自作してListへ変換する例
同様に、IntStream内の値を合算するにはIntStream#sum()があるが、forEachで書くと以下のようになる。
import java.util.stream.IntStream;
IntStream stream = IntStream.of(123, 456); // int sum = stream.sum(); int[] sum = { 0 }; stream.forEach(n -> sum[0] += n); System.out.println(sum[0]);
「int sum = 0;」と定義して「n -> sum += n」と書きたいところだが、
Javaのラムダ式は外部のローカル変数に対してはfinalな場合しかアクセスできないので、int配列にしている。
Scalaだったら「var sum = 0」と定義して「n => sum += n」(あるいは「sum += _」)と記述できる。
for-each構文を使用する例。[2019-11-28]
Stream<String> stream = 〜;
// for-each構文
for (String s : (Iterable<String>) stream::iterator) {
System.out.println(s);
}
Streamのiteratorメソッドをメソッド参照で呼ぶことにより、Iterableへ変換することが出来る(for-each構文が使える)。
→詳細な説明
Streamのmapメソッドは、値を別の値(別の型)に変換するメソッド。
個人的には、Streamのメソッドの中で一番便利なのはmapメソッドだと思う。
あるListから「入っている値を別の型に変えたList」を作りたい場面はよくある。
今までは複数行のコーディングをする必要があったが、Streamを使えば1行で書ける。
→プリミティブ型を扱うmap
→flatMap
→Scalaのmap()
import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors;
List<String> slist = Arrays.asList("a.txt", "b.txt");
mapメソッドを使う方法 (ラムダ式) |
List<Path> plist = slist.stream().map(s -> Paths.get(s)).collect(Collectors.toList()); |
mapメソッドを使う方法 (メソッド参照) |
List<Path> plist = slist.stream().map(Paths
|
従来の方法 |
List<Path> plist = new ArrayList<>(); for (String s : slist) { plist.add(Paths.get(s)); } |
Scalaだと、ListからStreamに変換しなくてもそのままmapが使えるんだけどね^^;
val slist = Seq("a.txt", "b.txt") val plist = slist.map(Paths.get(_))
Streamのfilterメソッドは、条件を満たした値だけを抽出するメソッド。
mapと並んでよく紹介される。
→Scalaのfilter()
import java.lang.reflect.Method; import java.util.List; import java.util.Arrays; import java.util.stream.Stream;
filgerメソッドを使う方法 |
Stream<Method> stream = Arrays.stream(String.class.getMethods()); List<Method> list = stream.filter(method -> method.isVarArgs()).collect(Collectors.toList()); |
従来の方法 |
Method[] array = String.class.getMethods(); List<Method> list = new ArrayList<>(); for (Method method : array) { if (method.isVarArgs()) { list.add(method); } } |
Streamのpeekメソッドは、「Streamの状態を変えないforEach」のような感じのメソッド。
(forEachは終端処理だが、peekは中間処理)
デバッグ目的でStream内の値を表示したりするのに使う。
mapやfilterといった中間処理メソッドでは、そのメソッドを呼び出しただけでは実際の変換処理や抽出処理は行わない。
そのことがpeekメソッドを使って途中経過を表示してみるとよく分かる。
import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream;
Stream<String> s0 = Stream.of("a", "bcd", "z"); // 文字数が1文字だけのものを抽出し、2文字に増幅させる Stream<String> s1 = s0.peek(t -> System.out.printf("peek1=%s%n", t)); Stream<String> s2 = s1.filter(t -> t.length() == 1); Stream<String> s3 = s2.peek(t -> System.out.printf("peek2=%s%n", t)); Stream<String> s4 = s3.map(t -> t + t); Stream<String> s5 = s4.peek(t -> System.out.printf("peek3=%s%n", t)); List<String> r = s5.collect(Collectors.toList()); System.out.println(r);
↓出力結果
peek1=a peek2=a peek3=aa peek1=bcd peek1=z peek2=z peek3=zz [aa, zz]
処理される順番は、要素1つずつに対して「1個目のpeek」→「filter」→「2個目のpeek」→「map」→「3個目のpeek」であることが分かる。
ある値(この例では「bcd」)が途中のfilterで条件外になると、残りのmap以降は呼ばれていない。
つまり、(mapやfilterをいくつ組み合わせても、)全データを走査する回数は1回だけ(ループが1回だけ)ということになる。
以下のようなイメージ。
Stream<String> stream = Stream.of("a", "bcd", "z"); Builder<String> temp = Stream.builder(); for (Iterator<String> i = stream.iterator(); i.hasNext();) { String t = i.next(); // peek System.out.printf("peek1=%s%n", t); // filter if (t.length() == 1) { // fall through } else { continue; } // peek System.out.printf("peek2=%s%n", t); // map String m = t + t; // peek System.out.printf("peek3=%s%n", m); temp.add(m); } // collect List<String> r = temp.build().collect(Collectors.toList()); System.out.println(r);
もしStreamでなくListだったら、以下のようなプログラムになるだろう。
List<String> l0 = Arrays.asList("a", "bcd", "z"); List<String> l1 = peek(l0, t -> System.out.printf("peek1=%s%n", t)); List<String> l2 = filter(l1, t -> t.length() == 1); List<String> l3 = peek(l2, t -> System.out.printf("peek2=%s%n", t)); List<String> l4 = map(l3, t -> t + t); List<String> l5 = peek(l4, t -> System.out.printf("peek3=%s%n", t)); List<String> r = l5; System.out.println(r);
private static List<String> peek(List<String> list, Consumer<String> action) { for (String t : list) { action.accept(t); } return list; }
private static List<String> filter(List<String> list, Predicate<String> predicate) { List<String> result = new ArrayList<>(list.size()); for (String t : list) { if (predicate.test(t)) { result.add(t); } } return result; }
private static List<String> map(List<String> list, Function<String, String> mapper) { List<String> result = new ArrayList<>(list.size()); for (String t : list) { result.add(mapper.apply(t)); } return result; }
↓実行結果
peek1=a peek1=bcd peek1=z peek2=a peek2=z peek3=aa peek3=zz [aa, zz]
Streamの場合は、まず「a」というデータに対してpeek1→peek2→peek3の順で出力されていたが、
Listを使った場合は、最初にpeek1に対して全データが表示されている。
また、ソースコードを見れば分かる通り、filterやmapを呼ぶ度にListの走査(ループ)を行い、新しいListインスタンスを作っている。
しかしStream(の中間処理メソッド)はそういう挙動ではなく、もっと効率が良い。
StreamのflatMapメソッドは、「複数個の値を返す関数を使えるmap」のような感じ。
mapメソッドに渡す関数は、入力1個につき変換した出力が必ず1個になる。関数の型は「(T) -> R
」。
flatMapメソッドに渡す関数は、出力の個数が自由(0個も可)。関数の型は「(T) -> Stream<R>
」。Rを複数個返す。
なお、Java16でmapMultiメソッドが追加された。[2021-03-21]
機能はflatMapと同等なので、返すStreamを用意するのが面倒な場合は、mapMultiの方がflatMapより扱いやすいと思う。
→mapMultiの例
→ScalaのflatMap()
ちなみにScalaの場合、flatMapに渡す関数が返す型は、複数の値を返すもの(Seqの子クラス)であれば何でも構わない(Listでも配列でもよい)。
import java.util.*; import java.util.stream.Stream;
Stream<Class<?>> stream = Stream.of(Set.class, List.class, Map.class);
stream.flatMap(clazz -> Arrays.stream(clazz.getMethods()))
.map(method -> method.getName())
.distinct()
.sorted()
.forEach(System.out::
println);
この例におけるデータの変遷のイメージは、以下のような感じ。
最初のStream | → | flatMap関数 の各処理 |
→ | flatMapの結果 のStream |
→ | map関数の 各処理 |
→ | mapの結果 のStream |
→ | distinctの結果 のStream |
|||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
Set.class→ |
|
→ |
|
add()→ |
|
→ |
|
→ |
|
|||||||||||||||||||||||||||||||||||||||||
remove()→ |
|
→ | |||||||||||||||||||||||||||||||||||||||||||||||||
equals()→ |
|
→ | |||||||||||||||||||||||||||||||||||||||||||||||||
…→ | … |
→ | |||||||||||||||||||||||||||||||||||||||||||||||||
List.class→ |
|
→ | add()→ |
|
→ | ||||||||||||||||||||||||||||||||||||||||||||||
remove()→ |
|
→ | |||||||||||||||||||||||||||||||||||||||||||||||||
get()→ |
|
→ | |||||||||||||||||||||||||||||||||||||||||||||||||
equals()→ |
|
→ | |||||||||||||||||||||||||||||||||||||||||||||||||
…→ | … |
→ | |||||||||||||||||||||||||||||||||||||||||||||||||
Map.class→ |
|
→ | remove()→ |
|
→ | ||||||||||||||||||||||||||||||||||||||||||||||
get()→ |
|
→ | |||||||||||||||||||||||||||||||||||||||||||||||||
equals()→ |
|
→ | |||||||||||||||||||||||||||||||||||||||||||||||||
…→ | … |
→ |
Java16で追加されたmapMultiメソッドの例。[2021-03-21]
mapMultiは機能的にはflatMapメソッドと同等(1個の値を複数の値に変換する)だが、自分でStreamを返すのではなく、返したい値を「
(渡されたconsumerに)acceptする」という形をとる。
flatMapでStreamを作るのが面倒な場合は、mapMultiの方が便利だと思う。
mapMultiの引数にはラムダ式を渡すと分かりやすい。
ラムダ式の第1引数で、Streamの値を1個受け取る。
第2引数はconsumer決め打ち。ただしConsumerの型は、変換後の値の型になる。このconsumerに「返したい値」を渡すことになる。
import java.util.*; import java.lang.reflect.Method; import java.util.function.Consumer; import java.util.stream.Stream;
Stream<Class<?>> stream = Stream.of(Set.class, List.class, Map.class);
stream.mapMulti((Class<?> clazz, Consumer<Method> consumer) -> {
for (var method : clazz.getMethods()) {
consumer.accept(method);
}
})
.map(method -> method.getName())
.distinct()
.sorted()
.forEach(System.out::
println);
mapMultiのConsumerの型を明示してやらないとコンパイルが通らないことがあるようだ。
なので、上記の例ではラムダ式の引数に型を明記している。
いちいちラムダ式の引数の型を書くのは残念な感じなので、Consumerの型をmapMultiの型引数に明記する方がまだましか。
import java.util.*; import java.lang.reflect.Method; import java.util.stream.Stream;
stream.<Method>mapMulti((clazz, consumer) -> {
for (var method : clazz.getMethods()) {
consumer.accept(method);
}
})
Javaではプリミティブ型(intやlong・double等)はオブジェクトとして扱えないので、Streamでも特別な扱いをしている。
まず、プリミティブ型で値を保持するプリミティブ系のStream(IntStream・LongStream・DoubleStream)がある。
また、mapにもintに変換するmapToIntメソッド、longに変換するmapToLongメソッド、doubleに変換するmapToDoubleメソッドが存在する。
プリミティブ系のStreamでは、逆にオブジェクトに変換するmapToObjメソッドが存在する。
ちなみに、ScalaはInt等もクラス扱いなので、プリミティブ用のクラスは無いし、mapで数値も扱える。
import java.util.stream.IntStream; import java.util.stream.Stream;
例 | コード | Scala相当 |
---|---|---|
Stringをintに変換する例 |
Stream<String> ss = Stream.of("123", "456"); IntStream is = ss.mapToInt(s -> Integer.parseInt(s)); IntStream is = ss.mapToInt(Integer
|
val ss = Stream("123", "456") |
Stringをintに変換する例2 |
Stream<String> ss = Stream.of("a", "bc", "def"); IntStream is = ss.mapToInt(s -> s.length()); IntStream is = ss.mapToInt(String
|
val ss = Stream("a", "bc", "def") |
intをStringに変換する例 |
IntStream is = IntStream.of(123, 456); Stream<String> ss = is.mapToObj(n -> Integer.toString(n)); Stream<String> ss = is.mapToObj(Integer
|
val is = Stream(123, 456) |
intをIntegerに変換する例 |
IntStream is = IntStream.of(123, 456); Stream<Integer> ws = is.mapToObj(n -> n); |
|
IntStream is = IntStream.of(123, 456); Stream<Integer> ws = is.mapToObj(n -> Integer.valueOf(n)); Stream<Integer> ws = is.mapToObj(Integer
|
val is = Stream(123, 456) |
|
IntStream is = IntStream.of(123, 456); Stream<Integer> ws = is.boxed(); |
||
Integerをintに変換する例 |
Stream<Integer> ws = Stream.of(123, 456); IntStream is = ws.mapToInt(n -> n); |
|
Stream<Integer> ws = Stream.of(123, 456); IntStream is = ws.mapToInt(n -> n.intValue()); IntStream is = ws.mapToInt(Integer
|
val ws = Stream[Integer](123, 456) |
|
intをlongに変換する例 |
IntStream is = IntStream.of(123, 456); LongStream ls = is.mapToLong(n -> n); LongStream ls = is.asLongStream(); |
val is = Stream(123, 456) |
なお、「n -> n」というラムダ式は「同じ値を返す関数」のように見えるが、上記の使い方ではint→Integerだったりint→longだったり、微妙に型が異なっている。(ラムダ式は戻り値の型に関しては上手く
推論してくれる)
したがって、IntUnaryOperator.
identity()は使えない。(このidentityメソッドは「(int)
-> int
」なので)
Stream#collectメソッドは、Streamを走査して値を取得するのに使う。
sumやreduce・forEachといった終端処理メソッドも、内部ではcollectと同様の処理を行っている。
collectには引数を3つ渡すものとCollectorインターフェースを渡すものがあるが、渡し方が異なるだけ。
→Collectorを実装する例
StreamからListを生成して値を取り出すにはCollectors.toList()を使うのが便利だが、例として実装してみる。
(→List変換Collectorを自作する例)
import java.util.ArrayList; import java.util.List; import java.util.function.BiConsumer; import java.util.function.Supplier; import java.util.stream.Stream;
Stream<String> stream = Stream.of("a", "b", "c"); // ラムダ式の引数の型まで明示したバージョン // Supplier<List<String>> supplier = () -> new ArrayList<>(10); // BiConsumer<List<String>, String> accumulator = (List<String> l, String t) -> l.add(t); // BiConsumer<List<String>, List<String>> combiner = (List<String> l1, List<String> l2) -> l1.addAll(l2); // ラムダ式の引数の型を省略したバージョン Supplier<List<String>> supplier = () -> new ArrayList<>(10); BiConsumer<List<String>, String> accumulator = (l, t) -> l.add(t); BiConsumer<List<String>, List<String>> combiner = (l1, l2) -> l1.addAll(l2); List<String> list = stream.collect(supplier, accumulator, combiner); // ラムダ式をcollectの引数に直接指定したバージョン // List<String> list = stream.collect( // () -> new ArrayList<>(10), // (l, t) -> l.add(t), // (l1, l2) -> l1.addAll(l2) // ); System.out.println(list);
collectには結果を生成するのに使う関数を渡す。
Stream内の値(要素)の型をT(今回の例ではString)、欲しい型をR(今回の例ではList<String>)とすると、各関数の型は以下のようになる。
引数名 | 型 | 説明 | 例 | |
---|---|---|---|---|
supplier | Supplier<R> | () -> R |
出力するクラス(R)のインスタンスを生成する関数。 | () -> new ArrayList<String>(10) |
accumulator | BiConsumer<R, T> | (R, T) -> void |
要素の値(T)を出力する値(R)に集約する関数。 | (list, t) -> list.add(t) |
combiner | BiConsumer<R, R> | (R, R) -> void |
2つのRを1つにまとめる関数。 | (list1, list2)
-> list1.addAll(list2) |
このcollectメソッドは、イメージとしては以下のような動作をする。
Stream<String> stream = Stream.of("a", "b", "c"); // supplier List<String> r = new ArrayList<>(10); for (Iterator<String> i = stream.iterator(); i.hasNext();) { String t = i.next(); // accumulator r.add(t); } System.out.println(r);
combinerが使われるのは、データを分割して処理されるとき。(データ量が多い並列ストリーム)
以下のようなイメージになる。
Stream<String> stream = Stream.of("a", "b", "c", "d", "e");
// Streamを分割
Stream<String> s1 = stream.limit(3); // 先頭3件
Stream<String> s2 = stream.skip(3); // 先頭3件をスキップした残り(これを実際に動かすとIllegalStateExceptionが発生する)
List<String> list1 = toList(s1);
List<String> list2 = toList(s2);
// combiner
list1.addAll(list2);
System.out.println(list1);
static List<String> toList(Stream<String> stream) { // supplier List<String> list = new ArrayList<>(10); for (Iterator<String> i = stream.iterator(); i.hasNext();) { String t = i.next(); // accumulator list.add(t); } return list; }
Streamを分割して、それぞれでsupplierを呼び出してaccumulatorを使って処理を行い、最後にcombinerで結合する。
注意:上記のプログラムを実行すると「java.lang.IllegalStateException: stream has
already been operated upon or closed」が発生する。
中間処理を行った後で元のStreamを使って別の中間処理を行うことは出来ない為。
(上記のイメージでは、stream.limit()という中間処理を行った後で、元のstreamに対してskip()という中間処理を行おうとしている)
IntStreamの各値を文字コードと見なし、そこからStringを生成する例。
(→String変換Collectorを自作する例)
import java.util.stream.IntStream;
IntStream stream = IntStream.of(0x30, 0x31, 0x41, 0x42, 0x43);
// IntStream stream = "01ABC".chars();
CharSequence cs = stream.collect(
StringBuilder::
new,
(sb, c) -> sb.append((char) c),
(sb1, sb2) -> sb1.append(sb2)
);
String s = cs.toString();
System.out.println(s);
ちなみにコードポイントの場合は以下の様になる。(呼び出すappendメソッドが違うだけで、大して変わらない^^;)
IntStream stream = IntStream.of(0x30, 0x31, 0x41, 0x42, 0x43); // IntStream stream = "01ABC".codePoints(); // StringBuilder cs = stream.collect( // StringBuilder::
new, // (sb, c) -> sb.appendCodePoint(c), // (sb1, sb2) -> sb1.append(sb2) // ); StringBuilder cs = stream.collect( StringBuilder::
new, StringBuilder::
appendCodePoint, StringBuilder::
append ); String s = cs.toString(); System.out.println(s);