Can Stream replace for loop in Java?

This is a community that may be useful to you

One-to-one communication/interview brochure/resume optimization/job search questions, welcome to join the “Yudao Rapid Development Platform” Knowledge Planet. The following is some information provided by Planet:

  • “Project Practice (Video)”: Learn from books, “practice” from past events

  • “Internet High Frequency Interview Questions”: Studying with your resume, spring blossoms

  • “Architecture x System Design”: Overcoming difficulties and mastering high-frequency interview scenario questions

  • “Advancing Java Learning Guide”: systematic learning, the mainstream technology stack of the Internet

  • “Must-read Java Source Code Column”: Know what it is and why it is so

ea5d51e856b680c141236e8944632c66.gif

This is an open source project that may be useful to you

Domestic Star is a 100,000+ open source project. The front-end includes management backend + WeChat applet, and the back-end supports monomer and microservice architecture.

Functions cover RBAC permissions, SaaS multi-tenancy, data permissions, mall, payment, workflow, large-screen reports, WeChat public account, etc.:

  • Boot address: https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud address: https://gitee.com/zhijiantianya/yudao-cloud

  • Video tutorial: https://doc.iocoder.cn

Source: betterprogramming.pub/
can-streams-replace-loops-in
-java-f56d4461743a

  • 1 Code comparison

    • List of project names with type

    • Generate random list

  • 2 Performance comparison

    • Iterate elements

    • Parallel flow optimization

  • 3 limitations

    • conditional loop

    • repeat

  • 4 Summary

The release of Java 8 is a major moment in Java history. Streams and Lambdas were introduced and they are now widely used. If you don’t know about Streams, or have never heard of it, that’s totally fine. In most cases, loops will suit our needs just as well, and we won’t encounter any problems without Streams.

So why do we need Streams? Can they replace loops? Or is it more advantageous than looping? In this article, we’ll look at the code, compare performance, and see how good Streams are as a replacement for loops.

1 Code comparison

Streams increase code complexity because they require classes, interfaces, and importing new packages; in contrast, loops are built-in and don’t require anything extra to be imported. This is true on some points, but not necessarily: code complexity cannot be measured just by introducing a few classes and package files. What is more important is the readability of the code. Let’s look at some examples.

List of project names with types

Let’s say we have a list of items and want a list of names for a specific item type. Using a loop, we need to write the following:

List<String> getItemNamesOfType(List<Item> items, Item.Type type) {
    List<String> itemNames = new ArrayList<>();
    for (Item item : items) {
        if (item.type() == type) {
            itemNames.add(item.name());
        }
    }
    return itemNames;
}

Reading the code, we will see that ArrayList should be instantiated with a new and add() should be type checked and called in every loop. Let’s look at how the Streams version is handled:

List<String> getItemNamesOfTypeStream(List<Item> items, Item.Type type) {
    return items.stream()
            .filter(item -> item.type() == type)
            .map(item -> item.name())
            .toList();
}

With the help of Lambda, it can be seen immediately that we first select the items with a given type and then get the list of names to filter the items. In this kind of code, the line-by-line flow is very consistent with the logical flow.

Generate a random list

Let’s look at another example, in the time comparison section we will review the key Streams methods and compare their execution time with the loop. For this we need a random list of Items. Here’s a snippet with a static method that gives a random Item:

public record Item(Type type, String name) {
    public enum Type {
        WEAPON, ARMOR, HELMET, GLOVES, BOOTS,
    }

    private static final Random random = new Random();
    private static final String[] NAMES = {
            "beginner",
            "knight",
            "king",
            "dragon",
    };

    public static Item random() {
        return new Item(
                Type.values()[random.nextInt(Type.values().length)],
                NAMES[random.nextInt(NAMES.length)]);
    }
}

Now, let’s create a random list of Item using a loop. The code looks like this:

List<Item> items = new ArrayList<>(100);
for (int i = 0; i < 100; i + + ) {
    items.add(Item.random());
}

The code for Streams looks like this:

List<Item> items = Stream.generate(Item::random).limit(length).toList();

This is a wonderful and easy to read code. Additionally, the value returned by List in the toList() method is unmodifiable, providing us with immutability so we can share it anywhere in the code without worrying about side effects. This makes the code less error-prone and makes it easier for readers to understand our code.

Streams provide a variety of useful methods that allow us to write concise code. The most popular are:

  • allMatch()

  • anyMatch()

  • count()

  • filter()

  • findFirst()

  • forEach()

  • map()

  • reduce()

  • sorted()

  • limit()

Backend management system + user applet implemented based on Spring Boot + MyBatis Plus + Vue & Element, supporting RBAC dynamic permissions, multi-tenancy, data permissions, workflow, three-party login, payment, SMS, mall and other functions

  • Project address: https://github.com/YunaiV/ruoyi-vue-pro

  • Video tutorial: https://doc.iocoder.cn/video/

2 Performance comparison

Under normal circumstances, Streams behave like loops with little or no impact on execution time. Let’s compare some of the main behaviors in Streams with the loop implementation.

