Files
javademos/gatherers/src/GathererBasics.java
T
Claude 6b5a918278 Add two standalone modules: Stream Gatherers (JEP 485) and Scoped Values vs ThreadLocal (JEP 506)
gatherers/: windowFixed, windowSliding, fold, scan, mapConcurrent, a custom
Gatherer (dedupeConsecutive, takeUntil), and a Collector-vs-Gatherer comparison.
Verified: windowSliding emits one short window on a too-short stream rather than
shrinking to empty like windowFixed; mapConcurrent waits for in-flight siblings
to finish (~901ms) rather than cancelling them when one mapper throws.

scoped-values/: ScopedValue API and rebinding, why plain threads (virtual or
platform) do not inherit a binding while a StructuredTaskScope subtask does,
and the memory cost vs InheritableThreadLocal measured two ways -- a noisy
heap-delta first pass, then a precise jcmd GC.class_histogram object census
(5 shared Carrier objects vs ~700,000 copied ThreadLocalMap/Entry objects for
100,000 threads).

Companion code for the ankurm.com posts "Java Stream Gatherers (JEP 485)" and
"Scoped Values vs ThreadLocal in Java 25: Migration Guide with Virtual Threads".

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FtpJvZfg4nvLvtzgJTDWpB
2026-09-24 04:27:12 +00:00

105 lines
5.6 KiB
Java

import java.util.List;
import java.util.stream.Gatherers;
import java.util.stream.Stream;
/**
* The five built-in gatherers (JEP 485, final in Java 24, no preview flag on 25+):
* windowFixed, windowSliding, fold, scan, mapConcurrent. This file covers the first four --
* mapConcurrent gets its own file because it needs real (slow) work to say anything interesting.
* Chapter: docs/16-gatherers.md
*/
public class GathererBasics {
public static void main(String[] args) {
windowFixed();
windowSliding();
fold();
scan();
foldIsNotScan();
}
static void windowFixed() {
// Chunks the stream into lists of exactly `n`, except possibly the last one, which is
// shorter if the stream doesn't divide evenly. No overlap between windows.
List<List<Integer>> windows = Stream.of(1, 2, 3, 4, 5, 6, 7)
.gather(Gatherers.windowFixed(3))
.toList();
System.out.println("windowFixed(3) of 1..7 = " + windows);
Check.that(windows.equals(List.of(List.of(1, 2, 3), List.of(4, 5, 6), List.of(7))),
"windowFixed(3) groups into [1,2,3][4,5,6][7], last window is the short remainder");
try {
Stream.of(1).gather(Gatherers.windowFixed(0));
Check.that(false, "windowFixed(0) should reject a non-positive window size");
} catch (IllegalArgumentException expected) {
Check.that(true, "windowFixed(0) throws IllegalArgumentException: " + expected.getMessage());
}
}
static void windowSliding() {
// A window that advances by 1 each time, so consecutive windows overlap. Stops producing
// windows once fewer than `n` elements remain -- it never emits a short window.
List<List<Integer>> windows = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowSliding(3))
.toList();
System.out.println("windowSliding(3) of 1..5 = " + windows);
Check.that(windows.equals(List.of(List.of(1, 2, 3), List.of(2, 3, 4), List.of(3, 4, 5))),
"windowSliding(3) of 5 elements produces exactly 3 overlapping windows, each length 3");
// Easy to assume this behaves like windowFixed's leftover-shrinks-to-fit rule, or like
// Collectors.windowed variants elsewhere that drop a too-short remainder -- neither is
// right. windowSliding still emits ONE window holding whatever is there, as long as the
// stream isn't empty.
List<List<Integer>> tooFew = Stream.of(1, 2).gather(Gatherers.windowSliding(3)).toList();
Check.that(tooFew.equals(List.of(List.of(1, 2))), "windowSliding(3) on a 2-element stream still emits one short window: [[1, 2]]");
List<List<Integer>> empty = Stream.<Integer>of().gather(Gatherers.windowSliding(3)).toList();
Check.that(empty.isEmpty(), "windowSliding(3) on an empty stream emits nothing");
}
static void fold() {
// fold(supplier, BiFunction) is a sequential-only, non-associative reduction: the result
// type R doesn't have to match the element type T, and there's no combiner because fold
// never runs in parallel. That's exactly what lets it do things Stream.reduce can't --
// like building a String left-to-right from a Stream<Integer>.
String joined = Stream.of(1, 2, 3, 4)
.gather(Gatherers.fold(() -> "", (acc, n) -> acc + "[" + n + "]"))
.findFirst()
.orElseThrow();
System.out.println("fold building a String: " + joined);
Check.that(joined.equals("[1][2][3][4]"), "fold(\"\", acc+\"[n]\") over 1..4 builds \"[1][2][3][4]\" left to right");
// fold, like Collectors.reducing, emits exactly one final value -- it's an intermediate
// op that happens to produce a stream of size 1, not a running total.
long count = Stream.of(1, 2, 3).gather(Gatherers.fold(() -> 0, Integer::sum)).count();
Check.that(count == 1, "fold's output stream has exactly one element, the final accumulation");
}
static void scan() {
// scan(supplier, BiFunction) uses the identical signature to fold but emits every
// intermediate accumulation, not just the last one -- a running total, a prefix-sum. This
// is something no Collector can express: a Collector always reduces to one final value,
// never a sequence of intermediate ones.
List<Integer> runningTotals = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.scan(() -> 0, Integer::sum))
.toList();
System.out.println("scan running total of 1..5 = " + runningTotals);
Check.that(runningTotals.equals(List.of(1, 3, 6, 10, 15)), "scan(0, +) over 1..5 emits every prefix sum: [1,3,6,10,15]");
}
static void foldIsNotScan() {
// Same supplier and BiFunction, different gatherer -- proof the two aren't the same
// operation wearing different names.
var supplier = (java.util.function.Supplier<Integer>) () -> 0;
var adder = (java.util.function.BiFunction<Integer, Integer, Integer>) Integer::sum;
List<Integer> foldResult = Stream.of(1, 2, 3).gather(Gatherers.fold(supplier, adder)).toList();
List<Integer> scanResult = Stream.of(1, 2, 3).gather(Gatherers.scan(supplier, adder)).toList();
System.out.println("fold(0,+) over 1..3 = " + foldResult);
System.out.println("scan(0,+) over 1..3 = " + scanResult);
Check.that(foldResult.equals(List.of(6)), "fold(0,+) over 1..3 emits just the final sum [6]");
Check.that(scanResult.equals(List.of(1, 3, 6)), "scan(0,+) over 1..3 emits every partial sum [1,3,6]");
}
}