【Java】StreamAPIについて

本稿ではjava.util.Streamで、できることや挙動について確認していきたいと思います。

StreamAPIの各種メソッドについて、こちらでいくつか紹介しています。

StreamAPI使用時に多用されるラムダ式に関しては、こちらで紹介しています。



StreamAPIについて

StreamAPIはJava 8 から追加された機能で、要素の集合体に対する操作を記述できる機能が豊富に含まれています。

特定処理を各要素に実行、特定条件で要素の絞り込み、要素の変換、グループ化、要素同士の演算、並び替え、並列処理など、できることは多いです。

同じくJava8から追加されたラムダ式、メソッド参照の記述と組み合わせるとより簡潔なコードで複雑な操作を行えます。

ストリームの生成方法はいくつかありますが、実際にアプリケーションを作成する場合はCollectionや配列からの変換のあと使用する形になるかと思います。

import java.util.ArrayList;
import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		
		List<String> list = new ArrayList<>();
		list.add("A");
		list.add("B");
		list.add("C");
		
		//各種要素に対する処理を実行
		list.stream()
			.forEach(System.out::println);
	}
}
実行結果
A
B
C

各種処理はstream.中間操作.中間操作.終端操作と繋げることができ、連続で操作を行えます。

中間操作にはfilter(絞り込み)、map(値変換)などが含まれます。

終端操作はforEach(処理実行)、anyMatch(条件判定)などが含まれます。

中間操作は0回以上実行でき、1つの終端操作により処理を完結できます。

import java.util.stream.Stream;
 
public class Test {
 
	public static void main(String[] args) {
		
		Stream.of("ABC", "BXXXX", "C")
			.map(e -> e.length())//文字数値に変換
			.filter(e -> e >= 3)//3以上の数値に絞り込み
			.forEach(System.out::println);//値を表示
	}
}
実行結果
3
5

中間操作の時点で処理は行われない

中間操作を行った時点では処理は行われず終端操作が実行されるときに一連の処理として実行される点に注意してください。

上記のコードをfor文でイメージすると

import java.util.List;

//こっちだよ!!
public class Test {
 
	public static void main(String[] args) {
		
		for ( String s : List.of("ABC", "BXXXX", "C") ) {
			int len = s.length();//文字数値に変換
			if ( len >= 3 ) {//3以上の数値に絞り込み
				System.out.println(len);//値を表示
			}
		}
	}
}

のような感じで、下記のような処理のイメージではないので注意。

import java.util.ArrayList;
import java.util.List;

//こっちじゃないよ!!
public class Test {
 
	public static void main(String[] args) {

		List<String> list = List.of("ABC", "BXXXX", "C");
		List<Integer> nums = new ArrayList<>();
		for ( String s : list ) {
			nums.add(s.length());//文字数値に変換
		}
		List<Integer> filterNums = new ArrayList<>();
		for ( int n : nums ) {
			if ( n >= 3 ) {//3以上の数値に絞り込み
				filterNums.add(n);
			}
		}
		for ( int n : filterNums ) {
			System.out.println(n);//値を表示
		}
	}
}

中間操作で処理が実行されていないかはSystem.out.printlnですぐにわかりますので試してみましょう。

import java.util.stream.Stream;
 
public class Test {
 
	public static void main(String[] args) {
		
		System.out.println("開始");
		Stream<String> stream = Stream.of("ABC", "BXXXX", "C");
		System.out.println("構築");
		Stream<Integer> stream2 = stream.map(e -> {
			System.out.println("map実行中");
			return e.length();//文字数値に変換
		});
		System.out.println("map中間操作終了");
		Stream<Integer> stream3 = stream2.filter(e -> {
			System.out.println("filter実行中");
			return e >= 3;//3以上の数値に絞り込み
		});
		System.out.println("filter中間操作終了");
		stream3.forEach(System.out::println);//値を表示
		System.out.println("終了");
	}
}
実行結果
開始
構築
map中間操作終了
filter中間操作終了
map実行中
filter実行中
3
map実行中
filter実行中
5
map実行中
filter実行中
終了

結果を見てもらえればわかる通り、「map実行中」より先に「map中間操作終了」が出力されています。

Stream生成後に元のリストを操作する

Collectionからstreamを生成し元のCollectionを操作すると操作後の要素で処理されます。

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
 
