Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to convert a for iteration with conditions to Java 8 stream

Currently, I have this method, which I want to convert to a Java 8 stream style (I have little practice with this API btw, that's the purpose of this little exercise):

private static Map<Integer, List<String>> splitByWords(List<String> list) {
   for (int i = 0; i < list.size(); i++) { 
        if(list.get(i).length() > 30 && list.get(i).contains("-")) {
            mapOfElements.put(i, Arrays.stream(list.get(i).split("-")).collect(Collectors.toList()));
        } else if(list.get(i).length() > 30) {
            mapOfElements.put(i, Arrays.asList(new String[]{list.get(i)}));
        } else {
            mapOfElements.put(i, Arrays.asList(new String[]{list.get(i) + "|"}));
        }
   }

   return mapOfElements;
}

This is what I´ve got so far:

private static Map<Integer, List<String>> splitByWords(List<String> list) {
   Map<Integer, List<String>> mapOfElements = new HashMap<>();
   IntStream.range(0, list.size())
        .filter(i-> list.get(i).length() > 30 && list.get(i).contains("-"))
        .boxed()
        .map(i-> mapOfElements.put(i, Arrays.stream(list.get(i).split("-")).collect(Collectors.toList())));

//Copy/paste the above code twice, just changing the filter() and map() functions?

In the "old-fashioned" way, I just need one for iteration to do everything I need regarding my conditions. Is there a way to achieve that using the Stream API or, if I want to stick to it, I have to repeat the above code just changing the filter() and map() conditions, therefore having three for iterations?

like image 449
StatelessDev Avatar asked Sep 04 '18 16:09

StatelessDev


People also ask

Is Java 8 stream faster than for loop?

Yes, streams are sometimes slower than loops, but they can also be equally fast; it depends on the circumstances. The point to take home is that sequential streams are no faster than loops.

Which is better stream or for loop Java?

If you have a small list, loops perform better. If you have a huge list, a parallel stream will perform better.

Why is Java 8 stream lazy?

Streams are lazy because intermediate operations are not evaluated unless terminal operation is invoked. Each intermediate operation creates a new stream, stores the provided operation/function and return the new stream. The pipeline accumulates these newly created streams.


2 Answers

The current solution with the for-loop looks good. As you have to distinguish three cases only, there is no need to generalize the processing.

Should there be more cases to distinguish, then it could make sense to refactor the code. My approach would be to explicitly define the different conditions and their corresponding string processing. Let me explain it using the code from the question.

First of all I'm defining the different conditions using an enum.

  public enum StringClassification {
    CONTAINS_HYPHEN, LENGTH_GT_30, DEFAULT;

    public static StringClassification classify(String s) {
      if (s.length() > 30 && s.contains("-")) {
        return StringClassification.CONTAINS_HYPHEN;
      } else if (s.length() > 30) {
        return StringClassification.LENGTH_GT_30;
      } else {
        return StringClassification.DEFAULT;
      }
    }
  }

Using this enum I define the corresponding string processors:

  private static final Map<StringClassification, Function<String, List<String>>> PROCESSORS;
  static {
    PROCESSORS = new EnumMap<>(StringClassification.class);
    PROCESSORS.put(StringClassification.CONTAINS_HYPHEN, l -> Arrays.stream(l.split("-")).collect(Collectors.toList()));
    PROCESSORS.put(StringClassification.LENGTH_GT_30, l -> Arrays.asList(new String[] { l }));
    PROCESSORS.put(StringClassification.DEFAULT, l -> Arrays.asList(new String[] { l + "|" }));
  }

Based on this I can do the whole processing using the requested IntStream:

  private static Map<Integer, List<String>> splitByWords(List<String> list) {
    return IntStream.range(0, list.size()).boxed()
      .collect(Collectors.toMap(Function.identity(), i -> PROCESSORS.get(StringClassification.classify(list.get(i))).apply(list.get(i))));
  }

The approach is to retrieve for a string the appropriate StringClassification and then in turn the corresponding string processor. The string processors are implementing the strategy pattern by providing a Function<String, List<String>> which maps a String to a List<String> according to the StringClassification.

A quick example:

  public static void main(String[] args) {
    List<String> list = Arrays.asList("123",
      "1-2",
      "0987654321098765432109876543211",
      "098765432109876543210987654321a-b-c");
    System.out.println(splitByWords(list));
  }

The output is:

{0=[123|], 1=[1-2|], 2=[0987654321098765432109876543211], 3=[098765432109876543210987654321a, b, c]}

This makes it easy to add or to remove conditions and string processors.

like image 191
LuCio Avatar answered Nov 14 '22 23:11

LuCio


First of I don't see any reason to use the type Map<Integer, List<String>> when the key is an index. Why not use List<List<String>> instead? If you don't use a filter the elements should be on the same index as the input.

The power in a more functional approach is that it's more readable what you're doing. Because you want to do multiple things for multiple sizes strings it's pretty hard write a clean solution. You can however do it in a single loop:

private static List<List<String>> splitByWords(List<String> list)
{
    return list.stream()
        .map(
            string -> string.length() > 30
                ? Arrays.asList(string.split("-")) 
                : Arrays.asList(string + "|")
        )
        .collect(Collectors.toList());
}

You can add more complex logic by making your lambda multiline (not needed in this case). eg.

.map(string -> {
    // your complex logic

    // don't forget, when using curly braces you'll
    // need to return explicitly
    return result;
})

The more functional approach would be to group the strings by size followed by applying a specific handler for the different groups. It's pretty hard to keep the index the same, so I change the return value to Map<String, List<String>> so the result can be fetched by providing the original string:

private static Map<String, List<String>> splitByWords(List<String> list)
{
    Map<String, List<String>> result = new HashMap<>();
    Map<Boolean, List<String>> greaterThan30;

    // group elements
    greaterThan30 = list.stream().collect(Collectors.groupingBy(
        string -> string.length() > 30
    ));

    // handle strings longer than 30 chars
    result.putAll(
        greaterThan30.get(true).stream().collect(Collectors.toMap(
            Function.identity(), // the same as: string -> string
            string -> Arrays.asList(string.split("-"))
        ))
    );

    // handle strings not longer than 30 chars
    result.putAll(
        greaterThan30.get(false).stream().collect(Collectors.toMap(
            Function.identity(), // the same as: string -> string
            string -> Arrays.asList(string + "|")
        ))
    );

    return result;
}

The above seems like a lot of hassle, but is in my opinion better understandable. You could also dispatch the logic to handle large and small strings to other methods, knowing the provided string does always match the criteria.

This is slower than the first solution. For a list of size n, it has to loop through n elements to group by the criteria. Then loop through x (0 <= x <= n) elements that match the criteria, followed by a loop through n - x elements that don't match the criteria. (In total 2 times the whole list.)

In this case it might not be worth the trouble since both the criteria, as well as the logic to apply are pretty simple.

like image 24
3limin4t0r Avatar answered Nov 15 '22 00:11

3limin4t0r