Java 中的 Stream 可以替代 for 循环吗?

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()方法中值是不可修改的,为我们提供了不变性,因此我们可以在代码中的任何位置共享它,而不必担心副作用。这使得代码不易出错,并且读者更容易理解我们的代码。

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 == nullreturn 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 次尝试的平均执行时间:

正如我们在上面的比较表中看到的,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()。

这是比较:

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

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

并行流并非万能通用,只有当任务是独立的时,它才有用。如果任务不是独立的,并且必须共享相同的资源,则必须使用锁(主要是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(010).forEach(i -> ...);

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

4概括

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

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

来源:betterprogramming.pub/can-streams-replace-loops-in-java-f56d4461743a
后端专属技术群

构建高质量的技术交流社群,欢迎从事编程开发、技术招聘HR进群,也欢迎大家分享自己公司的内推信息,相互帮助,一起进步!

文明发言,以交流技术职位内推行业探讨为主

广告人士勿入,切勿轻信私聊,防止被骗

加我好友,拉你进群

相关推荐

  • 干掉 powerdesigner,设计数据库表用它就够了!
  • 大多数公司,都对 AGPL 许可证“敬而远之”
  • 保护生产中 Node.js 应用程序安全的 15 项最佳实践
  • 从0到1实现一个前端监控系统(附源码)
  • 语雀发布故障公告,个人用户可领6个月会员服务
  • LeCun又双叒唱衰自回归LLM:GPT-4的推理能力非常有限,有两篇论文为证
  • 前端线上部署,如何通知用户有新版本???
  • 在Vue中,template和JSX到底哪个好?有思考过吗?
  • 高性能的手机端神经网络计算框架
  • 如何保证分布式情况下的幂等性
  • [开源]前后端分离、不一样的快速发开框架,全自动生成代码
  • 自从学会 Arthas,日常开发效率直接起飞!!
  • 7.9K Star开源项目:可显示命令执行的进度条,体验感拉满
  • Inceptive:指令药物时代的 AGI 编辑器
  • 这张图清晰地展示了当前主流的AI创意生成工具和它们之间的从属关系。
  • AI Copilot 市场图谱
  • 1025.AI日报:2023年最佳发明榜单,AI领域有14项入选
  • 终身疼痛、焦虑自卑、居家0收入,我是如何走出人生至暗时刻的?
  • 混合精度训练原理总结
  • 拼参数、拼应用、拼落地,大模型时代的 AI 编程何以称强?