import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Gatherers; import java.util.stream.Stream; /** * When each is the right tool. A Collector always reduces a stream to exactly one final value, * at the very end of the pipeline -- it cannot appear in the middle, and it cannot stop the * source early. A Gatherer is an intermediate operation: it produces a new Stream, can emit zero, * one, or many elements per input, and (as CustomGatherer.takeUntil showed) can stop pulling from * upstream before the source is exhausted. Reach for a Collector when the answer is genuinely one * value; reach for a Gatherer when you need to keep streaming afterward, or the transform itself * needs private state, ordering, or the ability to bail out early. * Chapter: docs/16-gatherers.md */ public class CollectorVsGatherer { public static void main(String[] args) { collectorIsRightForATerminalAggregate(); gathererIsRightForAnIntermediateStatefulTransform(); aGathererResultCanKeepBeingAStream(); } static void collectorIsRightForATerminalAggregate() { // groupingBy needs to see the whole stream and produce ONE Map. There's no intermediate // stage here to preserve -- a Gatherer would just add ceremony around the same result. record Order(String customer, double amount) {} List orders = List.of( new Order("amy", 12.50), new Order("bo", 4.00), new Order("amy", 7.25), new Order("bo", 19.99)); Map totals = orders.stream() .collect(Collectors.groupingBy(Order::customer, Collectors.summingDouble(Order::amount))); System.out.println("Collectors.groupingBy totals = " + totals); Check.that(totals.get("amy") == 19.75, "amy's total via groupingBy/summingDouble is 19.75"); Check.that(totals.get("bo") == 23.99, "bo's total via groupingBy/summingDouble is 23.99"); } static void gathererIsRightForAnIntermediateStatefulTransform() { // A running total that still needs filtering and mapping done to it afterward. Doing this // with a Collector would mean collecting to a List of running totals first, then // starting a SECOND stream over that list -- two passes, and the "it's still one // pipeline" property is gone. gather() keeps it as one lazy pipeline throughout. List overThreshold = Stream.of(5, 10, 15, 20, 25) .gather(Gatherers.scan(() -> 0, Integer::sum)) // running totals: 5,15,30,50,75 .filter(total -> total > 20) .map(total -> "total=" + total) .toList(); System.out.println("scan -> filter -> map, one pipeline = " + overThreshold); Check.that(overThreshold.equals(List.of("total=30", "total=50", "total=75")), "scan feeding straight into filter/map in the same pipeline gives [total=30, total=50, total=75]"); } static void aGathererResultCanKeepBeingAStream() { // The defining shape difference, made concrete: a Collector's result type is whatever the // Collector was parameterized with (here, a single Long). A Gatherer's result is always a // Stream you can keep chaining -- gather() returns Stream, not R. long distinctWindowCount = Stream.of(1, 2, 3, 4, 5, 6, 7) .gather(Gatherers.windowFixed(2)) .collect(Collectors.counting()); System.out.println("windowFixed(2) over 7 elements, then collect(counting()) = " + distinctWindowCount); Check.that(distinctWindowCount == 4, "collect(counting()) after gather() proves the gather result is still a Stream you can collect further: 4 windows"); } }