43

Using Java 8 stream what is the best way to map a List<Integer> when you have no output for the input Integer ?

Simply return null? But now my output list size will be smaller than my input size...

    List<Integer> input = Arrays.asList(0,1,2,3);
    List<Integer> output = input.stream()
                                .map(i -> { 
                                    Integer out = crazyFunction(i);
                                    if(out == null || out.equals(0))
                                        return null;
                                    return Optional.of(out);
                                    })
                                .collect(Collectors.toList());
Stuart Marks
  • 127,867
  • 37
  • 205
  • 259
Martin Magakian
  • 3,746
  • 5
  • 37
  • 53

4 Answers4

80

I don’t get why you (and all answers) make it so complicated. You have a mapping operation and a filtering operation. So the easiest way is to just apply these operation one after another. And unless your method already returns an Optional, there is no need to deal with Optional.

input.stream().map(i -> crazyFunction(i))
              .filter(out -> out!=null && !out.equals(0))
              .collect(Collectors.toList());

It may be simplified to

input.stream().map(context::crazyFunction)
              .filter(out -> out!=null && !out.equals(0))
              .collect(Collectors.toList());

But you seem to have a more theoretical question about what kind of List to generate, one with placeholders for absent values or one with a different size than the input list.

The simple answer is: don’t generate a list. A List is not an end in itself so you should consider for what kind of operation you need this list (or its contents) and apply the operation right as the terminal operation of the stream. Then you have your answer as the operation dictates whether absent values should be filtered out or represented by a special value (and what value that has to be).

It might be a different answer for different operations…

Holger
  • 285,553
  • 42
  • 434
  • 765
  • A small detail here: when your "map" return a null value, it isn't added to the list. So you get less element in input then input. And thanks/because of that no point to check if out!=null in you case. It can't be null. – Martin Magakian Jul 28 '14 at 09:52
  • 8
    @Martin Magakian: Are you sure? I don’t see any automatic filtering of `null` documented, neither on the `map` function nor the `toList` collector. And a quick test on my machine reveals the `map` function works 1:1 just as expected. You have to `filter` or `flatMap` explicitly if you want a differently sized result list. – Holger Jul 28 '14 at 10:14
  • you are actually true! Well my question don't make sense anymore... hahha – Martin Magakian Jul 28 '14 at 11:06
  • 2
    This is quite simple. Further, if you just want to filter null return values, then we could simplify a `.filter(out -> out != null)` to `.filter(Objects::nonNull)` as well. – Kasun Gajasinghe Dec 30 '19 at 09:14
46

Replace the map call with flatMap. The map operation produces one output value per input value, whereas the flatMap operation produces any number of output values per input value -- include zero.

The most straightforward way is probably to replace the check like so:

List<Integer> output = input.stream()
                            .flatMap(i -> { 
                                Integer out = crazyFunction(i);
                                if (out == null || out.equals(0))
                                    return Stream.empty();
                                else
                                    return Stream.of(out);
                                })
                            .collect(Collectors.toList());

A further refactoring could change crazyFunction to have it return an Optional (probably OptionalInt). If you call it from map, the result is a Stream<OptionalInt>. Then you need to flatMap that stream to remove the empty optionals:

List<Integer> output = input.stream()
    .map(this::crazyFunctionReturningOptionalInt)
    .flatMap(o -> o.isPresent() ? Stream.of(o.getAsInt()) : Stream.empty())
    .collect(toList());

The result of the flatMap is a Stream<Integer> which boxes up the ints, but this is OK since you're going to send them into a List. If you weren't going to box the int values into a List, you could convert the Stream<OptionalInt> to an IntStream using the following:

flatMapToInt(o -> o.isPresent() ? IntStream.of(o.getAsInt()) : IntStream.empty())

For further discussion of dealing with streams of optionals, see this question and its answers.

Community
  • 1
  • 1
Stuart Marks
  • 127,867
  • 37
  • 205
  • 259
  • Is there an advantage to using flatMap here rather than simply using `.filter(o -> o.isPresent())` to filter out the nulls? – Eran Jul 25 '14 at 11:11
  • 2
    Not creating the null item representations in the first place is clearly an advantage in and of itself. – Jake H Jul 25 '14 at 18:11
  • 4
    @Eran Mainly it's a matter of style, I think. If you have a mapping operation that sometimes doesn't want to return a result, using `flatMap` to return zero-or-one values might make sense. Returning `Optional` values to the stream followed by `filter(Optional::isPresent).map(Optional::get)` works and is simple and concise but might not align well with a might-return-no-value mapper. But it's certainly a value technique. – Stuart Marks Jul 25 '14 at 21:48
  • @Holgers answer works but this is faster as the .filter() must have to parse over the values in the stream again. – Stuart Clark May 25 '16 at 10:11
  • 2
    @StuartClark: if you want to know, which one is faster, measure, don’t guess, especially don’t guess based on fundamental misunderstanding of how streams work. There is no such thing as “parse over the values in the stream again”. – Holger May 24 '17 at 10:57
  • 2
    @Holger,@StuarClark: I measured `map(...).filter(out -> out!=null && !out.equals(0))` and `flatMap(...)`. The average results for different Listsizes(where the first part of the ratio is the measured time of the filter-method and the second part of the flatMap-method, all approximately) -> 10.000 List-Elements = 120ms:10ms, 100.000 = 130ms:60ms, 1.000.000= 160ms:100ms. Big surprise at 10.000.000=500ms:5sec. So a gut-feeling is, that Holgers's approach performs at really huge lists like 10.000.000 elements. – JackLeEmmerdeur May 30 '17 at 12:43
3

Simpler variants of @Martin Magakian 's answer:

List<Integer> input = Arrays.asList(0,1,2,3);
List<Optional<Integer>> output =
  input.stream()
    .map(i -> crazyFunction(i)) // you can also use a method reference here
    .map(Optional::ofNullable) // returns empty optional
                               // if original value is null
    .map(optional -> optional.filter(out -> !out.equals(0))) // return empty optional
                                                           // if captured value is zero
    .collect(Collectors.toList())
;

List<Integer> outputClean =
  output.stream()
    .filter(Optional::isPresent)
    .map(Optional::get)
    .collect(Collectors.toList())
;
srborlongan
  • 4,460
  • 4
  • 26
  • 33
2

You can wrap the output into an Optional which may or may not contain a non-null value.
With an output: return Optional.of(out);
Without output: return Optional.<Integer>empty();

You have to wrap into an option because an array cannot contain any null value.

    List<Integer> input = Arrays.asList(0,1,2,3);
    List<Option<Integer>> output = input.stream()
                                .map(i -> { 
                                    Integer out = crazyFunction(i);
                                    if(out == null || out.equals(0))
                                        return Optional.<Integer>empty();
                                    return Optional.of(out);
                                    })
                                .collect(Collectors.toList());

This will make sure input.size() == output.size().

Later on you can exclude the empty Optional using:

    List<Integer> outputClean = output.stream()
                                   .filter(Optional::isPresent)
                                   .map(i -> {
                                           return i.get();
                                        })
                                   .collect(Collectors.toList());
Martin Magakian
  • 3,746
  • 5
  • 37
  • 53