Home > Software engineering >  filtering a stream changes its wildcard bounds?
filtering a stream changes its wildcard bounds?

Time:12-21

the below method compiles without problems:

static Stream<Optional<? extends Number>> getNumbers(Stream<Number> numbers) {
    return numbers.map(Optional::of);
}

yet if I add a simple filtering to it like this:

static Stream<Optional<? extends Number>> getNumbers2(Stream<Number> numbers) {
    return numbers.map(Optional::of).filter(number -> true);
}

it generates the following error:

incompatible types: java.util.stream.Stream<java.util.Optional<java.lang.Number>> cannot be converted to java.util.stream.Stream<java.util.Optional<? extends java.lang.Number>>

tested on openJdk-11 and openJdk-17.

I'd expect them both to do the same (either both compile ok or both generate the same compilation error), so I'm really puzzled by this: what is the general rule here that explains why the 1st method compiles ok yet the 2nd does not? Thanks!

CodePudding user response:

The important thing you need to realize here is that compatibility with the return type Stream<Optional<? extends Number>> in the first case is not obtained by virtue of numbers.map(Optional::of) returning a Stream<Optional<? extends Number>> on its own; it's the compiler inferring the return type of numbers.map(...).

With that said, Stream.filter() and Stream.map() cannot be expected to work in the same way with respect to return type inference.

Stream.map() is generic, and the type variable is the return type's type argument:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Stream.filter() is not:

Stream<T> filter(Predicate<? super T> predicate);

In short, what is happening is that the compiler is calculating the actual return type of numbers.map(Optional::of) by taking into account the return statement's context.

As a different example to illustrate that, please figure out why both of these compile (List.of() is the same code, after all):

static List<String> stringList() {
    return List.of();
}
static List<Integer> intList() {
    return List.of();
}

Now, why does this fail:

static List<String> stringList() {
    return List.of().subList(0, 0);
}

That's because List.subList(...) does not infer the returned list's E type in context (i.e., the method is not generic), it carries the List instance's E type, which, with List.of() in that case gets defaulted to Object (yes, when you have return List.of();, return type inference kicks in, forcing the compiler to figure out that the intent is to make E match String, the type argument in the method's return type). Please note that this gets more complex than that, there are corners where inference doesn't work as wished/expected.


Short answer: return numbers.map(Optional::of) takes advantage of type inference as map() is generic, and filter() does not, expecting the E of Stream<E> to be carried. And with numbers.map(Optional::of), E is Optional<Number>, not Optional<? extends Number>, and filter carries that.

  • Related