流是什么

流是对Java API的更新,允许你以声明式的方式操作数据集合。首先看一个例子,假设你想获得所有热量低于400卡的菜肴的名字,并按热量排序。在Java 8之前,你可以这样做:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
List<Dish> lowCaloricDishes = new ArrayList<>();
for (Dish dish : menu) {
    if (dish.getCalories() < 400) {
        lowCaloricDishes.add(dish);
    }
}

Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
    public int compare(Dish dish1, Dish dish2) {
        return Integer.compare(dish1.getCalories(), dish2.getCalories());
    }
});

List<String> lowCaloricDishesName = new ArrayList<>();
for (Dish dish : lowCaloricDishes) {
    lowCaloricDishesName.add(dish.getName());
}

在Java 8之后,你只需这样做:

1
2
3
4
5
6
7
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
List<String> lowCaloricDishesName = menu.stream()
    .filter(d -> d.getCalories() < 400)
    .sorted(comparing(Dish::getCalories))
    .map(Dish::getName)
    .collect(toList());

要利用多核体系结构并行执行此代码,只需将stream()更改为parallelStream()

1
2
3
4
5
List<String> lowCaloricDishesName = menu.parallelStream()
    .filter(d -> d.getCalories() < 400)
    .sorted(comparing(Dish::getCalories))
    .map(Dish::getName)
    .collect(toList());

总之,Java 8中的流API允许你编写这样的代码

  • 声明式:更简洁,可读性更强
  • 可组合:更灵活
  • 可并行:更好的性能

开始使用流

首先,流到底是什么?一个简短的定义是,来自某个数据源的支持数据处理操作的元素序列。

  • 元素序列:与集合类似,流提供了访问特定类型元素集的接口。集合是关于数据的,流是关于计算的。
  • 数据源:流从数据源(如集合、数组或I/O资源)消费。从集合生成的流保留了集合中元素的顺序。
  • 数据处理操作:流支持类似数据库的操作,以及函数式编程语言的常见操作来操作数据。比如filter,map,reduce,find, match, sort等。流操作可以顺序执行,也可以并行执行。

此外,流操作还有两个重要特征:

  • 流管道:许多流操作返回流本身,允许将操作链接起来形成一个更大的流管道。这样可以支持某些优化,比如惰性求值和短路求值。在数据源上的流管道操作可以看作类似数据库的查询。
  • 内部迭代:与集合相反,流操作在内部进行迭代。

流与集合

只遍历一次

与迭代器类似,流只能遍历一次,遍历之后流就被消耗殆尽了。你可以从初始数据源获得一个新的流来再次遍历它。

1
2
3
4
List<String> title = Arrays.asList("Modern", "Java", "In", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println); // java.lang.IllegalStateException

外部迭代与内部迭代

集合需要用户进行迭代,比如使用foreach,这称为外部迭代。与之相反,流使用内部迭代,即为你执行迭代,并负责将流产生的值存储在某个地方,你只需提供一个函数说明要做什么。

流操作

可以连接的流操作称为中间操作,关闭流的操作称为结束操作。

中间操作

中间操作(如filter或sorted)返回另一个流,这允许将操作连接起来形成查询。重要的是,在调用结束操作之前,中间操作不会执行任何处理,它们是惰性求值的。这是因为中间操作通常可以被结束操作合并一次处理。

结束操作

结束操作从流管道产生一个非流值结果。

使用流

总而言之,使用流通常包括三个部分:

  • 执行查询的数据源,比如集合
  • 形成流管道的一系列中间操作
  • 执行流管道并产生结果的结束操作

下表总结了一些常见流操作。

操作 类型 返回类型 参数 函数描述符
filter 中间操作 Stream<T> Predicate<T> T -> boolean
map 中间操作 Stream<R> Function<T, R> T -> R
limit 中间操作 Stream<T>
sorted 中间操作 Stream<T> Comparator<T> (T, T) -> int
distinct 中间操作 Stream<T>
forEach 结束操作 void Consumer<T> T -> void
count 结束操作 long
collect 结束操作 R Collector<T, A, R>