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> 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> 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> 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> empty = Stream.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. 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 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) () -> 0; var adder = (java.util.function.BiFunction) Integer::sum; List foldResult = Stream.of(1, 2, 3).gather(Gatherers.fold(supplier, adder)).toList(); List 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]"); } }