javajava-streamjava-17java-16

Why does mapMulti need type information in comparison to flatMap


I want to use mapMulti instead of flatMap and refactored the following code:

// using flatMap (version 1) => returns Set<Item>
var items = users.stream()
                 .flatMap(u -> u.getItems().stream())
                .collect(Collectors.toSet());

into this (version 2):

// using mapMulti (version 2) => returns Set<Item>
var items = users.stream()
                 .<Item>mapMulti((u, consumer) -> u.getItems().forEach(consumer))
                 .collect(Collectors.toSet());

Both return the same elements. However, I am in doubt if I should really replace all my flatMap with that more verbose code of mapMulti. Why do I need to add the type information before mapMuli (.<Item>mapMulti). If I don't inlcude the type information, it will return a Set<Object>. (How) can I simplify mapMulti?


Solution

  • Notice that the kind of type inference required to deduce the resulting stream type when you use flatMap, is very different from that when you use mapMulti.

    When you use flatMap, the type of the resulting stream is the same type as the return type of the lambda body. That's a special thing that the compiler has been designed to infer type variables from (i.e. the compiler "knows about" it).

    However, in the case of mapMulti, the type of the resulting stream that you presumably want can only be inferred from the things you do to the consumer lambda parameter. Hypothetically, the compiler could be designed so that, for example, if you have said consumer.accept(1), then it would look at what you have passed to accept, and see that you want a Stream<Integer>, and in the case of getItems().forEach(consumer), the only place where the type Item could have come from is the return type of getItems, so it would need to go look at that instead.

    You are basically asking the compiler to infer the parameter types of a lambda, based on the types of arbitrary expressions inside it. The compiler simply has not been designed to do this.

    Other than adding the <Item> prefix, there are other (longer) ways to let it infer a Stream<Item> as the return type of mapMulti:

    Make the lambda explicitly typed:

    var items = users.stream()
                 .mapMulti((User u, Consumer<Item> consumer) -> u.getItems().forEach(consumer))
                 .collect(Collectors.toSet());
    

    Add a temporary stream variable:

    // By looking at the type of itemStream, the compiler can figure out that mapMulti should return a Stream<Item>
    Stream<Item> itemStream = users.stream()
                 .mapMulti((u, consumer) -> u.getItems().forEach(consumer));
    var items = itemStream.collect(Collectors.toSet());
    

    I don't know if this is more "simplified", but I think it is neater if you use method references:

    var items = users.stream()
                 .map(User::getItems)
                 .<Item>mapMulti(Iterable::forEach)
                 .collect(Collectors.toSet());