Search code examples
javajava-stream

"Interim condition check" in Java Stream


I am searching for "interim condition check" in the Java stream chain.

My inspiration is in Reactor (Spring Webflux) where I can write this:

public Mono<String> interimConditionCheckMono(Flux<String> input) {
    return input
            .filter(s -> s.startsWith("A"))
            .switchIfEmpty(Mono.error(() -> new IllegalArgumentException("No string starts with 'A'")))
            .filter(s -> s.length() > 10)
            .switchIfEmpty(Mono.error(() -> new IllegalArgumentException("No string is long enough")))
            .next();
}

And I am wondering if an analogical construct is possible in the Java stream chain, something like this:

public String interimConditionCheckStream(Collection<String> input) {
    return input.stream()
            .filter(s -> s.startsWith("A"))
            // Here I want to trow an exception if no string starts with 'A'
            .filter(s -> s.length() > 10)
            .findAny()
            .orElseThrow(() -> new IllegalArgumentException("No string is long enough"));
}

Solution

  • As said in the comments, the upcoming Gatherer API offers a potential solution:

    static <T> Gatherer<T,?,T> throwWhenEmpty(
        Supplier<? extends RuntimeException> exceptionFactory) {
    
        final class State {
            boolean seenAny;
        }
        return Gatherer.of(
            State::new,
            (state, element, down) -> {
                state.seenAny = true;
                return down.push(element);
            },
            (a, b) -> a.seenAny? a: b,
            (state, down) -> {
                if(!state.seenAny) throw exceptionFactory.get();
            });
    }
    

    This provides a reusable gatherer for similar situations. It can be used for your example like

    public String interimConditionCheckStream(Collection<String> input) {
        return input.stream()
            .filter(s -> s.startsWith("A"))
            .gather(throwWhenEmpty(
                () -> new IllegalArgumentException("No string starts with A")))
            .filter(s -> s.length() > 10)
            .findAny()
            .orElseThrow(
                () -> new IllegalArgumentException("No string is long enough"));
    }
    

    or

    public String interimConditionCheckStream(Collection<String> input) {
        return input.stream()
            .filter(s -> s.startsWith("A"))
            .gather(throwWhenEmpty(
                () -> new IllegalArgumentException("No string starts with A")))
            .filter(s -> s.length() > 10)
            .gather(throwWhenEmpty(
                () -> new IllegalArgumentException("No string is long enough")))
            .findAny()
            .orElseThrow(AssertionError::new); // should never be empty
    }
    

    This works with JDK 22, with preview features enabled.

    But keep in mind that if no element passes a filter, all subsequent stages become no-ops anyway. There is no need to throw immediately at this point.

    Your specific example could also be handled with earlier Java versions using

    public String interimConditionCheckStream(Collection<String> input) {
        return input.stream()
            .filter(s -> s.startsWith("A"))
            .filter(s -> s.length() > 10)
            .findAny()
            .orElseThrow(() -> new IllegalArgumentException(
                  input.stream().anyMatch(s -> s.startsWith("A"))?
                  "No string is long enough":
                  "No string starts with A"));
    }
    

    or even

    public String interimConditionCheckStream(Collection<String> input) {
        return input.stream()
            .filter(s -> s.startsWith("A") && s.length() > 10)
            .findAny()
            .orElseThrow(() -> new IllegalArgumentException(
                  input.stream().anyMatch(s -> s.startsWith("A"))?
                  "No string is long enough":
                  "No string starts with A"));
    }
    

    This performs another search to decide for the right exception message, but this price is only paid in the exceptional case. And we normally do not optimize for the exceptional case…