在 Stream 和 Collections 之间选择 API

Choosing between Stream and Collections API

考虑以下打印 List 中最大元素的示例:

List<Integer> list = Arrays.asList(1,4,3,9,7,4,8);           
list.stream().max(Comparator.naturalOrder()).ifPresent(System.out::println);

同样的objective也可以使用Collections.max方法实现:

System.out.println(Collections.max(list));

上面的代码不仅更短而且更清晰易读(在我看来)。有类似的例子浮现在脑海中,例如 binarySearchfilterfindAny.

结合使用

我知道 Stream 可以是无限管道,而不是受 JVM 可用内存限制的 Collection。这将是我决定使用 Stream 还是 Collections API 的标准。选择 Stream 而不是 Collections API 是否还有其他原因(例如性能)。更一般地说,这是选择 Stream 而不是可以以更简洁、更短的方式完成工作的旧 API 的唯一原因吗?

Stream API 就像一把瑞士军刀:它允许您通过有效地组合工具来执行相当复杂的操作。另一方面,如果您只需要一把螺丝刀,独立的螺丝刀可能会更方便。 Stream API 包括许多东西(如 distinctsorted、原始操作等),否则将需要您编写几行代码并引入中间 variables/data 结构和无聊的循环绘图程序员的注意力来自于实际的算法。有时使用 Stream API 甚至可以提高顺序代码的性能。例如,考虑一些旧的 API:

class Group {
    private Map<String, User> users;

    public List<User> getUsers() {
        return new ArrayList<>(users.values());
    }
}

这里我们要return该组的所有用户。 API 设计师决定 return 一个 List。但它可以以多种方式在户外使用:

List<User> users = group.getUsers();
Collections.sort(users);
someOtherMethod(users.toArray(new User[users.size]));

在这里它被排序并转换为数组以传递给碰巧接受数组的其他方法。在其他地方 getUsers() 可以这样使用:

List<User> users = group.getUsers();
for(User user : users) {
    if(user.getAge() < 18) {
        throw new IllegalStateException("Underage user in selected group!");
    }
}

这里我们只想找到符合某些条件的用户。在这两种情况下,复制到中间 ArrayList 实际上是不必要的。当我们移动到 Java 8 时,我们可以将 getUsers() 方法替换为 users():

public Stream<User> users() {
    return users.values().stream();
}

并修改来电代码。第一个:

someOtherMethod(group.users().sorted().toArray(User[]::new));

第二个:

if(group.users().anyMatch(user -> user.getAge() < 18)) {
    throw new IllegalStateException("Underage user in selected group!");
}

这种方式不仅更短,而且可能工作得更快,因为我们跳过了中间复制。

Stream API 中的另一个概念点是,根据指南编写的任何流代码都可以通过添加 parallel() 步骤简单地进行并行化。当然,这并不总是会提高性能,但它的帮助比我预期的要多。通常如果操作顺序执行 0.1ms or longer,它可以从并行化中获益。无论如何,我们之前在 Java 中从未见过如此简单的并行编程方法。

当然,这总是要视情况而定。举个例子:

List<Integer> list = Arrays.asList(1,4,3,9,7,4,8);           
list.stream().max(Comparator.naturalOrder()).ifPresent(System.out::println);

如果你想高效地完成相同的事情,你可以使用

IntStream.of(1,4,3,9,7,4,8).max().ifPresent(System.out::println);

不涉及任何自动装箱。但是如果您假设事先有一个 List<Integer>,那可能不是一个选项,所以如果您只对 max 值感兴趣,Collections.max 可能是更简单的选择。

但这会导致一个问题,为什么你事先有一个 List<Integer>。也许,这是旧代码(或使用旧思维编写的新代码)的结果,除了使用装箱和 Collection 之外别无选择,因为过去没有其他选择?

因此,也许您应该先考虑生成集合的来源,然后再考虑如何使用它(或者,同时考虑两者)。

如果你只有一个 Collection 并且你只需要一个终端操作,并且存在一个简单的基于 Collection 的实现,你可以直接使用它而不必理会 Stream API。 API 设计师承认了这个想法,因为他们将 forEach(…) 之类的方法添加到 Collection API 而不是坚持每个人都使用 stream().forEach(…)Collection.forEach(…) 并不是 Collection.stream().forEach(…) 的简单简写,事实上,它已经定义在更抽象的 Iterable 接口上,甚至没有 stream() 方法。

顺便说一句,你应该明白Collections.binarySearchStream.filter/findAny之间的区别。前者要求集合 sorted 如果满足该先决条件,可能是更好的选择。但是如果集合没有排序,一个简单的线性搜索比只使用二分搜索排序更有效,更不用说二分搜索只在 Lists 时工作,而 [=48] =] 适用于支持各种源集合的任何流。