Java Stream・ラムダ・Optional:遅延評価と並列の落とし穴を実測
peek で見る遅延評価、toMap の重複キー例外、for・stream・parallelStream のウォームアップ付き実測、Optional の誤用、Java 24 の Gatherer まで JDK 26 で実行。
シリーズ:Java 講座 4/6(中級)。 ラムダ式は読めるものの、Stream がいつ実際に動くのか、Collector がなぜ落ちるのか、parallelStream() が効くのかに自信がない方向けです。読み終えると、パイプラインの実行順序を予測し、初心者がつまずく Stream の例外 4 つを直し、ループ・Stream・並列 Stream を実測に基づいて選べるようになります。
Java Gold の試験では Stream API と Optional、Collectors が大きな比重を占めますが、問われるのは主に「何が出力されるか」で、「どれが速いか」「本番でどう落ちるか」までは扱いません。一方、業務コードのレビューで Stream について付くコメントはだいたい決まっています。「これ2回走っています」「キーが重複すると落ちます」「なぜ parallel なんですか」「Optional に get() はやめましょう」。この記事はそれぞれを、実際に動かしたコードと出力そのもので確認します。
実行環境: OpenJDK 26(Temurin 26+35)、Apple Silicon Mac(M1 Pro、8 コア、共通 fork-join プールの並列度 7)。短い例は java ファイル.java、ベンチマークは javac のあと java -Xms2g -Xmx2g で実行し、乱数は new Random(7) で固定しました。
ラムダ式、メソッド参照、関数型インターフェース
ラムダ式は関数型インターフェース(抽象メソッドがちょうど1つのインターフェース)のインスタンスです。よく使う形は java.util.function にそろっていて、メソッド参照は「メソッドを1つ呼ぶだけのラムダ」の省略形です。
Function<String, Integer> len = String::length; // T -> R
Predicate<String> empty = String::isEmpty; // T -> boolean
Supplier<List<String>> mk = ArrayList::new; // () -> T
BiFunction<String, String, Boolean> eq = String::equalsIgnoreCase;
UnaryOperator<String> up = s -> s.toUpperCase(); // T -> T
System.out.println(len.apply("tokyo") + " " + empty.negate().test("") + " "
+ mk.get() + " " + eq.apply("Java", "JAVA") + " " + up.andThen(s -> s + "!").apply("hi"));
5 false [] true HI!
String::length は受け取るオブジェクト自身が引数、String::equalsIgnoreCase は 2 つの引数のうち 1 つ目が受け取るオブジェクト、ArrayList::new はコンストラクタです。試験でも出る3パターンですね。negate() や andThen() といったデフォルトメソッドで、新しいラムダを書かずに組み合わせられます。
ラムダで最初に出会うコンパイルエラーは、ローカル変数に関するものです。
List<Integer> prices = List.of(980, 3200, 600);
int total = 0;
prices.forEach(p -> total += p);
Eff.java:7: error: local variables referenced from a lambda expression must be final or effectively final
prices.forEach(p -> total += p);
^
1 error
ラムダが読めるのは、代入後に一度も変わらない(実質的に final な)ローカル変数だけです。対策は要素数 1 の配列や AtomicInteger を使う小細工ではなく、値を Stream に計算させることです。int total = prices.stream().mapToInt(Integer::intValue).sum();(出力 4780)。C# から来た方は、変数を参照でキャプチャする C# のクロージャより厳しい点に注意してください。C# のラムダ式の記事 と比べてみてください。
peek で目に見える遅延評価
Stream のパイプラインは、終端操作(findFirst、collect、count、forEach など)が結果を求めるまで何もしません。peek でその様子を観察できます。
Stream<String> s = Stream.of("fig", "kiwi", "apple", "banana")
.peek(x -> System.out.println(" peek " + x))
.filter(x -> x.length() > 3)
.map(x -> { System.out.println(" map " + x); return x.toUpperCase(); });
System.out.println("pipeline built, nothing printed yet");
Optional<String> first = s.findFirst();
System.out.println("findFirst -> " + first.get());
System.out.println("-- sorted() is a barrier:");
Stream.of("kiwi", "apple", "fig")
.peek(x -> System.out.println(" before sort " + x))
.sorted()
.peek(x -> System.out.println(" after sort " + x))
.findFirst();
pipeline built, nothing printed yet
peek fig
peek kiwi
map kiwi
findFirst -> KIWI
-- sorted() is a barrier:
before sort kiwi
before sort apple
before sort fig
after sort apple
この出力から3つのことがわかります。パイプラインを組み立てている間は何も表示されませんでした。要素は1つずつ上から下へ流れました。「全部 peek してから全部 filter」ではなく、fig が peek されて落ち、そのあと kiwi が最後まで進みました。そして findFirst が途中で止めたので、apple と banana は読まれてさえいません。2つ目のパイプラインは例外を示しています。sorted() はすべての要素を見ないと最初の1つを出せないので、ここで流れがせき止められます。
実務での結論はこうです。必ず実行されてほしい副作用を peek や map に入れないでください。実行されるかどうか、何回実行されるかは終端操作しだいです。count() はソースからサイズがわかればパイプラインを飛ばしてよいことになっていて、JDK 26 は実際に飛ばします。
List<String> l = List.of("a", "b", "c");
long n = l.stream().peek(x -> System.out.println("peek " + x)).count();
System.out.println("count=" + n);
long m = l.stream().peek(x -> System.out.println("peek " + x)).filter(x -> !x.isEmpty()).count();
System.out.println("count after filter=" + m);
count=3
peek a
peek b
peek c
count after filter=3
1つ目の peek は一度も動いていません。filter を挟むとサイズが事前にわからなくなるので、2つ目のパイプラインは実行されました。「ログ用に入れた peek が出力されない」という質問への答えがこれです。
Collector:groupingBy、counting、joining、toMap
record Order(String city, String item, int yen) {}
List<Order> orders = List.of(
new Order("Osaka", "ramen", 980), new Order("Tokyo", "sushi", 3200),
new Order("Osaka", "takoyaki", 600), new Order("Fukuoka", "ramen", 850),
new Order("Tokyo", "ramen", 1100));
System.out.println(orders.stream()
.collect(Collectors.groupingBy(Order::city, Collectors.counting())));
System.out.println(orders.stream()
.collect(Collectors.groupingBy(Order::city, TreeMap::new, Collectors.counting())));
System.out.println(orders.stream()
.collect(Collectors.groupingBy(Order::city, TreeMap::new, Collectors.summingInt(Order::yen))));
System.out.println(orders.stream().map(Order::item).distinct().sorted()
.collect(Collectors.joining(", ", "[", "]")));
System.out.println("max price per item " + orders.stream()
.collect(Collectors.toMap(Order::item, Order::yen, Integer::max, TreeMap::new)));
{Tokyo=2, Fukuoka=1, Osaka=2}
{Fukuoka=1, Osaka=2, Tokyo=2}
{Fukuoka=850, Osaka=1580, Tokyo=4300}
[ramen, sushi, takoyaki]
max price per item {ramen=1100, sushi=3200, takoyaki=600}
1行目に注目してください。groupingBy は HashMap を返すので、都市の順序は決まっていません。帳票や文字列を比較するテストのように順序が大事なら、2行目のようにマップのファクトリとして TreeMap::new を渡します。
よくある間違い:重複キーのある toMap
Map<String, Integer> priceByItem = orders.stream()
.collect(Collectors.toMap(Order::item, Order::yen)); // "ramen" が 3 回出てくる
実際に起きること:
Exception in thread "main" java.lang.IllegalStateException: Duplicate key ramen (attempted merging values 980 and 850)
at java.base/java.util.stream.Collectors.duplicateKeyException(Collectors.java:137)
at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$0(Collectors.java:184)
at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
at java.base/java.util.stream.AbstractList$RandomAccessSpliterator.forEachRemaining(AbstractList.java:722)
…
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:723)
at Collect.main(Collect.java:34)
引数 2 つの toMap は、キーが一意である前提です。テストデータにはラーメンが1件しかなくて通り、本番で落ちる典型的なバグです。ありがたいことに、メッセージにはキーと 2 つの値が出ます。対策: 重複が何を意味するかを決めます。最大値や最後の値を残すならマージ関数(Integer::max、(a, b) -> b)、すべて残すなら groupingBy です。
よくある間違い:Stream を2回使う
Stream はコレクションではなく、一度使ったら終わりのパイプラインです。
Stream<String> names = Stream.of("ana", "bo", "chie");
System.out.println(names.count());
System.out.println(names.filter(n -> n.length() > 2).count());
3
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.base/java.util.stream.AbstractPipeline.<init>(AbstractPipeline.java:201)
…
at java.base/java.util.stream.ReferencePipeline.filter(ReferencePipeline.java:184)
at Twice.main(Twice.java:7)
消費済みの Stream に最初に呼んだ filter で失敗します。よくある原因は、Stream<T> を返すメソッドと、その戻り値を2回使う呼び出し側です。対策: ソース(List<String> names)を持っておき毎回 names.stream() を呼ぶか、ソースの生成が重いなら Supplier<Stream<String>> を渡します。どちらも 3 と 2 を出力しました。
for ループ、stream、parallelStream の実測
第 3 回 と同じ計測関数を使いました。時間を計らないウォームアップのあと 11 回計った中央値で、結果をフィールドに足して JIT が計算を捨てられないようにしています。本来は JMH を使うべきところですが、このシリーズは何もインストールしない方針なので使っていません。処理は「偶数の二乗和」です。
cores=8 commonPool parallelism=7
== A: sum of squares of even values, int[10,000,000]
for loop median 27.1625 ms (min 26.6018, max 27.8053)
IntStream median 33.0102 ms (min 32.3225, max 34.3361)
IntStream.parallel() median 8.9625 ms (min 7.4494, max 10.0775)
== B: same work, List<Integer> of 1,000 elements, 1,000 calls per sample
for loop median 0.6654 ms (min 0.6511, max 0.6747)
stream() median 0.7942 ms (min 0.7686, max 2.4748)
parallelStream() median 6.5678 ms (min 4.3373, max 22.6658)
== C: LinkedList<Integer> of 1,000,000
for loop median 4.5086 ms (min 4.4550, max 4.7740)
stream() median 7.5466 ms (min 7.5023, max 7.8438)
parallelStream() median 4.6535 ms (min 3.8963, max 7.4222)
== D: Stream.iterate(...).limit(2,000,000)
for loop median 0.6605 ms (min 0.6594, max 0.8373)
iterate+limit median 16.1651 ms (min 13.1412, max 18.0171)
iterate+limit parallel median 24.7791 ms (min 14.7319, max 35.6399)
LongStream.range parallel median 0.7683 ms (min 0.3322, max 2.5199)
A:大きなプリミティブ配列。 並列 Stream はループの 3 倍速くなりました。配列はコストなしで半分ずつに分割でき、ボクシングもなく、1,000 万件あればワーカースレッド 7 本を動かす価値のある処理量です。逐次の IntStream はループより 2 割ほど遅い程度でした。
B:並列のほうが遅いケース。 1,000 要素のリストでは、parallelStream() はループより約 10 倍遅くなりました。呼ぶたびにリストを分割し、fork-join プールに配り、待ち、結果を合わせますが、1,000 件ではその準備のほうが処理そのものより重いのです。Web リクエストを処理するコードの多くはこの形です。巨大なコレクションが1つではなく、小さなコレクションが大量にあります。
C と D:予想外の結果。 LinkedList は並列化に向かないとよく言われますが、ここでは並列 Stream はループと同程度で、負けてはいませんでした。本当の落とし穴は Stream.iterate(...).limit(n) です。各要素が直前の要素に依存し、limit が順序を守らなければならないため、並列版は逐次版より遅く(24.8ms 対 16.2ms)、どちらも単純なループより 20 倍以上遅くなりました。同じ数列を分割できる形で表す LongStream.range は速いままでした。3 回の実行で並列側の数字がいちばん揺れましたが(A 5.3〜9.0ms、B 6.6〜17.9ms、D 15.9〜24.8ms)、結論は変わりませんでした。
Optional:良い使い方と悪い使い方
Optional は「結果がないかもしれない」を表す戻り値の型です。null の万能な代わりではありません。
record User(String id, String email) {}
static final Map<String, User> DB = Map.of("u1", new User("u1", "a@example.com"), "u2", new User("u2", null));
static Optional<User> find(String id) { return Optional.ofNullable(DB.get(id)); }
static String slowDefault() { System.out.println(" (slowDefault called)"); return "none"; }
System.out.println(find("u1").map(User::email).orElse("none"));
System.out.println(find("u2").map(User::email).orElse("none")); // map が null を空にする
System.out.println(find("u9").map(User::email).orElse("none"));
System.out.println("orElse:");
System.out.println(find("u1").map(User::email).orElse(slowDefault()));
System.out.println("orElseGet:");
System.out.println(find("u1").map(User::email).orElseGet(Opt::slowDefault));
a@example.com
none
none
orElse:
(slowDefault called)
a@example.com
orElseGet:
a@example.com
良い使い方は、検索メソッドが Optional を返し、map でつないで、最後に一度だけ決めることです。map はマッパーが返した null も空として吸収します(u2 はメールアドレスなし)。落とし穴は orElse(x) です。x は普通の引数なので、値があっても先に評価されます。デフォルト値が DB 呼び出しやオブジェクト生成なら orElseGet を使います。試験でも「何が出力されるか」でよく狙われるところです。
悪い使い方と、その結果です。
try { find("u9").get().email(); }
catch (NoSuchElementException e) { System.out.println("get() on empty -> " + e.getMessage()); }
try { Optional.of(DB.get("u9")); }
catch (NullPointerException e) { System.out.println("Optional.of(null) -> NPE"); }
Optional<User> maybe = null;
try { maybe.isPresent(); }
catch (NullPointerException e) { System.out.println("null Optional -> " + e.getMessage()); }
get() on empty -> No value present
Optional.of(null) -> NPE
null Optional -> Cannot invoke "java.util.Optional.isPresent()" because "maybe" is null
チェックなしの get() は名前が違うだけの NullPointerException です。値が確実にあるなら orElseThrow() で意図を示しましょう。Optional.of は null になりえない値用で、そうでなければ ofNullable です。最後の行は Optional 型の変数そのものが null のケースで、使う意味がなくなります。Optional をフィールドやメソッド引数に使うのも避け(Serializable ではなく、割り当てが1つ増える)、コレクションは包まずに空のリストを返します。補足すると、親切な NPE メッセージに変数名 "maybe" が出るのは -g 付きでコンパイルしたときだけで、ソースファイル実行(java Opt.java)では "<local1>" になりました。
ラムダ内の検査例外
Function.apply は throws を宣言していないので、検査例外を投げるメソッドを呼ぶラムダはコンパイルできません。
List<String> texts = files.stream()
.map(p -> Files.readString(p))
.toList();
Checked.java:8: error: unreported exception IOException; must be caught or declared to be thrown
.map(p -> Files.readString(p))
^
1 error
定番の対策は、小さなヘルパーメソッドの中で検査例外を非検査例外に包み、処理できる場所で取り出すことです。
static String read(Path p) {
try { return Files.readString(p); }
catch (IOException e) { throw new UncheckedIOException(e); }
}
…
try {
List<String> texts = List.of(Path.of("a.txt"), Path.of("missing.txt")).stream()
.map(Checked2::read)
.toList();
} catch (UncheckedIOException e) {
System.out.println("caught: " + e.getCause());
}
caught: java.nio.file.NoSuchFileException: missing.txt
UncheckedIOException はまさにこの用途のためにあります。ただし結果は冷静に見てください。最初の失敗でパイプライン全体が止まります。バッチ処理で「読めるものは読み、残りはエラー一覧に出す」が必要なら、ファイルごとに try/catch を置いた普通の for ループのほうが、どんな Stream の工夫よりも明快です。
Stream.toList() と collect(Collectors.toList())
List<Integer> a = Stream.of(3, 1, 2).collect(Collectors.toList());
a.add(4);
System.out.println("collect(toList()) " + a + " " + a.getClass().getName());
List<Integer> u = Stream.of(3, 1, 2).collect(Collectors.toUnmodifiableList());
System.out.println("toUnmodifiableList " + u.getClass().getName());
System.out.println("toList with null " + Stream.of(1, null, 3).toList());
try { Stream.of(1, null, 3).collect(Collectors.toUnmodifiableList()); }
catch (NullPointerException e) { System.out.println("toUnmodifiableList with null -> " + e); }
List<Integer> b = Stream.of(3, 1, 2).toList();
System.out.println("Stream.toList() " + b + " " + b.getClass().getName());
b.add(4);
collect(toList()) [3, 1, 2, 4] java.util.ArrayList
toUnmodifiableList java.util.ImmutableCollections$ListN
toList with null [1, null, 3]
toUnmodifiableList with null -> java.lang.NullPointerException
Stream.toList() [3, 1, 2] java.util.ImmutableCollections$ListN
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:145)
at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:150)
at ToList.main(ToList.java:17)
IDE は .collect(Collectors.toList()) を短い .toList() に置き換えるようよく提案しますが、これは動作が変わる変更です。あとで結果に add するコードがあれば例外になります。Stream.toList() は変更不可ですが null を許し、toUnmodifiableList() は変更不可で null も拒否します。Collectors.toList() は JDK 26 では ArrayList でしたが、Javadoc は変更可能かどうかを約束していません。変更するリストなら Collectors.toCollection(ArrayList::new) と書きましょう。
Gatherer(Java 24):26 で確認
Java 24 より前は、ウィンドウ、累積和、「直前より上がった値だけ」のような独自の中間操作をするには、Stream の外に出るか Spliterator を自作する必要がありました。JEP 485 で Stream.gather と java.util.stream.Gatherers が Java 24 の正式機能になり、26 でコンパイル・実行できます。
List<Integer> temps = List.of(21, 23, 22, 26, 25, 27, 30);
System.out.println(temps.stream().gather(Gatherers.windowFixed(3)).toList());
System.out.println(temps.stream().gather(Gatherers.windowSliding(3))
.map(w -> w.stream().mapToInt(Integer::intValue).average().orElse(0))
.map(a -> String.format("%.1f", a)).toList());
System.out.println(temps.stream().gather(Gatherers.scan(() -> 0, Integer::sum)).toList());
// 独自の gatherer:直前の値より上がった値だけを残す
Gatherer<Integer, int[], Integer> rising = Gatherer.ofSequential(
() -> new int[]{Integer.MIN_VALUE},
(state, x, down) -> { boolean up = x > state[0]; state[0] = x; return !up || down.push(x); });
System.out.println(temps.stream().gather(rising).toList());
[[21, 23, 22], [26, 25, 27], [30]]
[22.0, 23.7, 24.3, 26.0, 27.3]
[21, 44, 66, 92, 117, 144, 174]
[21, 23, 26, 27, 30]
同じファイルを javac --release 23 でコンパイルすると error: Gatherers is a preview API and is disabled by default. で失敗し(22 と 23 ではプレビュー)、--release 24 では通ります。独自 gatherer の integrator は false を返すと Stream を早めに止めます。要素を飛ばしたときに止まらないよう、!up || down.push(x) を返しています。
判断表
| 状況 | 使うもの | 根拠(上の実測) |
|---|---|---|
| 単純な filter/map/collect、読みやすさ優先 | 逐次 stream() |
A・B でループより 2 割ほど遅い程度 |
| プリミティブ配列のホットループ | for ループか IntStream |
ボクシングなし、ループ 27ms 対 Stream 33ms |
| 大きな配列、独立した CPU 処理、100 万件以上 | parallel() |
8 コアで 9.0ms 対 27.2ms |
| 小さなコレクション、リクエストごとのコード | parallelStream() は使わない |
1,000 件で 10 倍遅い |
| 数列の生成 | Stream.iterate().limit() ではなく IntStream.range |
0.77ms 対 16〜25ms |
| キーが重複しうる | toMap(k, v, merge) か groupingBy |
引数 2 つの toMap は IllegalStateException |
| 結果を2回使う | ソースを持ち .stream() を呼び直す |
Stream は2回目で例外 |
| 検索結果がないかもしれない | Optional を返し、最後は orElseGet |
orElse は引数を必ず評価 |
| 結果のリストを変更する | collect(toCollection(ArrayList::new)) |
Stream.toList() は変更不可 |
| ウィンドウ、累積和 | gather(...)(Java 24 以降) |
パイプラインの中で完結 |
| 項目ごとのエラー処理が要る I/O | 普通の for + try/catch |
検査例外はラムダを越えられない |
限界とバージョンのメモ
- JMH ではありません。 ウォームアップと中央値、全体を 3 回実行。並列の結果がいちばん揺れました(B の1サンプルは 22.7ms)。ミリ秒ではなく比率を結果として見てください。2 コアのマシンなら A の効果はずっと小さくなります。
- fork-join の共通プールは、JVM 内のすべての並列 Stream で共有されます。アプリケーションサーバー上の並列 Stream はほかのリクエストと取り合いになりますが、プログラム1本で計ったこの実測にはその影響は出ません。
- JDK 26 で
--releaseを使って確認したバージョン:Stream.toList()は Java 16(--release 15はcannot find symbolで失敗)。Gatherer は Java 24 で正式(23 ではプレビューのエラー)。ラムダと Stream は Java 8、Files.readStringは Java 11、レコードは Java 16 で、これらは 26 でのみ実行しました。 Collectors.teeing(Java 12)、プリミティブ用の Collector、独自 gatherer の並列動作は扱っていません。- 他の言語で同じ考え方を見るなら、C# の LINQ の遅延実行や、Python のジェネレーター の1つずつ取り出す仕組みが同じ構造です。
自分で動かす
StreamBench.java として保存し、javac StreamBench.java && java -Xms2g -Xmx2g StreamBench で実行します。
import java.util.*;
import java.util.function.LongSupplier;
import java.util.stream.*;
public class StreamBench {
static long sink;
static double measure(String name, int warmup, int runs, LongSupplier task) {
for (int i = 0; i < warmup; i++) sink += task.getAsLong();
double[] t = new double[runs];
for (int i = 0; i < runs; i++) {
long s = System.nanoTime();
sink += task.getAsLong();
t[i] = (System.nanoTime() - s) / 1e6;
}
Arrays.sort(t);
System.out.printf("%-30s median %9.4f ms (min %.4f, max %.4f)%n", name, t[runs / 2], t[0], t[runs - 1]);
return t[runs / 2];
}
public static void main(String[] args) {
System.out.println("cores=" + Runtime.getRuntime().availableProcessors()
+ " commonPool parallelism=" + java.util.concurrent.ForkJoinPool.commonPool().getParallelism());
Random rnd = new Random(7);
int[] big = new int[10_000_000];
for (int i = 0; i < big.length; i++) big[i] = rnd.nextInt(1000);
System.out.println("== A: sum of squares of even values, int[10,000,000]");
measure("for loop", 20, 11, () -> { long s = 0; for (int v : big) if ((v & 1) == 0) s += (long) v * v; return s; });
measure("IntStream", 20, 11, () -> Arrays.stream(big).filter(v -> (v & 1) == 0).mapToLong(v -> (long) v * v).sum());
measure("IntStream.parallel()", 20, 11, () -> Arrays.stream(big).parallel().filter(v -> (v & 1) == 0).mapToLong(v -> (long) v * v).sum());
System.out.println("== B: same work, List<Integer> of 1,000 elements, 1,000 calls per sample");
List<Integer> small = new ArrayList<>();
for (int i = 0; i < 1_000; i++) small.add(rnd.nextInt(1000));
measure("for loop", 20, 11, () -> { long t = 0; for (int r = 0; r < 1000; r++) { long s = 0; for (int v : small) if ((v & 1) == 0) s += (long) v * v; t += s; } return t; });
measure("stream()", 20, 11, () -> { long t = 0; for (int r = 0; r < 1000; r++) t += small.stream().filter(v -> (v & 1) == 0).mapToLong(v -> (long) v * v).sum(); return t; });
measure("parallelStream()", 20, 11, () -> { long t = 0; for (int r = 0; r < 1000; r++) t += small.parallelStream().filter(v -> (v & 1) == 0).mapToLong(v -> (long) v * v).sum(); return t; });
System.out.println("== C: LinkedList<Integer> of 1,000,000");
List<Integer> linked = new LinkedList<>();
for (int i = 0; i < 1_000_000; i++) linked.add(rnd.nextInt(1000));
measure("for loop", 20, 11, () -> { long s = 0; for (int v : linked) if ((v & 1) == 0) s += (long) v * v; return s; });
measure("stream()", 20, 11, () -> linked.stream().filter(v -> (v & 1) == 0).mapToLong(v -> (long) v * v).sum());
measure("parallelStream()", 20, 11, () -> linked.parallelStream().filter(v -> (v & 1) == 0).mapToLong(v -> (long) v * v).sum());
System.out.println("== D: Stream.iterate(...).limit(2,000,000)");
measure("for loop", 20, 11, () -> { long s = 0; for (int i = 0; i < 2_000_000; i++) s += (long) i * i; return s; });
measure("iterate+limit", 20, 11, () -> Stream.iterate(0, i -> i + 1).limit(2_000_000).mapToLong(i -> (long) i * i).sum());
measure("iterate+limit parallel", 20, 11, () -> Stream.iterate(0, i -> i + 1).limit(2_000_000).parallel().mapToLong(i -> (long) i * i).sum());
measure("LongStream.range parallel", 20, 11, () -> LongStream.range(0, 2_000_000).parallel().map(i -> i * i).sum());
System.out.println("sink=" + (sink != 0));
}
}
前回: 第 3 回 コレクションの性能 · 次回: 第 5 回 仮想スレッドと並行処理
よくある質問
parallelStream を使えば for 文より速くなりますか?
処理量が十分なときだけです。int[10,000,000] の偶数の二乗和は for ループ 27.2ms、IntStream.parallel() 9.0ms でした(8 コアの M1 Pro)。同じ処理を 1,000 要素のリストで 1,000 回行うと、for ループ 0.67ms に対し parallelStream は 6.57ms で約 10 倍遅くなりました。
Collectors.toMap で IllegalStateException: Duplicate key になるのはなぜですか?
引数 2 つの toMap はキーがすべて異なる前提です。同じキーが再び来ると 'Duplicate key ramen (attempted merging values 980 and 850)' のようにキーと 2 つの値を示して例外になります。第 3 引数に Integer::max などのマージ関数を渡すか、groupingBy を使います。
Stream.toList() と collect(Collectors.toList()) の違いは何ですか?
Stream.toList()(Java 16)は変更不可のリストで、add すると UnsupportedOperationException になります。collect(Collectors.toList()) は JDK 26 では java.util.ArrayList を返しましたが、Javadoc は型を保証していません。Stream.toList() は null を許し、Collectors.toUnmodifiableList() は許しません。