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
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Gatherer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Writing a Gatherer from scratch. Gatherer<T, A, R> is four composable pieces:
|
||||
* initializer (private mutable state A), integrator (per-element: read/write state A, push 0+ R
|
||||
* downstream, return whether to keep going), combiner (merge two states for parallel streams --
|
||||
* omit it and the gatherer just runs sequentially), finisher (flush anything left when the
|
||||
* upstream is exhausted). Two custom gatherers here: dedupeConsecutive (stateful, one-in/0-or-1
|
||||
* out) and takeUntil (short-circuiting -- something no Collector can do, because a Collector
|
||||
* never gets a say in whether the source stops producing).
|
||||
* Chapter: docs/16-gatherers.md
|
||||
*/
|
||||
public class CustomGatherer {
|
||||
|
||||
public static void main(String[] args) {
|
||||
dedupeConsecutiveDemo();
|
||||
takeUntilStopsTheUpstream();
|
||||
}
|
||||
|
||||
/** Collapses consecutive equal elements, like Unix `uniq` -- keeps non-adjacent duplicates. */
|
||||
static <T> Gatherer<T, ?, T> dedupeConsecutive() {
|
||||
return Gatherer.ofSequential(
|
||||
// initializer: a single-slot box holding "last element pushed downstream", or
|
||||
// null meaning "nothing pushed yet". A List<Object> of size 1 doubles as a
|
||||
// nullable box without a wrapper class.
|
||||
() -> new ArrayList<Object>(List.of()),
|
||||
// integrator: push only when this element differs from the last one we pushed.
|
||||
(state, element, downstream) -> {
|
||||
boolean isFirst = state.isEmpty();
|
||||
boolean changed = isFirst || !state.get(0).equals(element);
|
||||
if (changed) {
|
||||
if (isFirst) {
|
||||
state.add(element);
|
||||
} else {
|
||||
state.set(0, element);
|
||||
}
|
||||
return downstream.push(element);
|
||||
}
|
||||
return true; // swallowed a duplicate; keep pulling from upstream
|
||||
});
|
||||
}
|
||||
|
||||
static void dedupeConsecutiveDemo() {
|
||||
List<String> deduped = Stream.of("a", "a", "b", "b", "b", "a", "c", "c")
|
||||
.gather(CustomGatherer.<String>dedupeConsecutive())
|
||||
.toList();
|
||||
System.out.println("dedupeConsecutive of a,a,b,b,b,a,c,c = " + deduped);
|
||||
Check.that(deduped.equals(List.of("a", "b", "a", "c")),
|
||||
"consecutive runs collapse to [a, b, a, c]; the second 'a' survives because it isn't adjacent to the first");
|
||||
}
|
||||
|
||||
/**
|
||||
* takeWhile(p) stops BEFORE the first failing element. takeUntil(p) stops AFTER it --
|
||||
* includes the element that fails the predicate, then no more. Not expressible with
|
||||
* takeWhile(p.negate()) plus one extra element without re-running the predicate; building it
|
||||
* as a Gatherer means integrator itself decides when to stop pulling from upstream.
|
||||
*/
|
||||
static <T> Gatherer<T, ?, T> takeUntil(Predicate<? super T> stopAfter) {
|
||||
return Gatherer.ofSequential((state, element, downstream) -> {
|
||||
downstream.push(element);
|
||||
return !stopAfter.test(element); // false = stop; upstream is not pulled again
|
||||
});
|
||||
}
|
||||
|
||||
static void takeUntilStopsTheUpstream() {
|
||||
List<Integer> upstreamSeen = new ArrayList<>();
|
||||
List<Integer> result = Stream.of(1, 2, 3, 4, 5, 6)
|
||||
.peek(upstreamSeen::add)
|
||||
.gather(takeUntil((Integer n) -> n == 3))
|
||||
.toList();
|
||||
|
||||
System.out.println("takeUntil(n == 3) result = " + result + ", upstream elements actually pulled = " + upstreamSeen);
|
||||
Check.that(result.equals(List.of(1, 2, 3)), "takeUntil includes the element that satisfies the stop condition: [1, 2, 3]");
|
||||
Check.that(upstreamSeen.equals(List.of(1, 2, 3)),
|
||||
"the upstream peek() only saw 1, 2, 3 -- elements 4, 5, 6 were never pulled, this genuinely short-circuits");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user