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
105 lines
5.6 KiB
Java
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]");
|
|
}
|
|
}
|