public class Test {
	public static void main(String[] args) {
		List<Integer> list = new ArrayList<>(List.of(1,2));
		Stream<Integer> stream = list.stream();
		
		//stream生成後の操作
		list.add(3);
		list.remove(0);
		
		//結果を表示
		stream.forEachOrdered(System.out::println);
	}
}
実行結果
2
3

結果からわかる通り、終端操作時に初めて実行対象の要素が確定します。



必要のない処理を行わない

streamの処理は必要がないと判断された処理を実行しません。

peekなどの中間操作で値出力してみると思ったより処理が省かれていたりします。

import java.util.stream.Stream;
 
public class Test {
 
	public static void main(String[] args) {
		
		boolean isMatch = Stream.of(1,2,3)
			.peek(System.out::println)//値を表示
			.allMatch(e -> e == 1);//値が1かどうか
		
		System.out.println(isMatch);
	}
}
実行結果
1
2
false

3が出力されていません。

allMatchは結果がfalseとなった瞬間以降の処理結果がどうなろうとfalseになるため、3以降の処理は実行する必要がないからですね。

他のパターンもいくつか見ておきましょう。

import java.util.stream.Stream;
 
public class Test {
 
	public static void main(String[] args) {
		
		int result = Stream.of(1,2,3)
			.peek(System.out::println)//値を表示
			.findFirst()//最初の要素を取得
			.get();//値変換
		
		System.out.println("結果:"+result);
	}
}
実行結果
1
結果:1

findFirstは最初の要素を取得しますので2以降の処理は実行する必要がないですね。

では途中にsortedを挟んでみましょう。

import java.util.stream.Stream;
 
public class Test {
 
	public static void main(String[] args) {
		
		int result = Stream.of(1,2,3)
			.peek(System.out::println)//値を表示
			.sorted()//並び替え
			.findFirst()//最初の要素を取得
			.get();//値変換
		
		System.out.println("結果:"+result);
	}
}
実行結果
1
2
3
結果:1

取得データの結果は変わらないのに1,2,3が出力されました。

findFirstは最初の要素を取得しますが、並び替えの中間処理があるため、全要素を処理しないと結果を確定できません。

よって1,2,3の処理が実行されました。

このように確定できない処理の場合はすべての要素を処理していることがわかります。

並列処理

StreamAPIはparallelメソッドを呼び出すだけでマルチスレッド化できます。

import java.util.stream.Stream;
 
public class Test {
 
	public static void main(String[] args) {
		
		Stream.of(1,2,3,4,5,6,7).parallel()
			.forEach(e -> {
				long id = Thread.currentThread().getId();
				System.out.printf("id=%03d::element=%s\n",id,e);
			});
		
	}
}
実行結果
id=001::element=5
id=016::element=1
id=014::element=2
id=016::element=3
id=015::element=7
id=014::element=6
id=001::element=4

スレッドIDが変わっているため別スレッドであることがわかります。

せっかくなので並列streamと順次streamで速度比較してみましょうか。

ちょっとした処理では順次streamが速いですが重い処理やIO処理が入ってくると環境次第で並列streamのほうが速くなるかもしれません。

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
 
public class Test {
 
	public static void main(String[] args) {
		List<Integer> list = Stream.iterate(0, i -> i <= 10000000, i -> ++i)
				.collect(Collectors.toUnmodifiableList());

		//ダミー処理
		execute(list.stream());
		execute(list.stream().parallel());
		
		//並列ストリーム
		Stream<Integer> ps = list.parallelStream();
		long pt = System.currentTimeMillis();
		execute(ps);
		pt = System.currentTimeMillis() - pt;
		
		//順次ストリーム
		Stream<Integer> ss = list.stream();
		long st = System.currentTimeMillis();
		execute(ss);
		st = System.currentTimeMillis() - st;
		

		System.out.println("順次:"+st+"ms");
		System.out.println("並列:"+pt+"ms");
	}
	
	static void execute(Stream<Integer> stream) {
		long result = stream.mapToLong(e -> e)
			.filter(e -> e % 2 == 1)
			.sum();
		System.out.println(result);
	}
}
実行結果
25000000000000
25000000000000
25000000000000
25000000000000
順次:30ms
並列:14ms

DBアクセスとかしたら結構変わるのかなあ・・・


関連記事


Java

Posted by nompor