45.Streamは気を付けて使うべし
- Streamパイプラインの処理は終端処理が呼ばれて初めて実行される。
- 簡単に並列Streamにすることができるが、たいていの場合、並列にするのは適切でない。(Item48)
- いつStreamを使うべきかにかっちりしたルールはない。ヒューリスティックな解があるのみ。
- 指定したファイルに含まれる単語(例1)、行(例2,3)について、アナグラム毎にまとめ、指定した数以上の単語があるアナグラムを表示するプログラムが以下。
例2はstream処理を使いすぎて読みづらくなっている。例3が適切な使い方。
下の例のラムダ式内のgなどは本当はgroupとして、読みやすさを向上させるべき。
alphabetize のようなヘルパーメソッドを作ることは、ストリーム処理を書くにおいて、読みやすさを向上させるために重要なことである。
package tryAny.effectiveJava;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Anagrams {
public static void main(String[] args) throws IOException {
File dictionary = new File("/application/gradle/getting-started.html");
int minGroupSize = 2;
Map<String, Set<String>> groups = new HashMap<>();
// 例1 start
try (Scanner s = new Scanner(dictionary)) {
while (s.hasNext()) {
String word = s.next();
groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
}
}
for (Set<String> group : groups.values()) {
if (group.size() >= minGroupSize) {
System.out.println(group.size() + ":" + group);
}
}
// 例1 end
// 例2 start(こっちは一行毎でアナグラム取っているから例1と結果違う)
Path dictionary2 = Paths.get("/application/gradle/getting-started.html");
try (Stream<String> words = Files.lines(dictionary2)) {
words.collect(Collectors.groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new, (sb, c) -> sb.append((char) c), StringBuilder::append).toString()))
.values().stream().filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ":" + group).forEach(System.out::println);
}
// 例2 end
// 例3 start(例2と同じ結果)
try (Stream<String> words = Files.lines(dictionary2)) {
words.collect(Collectors.groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(g -> System.out.println(g.size() + ":" + g));
}
// 例3 end
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
- 以下の例のように、char型のstream処理は直感的でない動きをしうるので、原則としてchar型の値をstream処理で扱うべきでない。
package tryAny.effectiveJava;
public class CharStream {
public static void main(String[] args) {
"Hello, world!".chars().forEach(System.out::print);
// 72101108108111443211911111410810033 が表示
System.out.println();
"Hello, world!".chars().forEach(x -> System.out.print((char) x));
// Hello, world! が表示
}
}
-
既存のforループをstreamに置き換えるにおいては、やる意味があるときのみやるべき。
-
関数型オブジェクトを使用したstream処理ではできないが、code block(普通の繰り返し文のこと?)ではできることが以下。
- ラムダ式ではローカル変数について、実質finalなものしか読み取れないが、code blockでは、どんなものでも読めるし変更もできる。
- code block では enclosing methodからリターンすることができ(どういうこと?)、ループからbreakやcontinueの操作ができ、宣言している検査例外をスローすることができるが、ラムダ式では以上のことはできない。
-
streamで簡単にできるようになることは以下。
- 画一的な要素の変換
- 要素のフィルタリング処理
- 単一のオペレーションで要素を結びつける処理(加えたり、最小値を出したり)
- 要素をcollectionに集約する処理(ある属性でグルーピングするなど)
- 特定の基準を満たす要素を探す処理
-
パイプライン上の別のステージにある要素を同時に扱うような処理は、streamでは困難。
以下の例では、Mersenne素数というものを出力している。
Mersenne素数はpが素数であったときに、2^p-1で表される数で、必ず素数になるものである。
stream処理で、
素数→Mersenne素数→20個で区切る→表示
としているが、表示する部分では、元となった素数にアクセスすることはできない(今回は結果から逆算することができたが)。
package tryAny.effectiveJava;
import static java.math.BigInteger.*;
import java.math.BigInteger;
import java.util.stream.Stream;
public class MersennePrimes {
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)).filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
// .forEach(System.out::println);
.forEach(mp -> System.out.println(mp.bitLength() + ":" + mp));
}
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
}
- streamを使うべきか、繰り返し処理をすべきか迷う処理はたくさんある。迷った場合には、両方試してみてどちらが良いか判断してみるべき。