Iterate elements

When we have a collection of elements, there are many cases where we iterate over all the elements in the collection. In Streams, such as forEach(), map(), reduce() and filter() Method can perform this full-element iteration.

Let’s consider a situation where we want to count each type of item in a list.

The code with a for loop looks like this:

public Map<Item.Type, Integer> loop(List<Item> items) {
    Map<Item.Type, Integer> map = new HashMap<>();
    for (Item item : items) {
        map.compute(item.type(), (key, value) -> {
            if (value == null) return 1;
            return value + 1;
        });
    }
    return map;
}

The code for Streams looks like this:

public Map<Item.Type, Integer> stream(List<Item> items) {
    return items.stream().collect(Collectors.toMap(
            Item::type,
            value -> 1,
            Integer::sum));
}

They look very different, but how do they perform? The following table shows the average execution time for 100 attempts:

6d0a61736e9e2476d6d5ef360ca85c88.png

As we can see in the comparison table above, Streams and loops show a small difference in execution time when iterating over the entire list. In most cases, this is the same for other Stream methods such as map(), forEach(), reduce(), etc. .

Parallel stream optimization

Therefore, we find that streams perform no better or worse than loops when iterating over lists. However, there’s one magic thing about Streams that loops don’t have:We can easily leverage streams for multi-threaded computations. All that has to be done is to use parallelStream() instead of stream().

To understand how much impact we can get from this, let’s look at the following example where we simulate a long task as follows:

private void longTask() {
    // Mock long task.
    try {
        Thread.sleep(1);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

Looping through the list will look like this:

protected void loop(List<Item> items) {
    for (Item item : items) {
        longTask();
    }
}

The Stream will look like this:

protected void stream(List<Item> items) {
    items.stream().forEach(item -> longTask());
}

Finally, the parallel stream will look like this:

protected void parallel(List<Item> items) {
    items.parallelStream().forEach(item -> longTask());
}

Note that onlystream() has been changed to parallelStream().

Here is the comparison:

c046199c2523b2be55960abc3db2d2b0.png

As expected, there is little difference between a loop and a Stream. What about parallel streams? Sensational! It saves over 80% execution time compared to other implementations! How can this be?

For tasks that take a long time to complete and should be done independently for each element in the list, we can expect significant improvements if they can be run concurrently. This is what parallel streams are doing. They distribute them into multiple threads and make them run simultaneously.

Parallel streams are not a one-size-fits-all approach; they are only useful when tasks are independent. If the tasks are not independent and must share the same resources, you must use locks (mainly the synchronized keyword in Java) to keep them safe, at which point they run slower than normal iterations.

Backend management system + user applet implemented based on Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element, supporting RBAC dynamic permissions, multi-tenancy, data permissions, workflow, three-party login, payment, SMS, mall and other functions

  • Project address: https://github.com/YunaiV/yudao-cloud

  • Video tutorial: https://doc.iocoder.cn/video/

3 Limitations

However, Stream also has limitations. One case is conditional loop and the other case is repetition. Let’s see what they mean.

Conditional loop

We usually use while loops when we want to repeat until a condition is true but are not sure how many iterations are needed.

boolean condition = true;
while (condition) {
    ...
    condition = doSomething();
}

Code that performs the same thing using Streams looks like this:

Stream.iterate(true, condition -> condition, condition -> doSomething())
        .forEach(unused -> ...);

We can see that parts of the Streams code interfere with reading, such as condition -> condition checking whether the condition is true, unused and forEach(). With this in mind, conditional loops are best written within a while loop.

Repeat

Repetition is one of the main reasons for loops exist. Let’s say we want to repeat this process ten times. With a for loop, it can be easily written as:

for (int i = 0; i < 10; i + + ) {
  ...
}

In Streams, one way to achieve this is to create an IntStream containing [0, 1, 2, ..., 9] and iterate over it.

IntStream.range(0, 10).forEach(i -> ...);

While the code may look concise and correct, it looks more focused on values in the range 0 to 10 (excluded), where the for loop code can read ten times repeatedly, since it is more common to write repeat like this: from 0 Start, end with reps.

4 Summary

We’ve done some comparisons between streams and loops. So…can Streams replace loops? Well, as always, it depends! However, Streams often give us cleaner, easier-to-read code and optimizations.

What are you waiting for? Go ahead and start coding with Streams!

Welcome to join my knowledge planet and comprehensively improve your technical capabilities.

To join, Long press” or “Scan” the QR code below:

74ebab390112c07ec69bb2d50327f96b.png

Planet’s content includes: project practice, interviews and recruitment, source code analysis, and learning routes.

de70f07df0a585c2913e562571a5df2e.png

6e19cfcdf82c51fb9704796f98283d86.pngbc35ec508d6daf5517ff2ab9a3bf203c.png a3b0be87db381ea0ad4a9e6a0887fae2.pngf106aa0efbb1fa5bf5ce62406 da0bf70.png

If the article is helpful, please read it and forward it.
Thank you for your support (*^__^*)