With the introduction of Stream Gatherers (JEP 461/JEP 473/JEP 485) in modern Java, developers have gained a powerful mechanism to customize intermediate stream operations. However, when chaining gatherers directly into downstream collectors like Collectors.joining(), you might run into confusing compilation errors where the Java compiler fails to infer the stream's element types.

The Problem Explained

Consider the following snippet that compiles and executes without any issues:

List<String> letters = Arrays.asList("a", "b", "c", "d");

letters.stream()
       .gather(Gatherer.ofSequential((_, t, r) -> r.push(t.toUpperCase())))
       .forEach(System.out::println);

However, when replacing .forEach(...) with .collect(Collectors.joining()), compilation fails with an error:

String s = letters.stream()
                  .gather(Gatherer.ofSequential((_, t, r) -> r.push(t.toUpperCase())))
                  .collect(Collectors.joining()); // Error: cannot infer type-variable(s) T, R

Why does this happen?

Target Typing and Poly Expressions

In Java, lambda expressions and generic method calls like Gatherer.ofSequential(...) and Collectors.joining() are poly expressions. Their types are inferred from their context (the target type).

  • In the first example, .forEach(System.out::println) has a simple, concrete consumer type, allowing the compiler to perform inference sequentially from left to right.
  • In the second example, Collectors.joining() expects a stream of CharSequence (or String). The compiler attempts to solve the constraints bidirectionally across .stream(), Gatherer.ofSequential(...), and .collect(Collectors.joining()) simultaneously. Because Gatherer.ofSequential relies heavily on nested generic parameters for state, input element T, and output element R, the compiler's constraint graph gets overloaded and type inference fails.

Solutions to Fix Type Inference

1. Provide Explicit Types in the Gatherer Lambda

The simplest way to resolve ambiguity is to explicitly type the parameters inside the lambda expression passed to Gatherer.ofSequential:

List<String> letters = Arrays.asList("a", "b", "c", "d");

String s = letters.stream()
                  .gather(Gatherer.ofSequential((Void _, String t, Gatherer.Downstream<? super String> r) -> {
                      return r.push(t.toUpperCase());
                  }))
                  .collect(Collectors.joining());

2. Add Explicit Type Arguments to Gatherer.<T, R>ofSequential

Alternatively, you can provide explicit type arguments to the generic factory method, eliminating the need to type every lambda parameter:

List<String> letters = Arrays.asList("a", "b", "c", "d");

String s = letters.stream()
                  .<String>gather(Gatherer.<String, String>ofSequential((_, t, r) -> r.push(t.toUpperCase())))
                  .collect(Collectors.joining());

3. Break the Chain into a Local Variable

Assigning intermediate stages or the gatherer itself to a strongly-typed local variable gives the compiler concrete target types at each stage:

Gatherer<String, ?, String> toUpperGatherer = Gatherer.ofSequential((_, t, r) -> r.push(t.toUpperCase()));

String s = letters.stream()
                  .gather(toUpperGatherer)
                  .collect(Collectors.joining());

Summary

When working with preview or newly standardized features like Java Stream Gatherers, complex generic inference can occasionally break down when chaining poly expressions. Providing explicit type hints—either on the lambda parameters, the gatherer factory method, or through local variables—is the most reliable way to keep your pipelines clean and compilable.