Fergal Fitz
Fergal Fitz

Reputation: 226

How to flatmap multiple elements with Java 8 streams?

I have a class

public class Step
      {
         boolean isActive;
         String name;
      }

I have a Collection of type Steps. Without using streams this is what I have currently

  StringBuilder stringBuilder = new StringBuilder();
  for (Step step : steps)
  {
     List<String> nextStepNames = getNextStepNames(step);
     List<String> conditions = getConditions(step);
     for (int i = 0; i < nextStepNames.size(); i++)
     {
        stringBuilder.append("If ").append(step.getName()).append("is active, and condition (").append(conditions.get(i)).append(") is true, then move to ").append(nextStepNames.get(i)).append("\n");
     }
  }

If my step collection contains stepA, StepB and stepC, then this is my output:

If stepA is active, and condition (c1A) is true, then move to step1A
If stepA is active, and condition (c2A) is true, then move to step2A
If stepA is active, and condition (c3A) is true, then move to step3A
If stepB is active, and condition (c1B) is true, then move to step1B
If stepB is active, and condition (c2B) is true, then move to step2B
If stepB is active, and condition (c3B) is true, then move to step3B
If stepC is active, and condition (c1C) is true, then move to step1C
If stepC is active, and condition (c2C) is true, then move to step2C
If stepC is active, and condition (c3C) is true, then move to step3C

The nextStepNames and conditions list are the same size and the indexes in the lists correspond to each other.

I haven't been able to convert this code into streams. I not sure if its possible.

Upvotes: 1

Views: 2696

Answers (3)

aventurin
aventurin

Reputation: 2203

Java lacks the abilities to solve the problem efficiently in an elegant pure functional style.

However, you could try something like

    str = steps.stream()
        .map(step ->
            IntStream
                .range(0, getNextStepNames(step).size())
                .mapToObj(i -> Stream.of(
                    "If ",
                    step.getName(),
                    " is active, and condition (",
                    getConditions(step).get(i),
                    ") is true, then move to ",
                    getNextStepNames(step).get(i),
                    "\n"))
                .flatMap(Function.identity())
        )
        .flatMap(Function.identity())
        .collect(Collectors.joining());

This quite inefficient due to the repeated evaluation of getNextStepNames and getConditions and the inability to allocate the complete output buffer in advance.

Of course you could try to mitigate this by using third party libraries, but imho it's not worth the effort.

Your solution is more efficient and much easier to understand and maintain. You cold further improve this by initializing the StringBuilder with a size that is equal or a little bit greater than the final output size.

Upvotes: 1

MC Emperor
MC Emperor

Reputation: 23057

Try this:

String output = Arrays.stream(steps) // if it's an array or steps.stream() for a list
    .flatMap(step -> IntStream.range(0, getNextStepNames(step).size())
        .mapToObj(i -> String.format(
            "If %s is active, and condition (%s) is true, then move to %s",
            step.getName(),
            getConditions(step).get(i),
            getNextStepNames(step).get(i))))
    .collect(Collectors.joining("\n"));

Our initial stream only contains three elements (step A, B and C), so for each element, we need another stream. We create an IntStream with all valid indexed of both lists. We're mapping them to a string, getting the elements from the two methods. I used String.format, but this of course can be replaced by a StringBuilder or simple string concatenation.

At this point, we have streams within streams. We need to flattenize it to a single stream, simply by calling flatMap.

At last, we can join all elements using \n as glue.


It's a shame that getNextStepNames and getConditions both seem related, but yet are separate. But that's another story.

Upvotes: 0

Naman
Naman

Reputation: 32046

One step closer to that could be :

for (Step step : steps) {
    List<String> nextStepNames = getNextStepNames(step);
    List<String> conditions = getConditions(step);
    IntStream.range(0, nextStepNames.size())
            .forEach(i -> stringBuilder.append("If ")
                    .append(step.getName())
                    .append("is active, and condition (")
                    .append(conditions.get(i))
                    .append(") is true, then move to ")
                    .append(nextStepNames.get(i)).append("\n"));
}

Upvotes: 0

Related Questions