Java8的发布是Java历史上的一个重大时刻。Streams 和 Lambda 被引入,它们现在被广泛使用。如果你不知道 Streams,或者从来没有听说过它,那是完全没有问题的。在大多数情况下,循环同样可以满足我们的需要,没有 Streams 也不会遇到任何问题。

那我们为什么需要Streams?它们能取代循环吗?或者比循环更有优势?在本文中,我们将研究代码,比较性能,并看看Streams作为循环的替代品有多好。

1. 代码比较

Streams 增加了代码复杂性,因为它们需要类、接口和导入新的包;相比之下,循环天生就是内置的,不需要额外的引入任何东西。这在某些点上是对的,但也不尽然:代码复杂度并不能仅仅看引入几个类、几个包文件来衡量,更重要的是代码的可读性。让我们看一些例子。

具有类型的项目名称列表

假设我们有一个项目列表,并且想要特定项目类型的名称列表。使用循环,我们需要编写以下内容:

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;
}

阅读代码,我们会发现 ArrayList 应该实例化一个 new ,并且add()应该在每个循环中进行类型检查和调用。再来看,Streams 版本是如何处理的:

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

在 Lambda 的帮助下,可以立即发现我们首先选择具有给定类型的项目,然后获取过滤项目的名称列表。在这种代码中,逐行流程与逻辑流程非常一致。

生成随机列表

让我们看另一个例子,在时间比较部分,我们将回顾关键的 Streams 方法并将它们的执行时间与循环进行比较。为此,我们需要一个随机的Items 列表。这是一个带有静态方法的片段,它给出了 随机 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)]);
    }
}

现在,让我们Item使用循环创建一个随机列表。代码如下所示:

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

Streams 的代码如下所示:

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

这是一段精彩且易于阅读的代码。此外,List返回的toList()方法中值是不可修改的,为我们提供了不变性,因此我们可以在代码中的任何位置共享它,而不必担心副作用。这使得代码不易出错,并且读者更容易理解我们的代码。

推荐Java面试题神器,微信小程序内搜索:Java精选面试题

Streams 提供了多种有用的方法,让我们可以编写简洁的代码。最流行的是:

  • allMatch()
  • anyMatch()
  • count()
  • filter()
  • findFirst()
  • forEach()
  • map()
  • reduce()
  • sorted()
  • limit()

2. 性能比较

在正常情况下,Streams 的行为类似于循环,对执行时间影响很小或没有影响。让我们将 Streams 中的一些主要行为与循环实现进行比较。

迭代元素

当我们有一个元素集合时,在很多情况下都会迭代集合中的所有元素。在 Streams 中,诸如forEach()map()reduce()filter()类的方法可以执行这种全元素迭代。

让我们考虑一种情况,我们想要对列表中的每种类型的项目进行计数。

带 for 循环的代码如下所示:

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;
}

Streams 的代码如下所示:

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

它们看起来截然不同,但它们的表现如何呢?下表是 100 次尝试的平均执行时间:

java20中用什么代替ThreadPoolExecutor_迭代

正如我们在上面的比较表中看到的,Streams 和循环在迭代整个列表时显示出很小的执行时间差异。在大多数情况下,这对于其他 Stream 方法(如map()forEach()reduce()等)是相同的。

并行流优化

因此,我们发现在迭代列表时,流的性能并不比循环更好或更差。然而,Streams 有一个循环所不具备的神奇之处:我们可以轻松地利用流进行多线程计算。 所要做的就是使用parallelStream()而不是stream()

为了了解我们可以从中获得多少影响,让我们看一下下面的示例,其中我们模拟了耗时较长的任务,如下所示:

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

循环遍历列表将如下所示:

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

Stream将如下所示:

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

最后,并行流将如下所示:

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

请注意, onlystream()已更改为parallelStream()。

这是比较:

java20中用什么代替ThreadPoolExecutor_List_02

正如预期的那样,循环和Stream几乎没有什么区别。那么并行流呢?耸人听闻!与其他实现相比,它节省了 80% 以上的执行时间!这怎么可能?

对于需要很长时间才能完成并且应该为列表中的每个元素独立完成的任务,它们可以同时运行,我们可以期待显着的改进。这就是并行流正在做的事情。他们将它们分配到多个线程中并使它们同时运行。

推荐划水摸鱼地址:

https://www.yoodb.com/slack-off/home.html

并行流并非万能通用,只有当任务是独立的时,它才有用。如果任务不是独立的,并且必须共享相同的资源,则必须使用锁(主要是Java中的synchronized关键字)来保证它们的安全,此时它们的运行速度慢于正常的迭代。

3. 局限性

然而,Stream也有局限性。一种情况是条件循环,另一种情况是重复。让我们看看它们的意思。

条件循环

当我们想要重复直到条件为真但不确定需要多少次迭代时,我们通常使用while循环。

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

使用 Streams 表现相同的代码如下所示:

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

我们可以看到 Streams 代码部分会干扰读取,例如condition -> condition检查条件是否为真,unused以及forEach()。考虑到这一点,条件循环最好写在while循环中。

重复

重复是for循环存在的主要原因之一。假设我们想重复这个过程十次。有了for循环,就可以很容易地写成:

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

在 Streams 中,实现此目的的一种方法是创建IntStream包含[0, 1, 2, ... , 9]并迭代它的 。

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

虽然代码可能看起来简洁而正确,但它看起来更侧重于0到10(排除)范围内的值,其中for循环代码可以重复读取十次,因为更常见的做法是这样写repeat:从0开始,以重复次数结束。

4. 概况

我们已经对流和循环进行了一些比较。那么……Streams 可以取代循环吗?嗯,一如既往,这取决于情况!然而,Streams 通常可以为我们提供更简洁、易于阅读的代码和优化。

你还在等什么?继续开始使用 Streams 编写代码吧!


公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!
最近有很多人问,有没有读者交流群!加入方式很简单,公众号Java精选,回复“加群”,即可入群!

🔥Java精选面试题🔥:3000+道面试题,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计等,在线随时刷题!
------ 特别推荐 ------
特别推荐:专注分享最前沿的技术与资讯,为弯道超车做好准备及各种开源项目与高效率软件的公众号,「大咖笔记」,专注挖掘好东西,非常值得大家关注。点击下方公众号卡片关注。

点击“阅读原文”,了解更多精彩内容!文章有帮助的话,点在看,转发吧!