S-JIS[2014-04-13/2021-03-21] 変更履歴

Java Streamサンプル

JavaStreamの実験。


forEachの例

Streamの終端処理forEachメソッドだけあれば実現できる。
実際にはそれだけだと便利ではないので、他にもメソッドが色々用意されているわけだが。

forEachは戻り値を返さないので、すなわち副作用を起こすためのメソッドである。
副作用を起こす処理とは、メソッドの外部に変化を起こすような処理のこと。
例えばコンソールに値を出力(表示)するとか、外部の変数に値を代入するとか。

→Scalaのforeach()


Stream内の値を表示する例

import java.util.stream.Stream;
		Stream<String> stream = Stream.of("abc", "def");
ラムダ式を使う方法
stream.forEach(t -> System.out.println(t));
メソッド参照を使う方法
stream.forEach(System.out::println);
処理イメージ
for (Iterator<String> i = stream.iterator(); i.hasNext();) {
	String t = i.next();

	System.out.println(t);
}

Streamを変換する例

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構文が使える)。
詳細な説明


mapの例

Streamのmapメソッドは、値を別の値(別の型)に変換するメソッド。

個人的には、Streamのメソッドの中で一番便利なのはmapメソッドだと思う。
あるListから「入っている値を別の型に変えたList」を作りたい場面はよくある。
今までは複数行のコーディングをする必要があったが、Streamを使えば1行で書ける。

プリミティブ型を扱うmap
flatMap
→Scalaのmap()


StringのListをPathのListに変換する例

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::get).collect(Collectors.toList());
従来の方法
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(_))

filterの例

Streamのfilterメソッドは、条件を満たした値だけを抽出するメソッド。
mapと並んでよく紹介される。

→Scalaのfilter()


Stringクラスの中で可変長引数を持つメソッドを抽出する例

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);
	}
}

peekの例

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(の中間処理メソッド)はそういう挙動ではなく、もっと効率が良い。


flatMapの例

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
List.class
Map.class
  Set.class→
add()
remove()
euqlas()
add()
remove()
equals()
add()
remove()
get()
equals()
remove()
get()
equals()
  add()→
add
add
remove
equals
add
remove
get
equals
remove
get
equals
add
remove
equals
get









 
  remove()→
remove
  equals()→
equals
  …→
  List.class→
add()
remove()
get()
euqlas()
  add()→
add
  remove()→
remove
  get()→
get
  equals()→
equals
  …→
  Map.class→
remove()
get()
euqlas()
  remove()→
remove
  get()→
get
  equals()→
equals
  …→

mapMultiの例

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);
				}
			})

オブジェクトとプリミティブを変換するmap

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::parseInt);
val ss = Stream("123", "456")
val is = ss.map(_.toInt)
Stringをintに変換する例2
Stream<String> ss = Stream.of("a", "bc", "def");
IntStream is = ss.mapToInt(s -> s.length());
IntStream is = ss.mapToInt(String::length);
val ss = Stream("a", "bc", "def")
val is = ss.map(_.length)
intをStringに変換する例
IntStream is = IntStream.of(123, 456);
Stream<String> ss = is.mapToObj(n -> Integer.toString(n));
Stream<String> ss = is.mapToObj(Integer::toString);
val is = Stream(123, 456)
val ss = is.map(_.toString)
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::valueOf);
val is = Stream(123, 456)
val ws = is.map(Integer.valueOf)
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::intValue);
val ws = Stream[Integer](123, 456)
val is = ws.map(_.intValue)
intをlongに変換する例
IntStream is = IntStream.of(123, 456);
LongStream ls = is.mapToLong(n -> n);
LongStream ls = is.asLongStream();
val is = Stream(123, 456)
val ls = is.map(_.toLong)

なお、「n -> n」というラムダ式は「同じ値を返す関数」のように見えるが、上記の使い方ではint→Integerだったりint→longだったり、微妙に型が異なっている。(ラムダ式は戻り値の型に関しては上手く 推論してくれる)
したがって、IntUnaryOperator.identity()は使えない。(このidentityメソッドは「(int) -> int」なので)


collectの例

Stream#collectメソッドは、Streamを走査して値を取得するのに使う。
sumやreduce・forEachといった終端処理メソッドも、内部ではcollectと同様の処理を行っている。

collectには引数を3つ渡すものとCollectorインターフェースを渡すものがあるが、渡し方が異なるだけ。
Collectorを実装する例


Listを生成する例

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()という中間処理を行おうとしている)


Stringを生成する例

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

Streamへ戻る / Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま