24

I have the following code and would like to implement it using lambda functions just for fun. Can it be done using the basic aggregate operations?

List<Integer> result = new ArrayList<>();

for (int i = 1; i <= 10; i++) {
    if (10 % i == 0) {
        result.add(i);
        if (i != 5) {
            result.add(10 / i);
        }
    }
}

Using lambda:

List<Integer> result = IntStream.rangeClosed(1, 10)
                                .boxed()
                                .filter(i -> 10 % i == 0)
                                // a map or forEach function here?
                                // .map(return 10 / i -> if i != 5)
                                .collect(Collectors.toList());
VNT
  • 934
  • 2
  • 8
  • 19
moon
  • 1,702
  • 3
  • 19
  • 35

5 Answers5

53

The essential observation here is that your problem involves a non-isomorphic transformation: a single input element may map to zero, one, or two output elements. Whenever you notice this, you should immediately start looking for a solution which involves flatMap instead of map because that's the only way to achieve such a general transformation. In your particular case you can first apply filter for a one-to-zero element mapping, then flatMap for one-to-two mapping:

List<Integer> result =
    IntStream.rangeClosed(1, 10)
             .filter(i -> 10 % i == 0)
             .flatMap(i -> i == 5 ? IntStream.of(i) : IntStream.of(i, 10 / i))
             .boxed()
             .collect(toList());

(assuming import static java.util.stream.Collectors.toList)

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • 4
    I like how you not only answer the question but you also teach how to think about the problem to get to the solution. – marcus Nov 19 '16 at 02:59
5

You could declare a body for a lambda. For example:

Runnable run = () -> System.out.println("Hey");

Could be

Runnable run = () -> {
    System.out.println("Hey");
};

Within that body, you can create nested statements:

Runnable run = () -> {
    int num = 5;

    if(num == 5) {
        System.out.println("Hey");
    }
};
Vince
  • 14,470
  • 7
  • 39
  • 84
  • but is it possible to do it with `map`, `filter`, etc? I am trying to learn the basics of lambda functions. Thanks. – moon Jul 25 '15 at 17:51
  • @Z-1 I'm not sure if filters are able to do this, but the syntax would be `.filter( i -> { return yourLogic; } )` – randers Jul 25 '15 at 18:09
  • @Z-1 A lambda expression arises from the use of a *functional interface* (an interface with only 1 `abstract` method, such as `Runnable`; may have multiple `default` methods). For example, a `Thread` accepts a `Runnable` in it's constructor. We could write `new Thread(() -> { });`. You can even create your own functional interfaces. So to answer "*possible to do it with `map` and `filter`*": **yes**. It works for *all* lambdas. – Vince Jul 25 '15 at 18:18
3

Use flatMap as you are trying to add elements into the pipeline or a 1-to-many mapping. Map is a one to one mapping.

ArrayList<Integer> result = (ArrayList<Integer>) IntStream.rangeClosed(1, 10)
                .boxed()
                .filter(i -> 10 % i == 0)
                .flatMap((Integer i) -> {return i!=5 ? Stream.of(i, (10/i)):Stream.of(i);})
                .collect(Collectors.toList());

This results in the same list as

ArrayList<Integer> result2 = new ArrayList<Integer>();

        for (int i = 1; i <= 10; i++) {
            if (10 % i == 0) {
                result2.add(i);
                if (i != 5) {
                    result2.add(10 / i);
                }
            }
        }

In case your wondering which way is faster the loop method is ~3 times faster than using streams.

Benchmark                     Mode  Cnt      Score     Error  Units
testStreams.Bench.loops       avgt    5     75.221 ±   0.576  ns/op
testStreams.Bench.streams     avgt    5    257.713 ±  13.125  ns/op
Mantis
  • 112
  • 1
  • 8
  • 1
    This is interesting. Thanks for the benchmark. I noticed that too, Streams is much slower than traditional for loop. – moon Jul 25 '15 at 19:12
  • Depends on the application really, operations on ints are some of the simplest operartion you can do, for what you were doing here the overhead in setting up the stream is too high. I found the second answer on [this post](http://stackoverflow.com/questions/27925954/is-arrays-streamarray-name-sum-slower-than-iterative-approach/27994074#27994074) helpful. – Mantis Jul 25 '15 at 19:19
  • the stream overhead does indeed get smaller as the loop range grows larger. doesn't quite vanish in this case though. – the8472 Jul 26 '15 at 00:05
1

You can do this:

List<Integer> result1 = IntStream
    .rangeClosed(1, 10)
    .boxed()
    .filter(i -> 10 % i == 0)
    .map(i -> (i != 5 ? Stream.of(i, 10 / i) : Stream.of(i)))
    .flatMap(Function.identity())
    .collect(Collectors.toList());
Neuron
  • 5,141
  • 5
  • 38
  • 59
Mrinal
  • 1,846
  • 14
  • 17
0

Try using flatMap:

List<Integer> result = IntStream.rangeClosed(1, 10)
        .boxed()
        .flatMap((i) -> {
            List<Integer> results = new ArrayList<>();
            if (10 % i == 0) {
                results.add(i);
                if (i != 5) {
                    results.add(10 / i);
                }
            }
            return results.stream();
        })
        .collect(Collectors.toList());

See http://ideone.com/EOBiEP

Neuron
  • 5,141
  • 5
  • 38
  • 59
Danil Gaponov
  • 1,413
  • 13
  • 23