4.1 流是什么

是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无须写任何多线程代码了!第7章会详细解释流和并行化是怎么工作的。先来简单看看使用流的好处吧。下面两段代码都是用来返回低热量菜肴名称的,并按照卡路里排序,一个是用Java 7写的,另一个是用Java 8的流写的。比较一下。不用太担心Java 8代码怎么写,接下来的几节会详细解释。

之前(Java 7):

  1. List<Dish> lowCaloricDishes = new ArrayList<>();
  2. for(Dish dish: menu) {
  3. if(dish.getCalories() < 400) { ←---- 用累加器筛选元素
  4. lowCaloricDishes.add(dish);
  5. }
  6. }
  7. Collections.sort(lowCaloricDishes, new Comparator<Dish>() { ←---- 用匿名类对菜肴排序
  8. public int compare(Dish dish1, Dish dish2) {
  9. return Integer.compare(dish1.getCalories(), dish2.getCalories());
  10. }
  11. });
  12. List<String> lowCaloricDishesName = new ArrayList<>();
  13. for(Dish dish: lowCaloricDishes) {
  14. lowCaloricDishesName.add(dish.getName()); ←---- 处理排序后的菜名列表
  15. }

在这段代码中,你用了一个“垃圾变量”lowCaloricDishes。它唯一的作用就是作为一次性的中间容器。在Java 8中,实现的细节被放在它本该归属的库里了。

之后(Java 8):

  1. import static java.util.Comparator.comparing;
  2. import static java.util.stream.Collectors.toList;
  3. List<String> lowCaloricDishesName =
  4. menu.stream()
  5. .filter(d -> d.getCalories() < 400) ←---- 选出400卡路里以下的菜肴
  6. .sorted(comparing(Dish::getCalories)) ←---- 按照卡路里排序
  7. .map(Dish::getName) ←---- 提取菜肴的名称
  8. .collect(toList()); ←---- 将所有名称保存在List

为了利用多核架构并行执行这段代码,你只需要把stream()换成parallelStream()

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

你可能会想,在调用parallelStream方法的时候到底发生了什么。用了多少个线程?对性能有多大提升?是否应该使用这个方法?第7章会详细讨论这些问题。现在,你可以看出,从软件工程师的角度来看,新的方法有几个显而易见的好处。

  • 代码是以声明性方式写的:说明想要完成什么(筛选热量低的菜肴)而不是说明如何实现一个操作(利用循环和if条件等控制流语句)。你在前面的章节中也看到了,这种方法加上行为参数化让你可以轻松应对变化的需求:你很容易再创建一个代码版本,利用Lambda表达式来筛选高卡路里的菜肴,而用不着去复制粘贴代码。这种方式的另一个好处是,线程模型与查询操作实现了解耦。由于你提供了查询的菜谱,因此具体的执行既可以串行,也可以并行。这部分内容的更多细节请参考第7章。
  • 你可以把几个基础操作链接起来,来表达复杂的数据处理流水线(在filter后面接上sortedmapcollect操作,如图4-1所示),同时保持代码清晰可读。filter的结果被传给了sorted方法,再传给map方法,最后传给collect方法。

4.1 流是什么 - 图1

图 4-1 将流操作链接起来构成流的流水线

因为filtersortedmapcollect等操作是与具体线程模型无关的高层次构件,所以它们的内部实现可以是单线程的,也可能透明地充分利用你的多核架构!在实践中,这意味着你用不着为了让某些数据处理任务并行而去操心线程和锁,Stream API都替你做好了!

新的Stream API表达能力非常强。比如在读完本章以及第5章、第6章之后,你就可以写出像下面这样的代码:

  1. Map<Dish.Type, List<Dish>> dishesByType =
  2. menu.stream().collect(groupingBy(Dish::getType));

第6章会解释这个例子。简单来说就是,按照Map里面的类别对菜肴进行分组。比如,Map可能包含下列结果:

  1. {FISH=[prawns, salmon],
  2. OTHER=[french fries, rice, season fruit, pizza],
  3. MEAT=[pork, beef, chicken]}

想想要是改用循环这种典型的指令型编程方式该怎么实现吧。别浪费太多时间了。拥抱这一章和接下来几章中强大的流吧!

其他库:Guava、Apache和lambdaj

为了给Java程序员提供更好的库操作集合,前人已经做过了很多尝试。比如,Guava就是谷歌创建的一个很流行的库。它提供了multimapsmultisets等额外的容器类。Apache Commons Collections库也提供了类似的功能。最后,本书作者Mario Fusco编写的lambdaj受到函数式编程的启发,也提供了很多声明性操作集合的工具。

如今Java 8自带了官方库,可以以更加声明性的方式操作集合了。

总结一下,Java 8中的Stream API可以让你写出这样的代码:

  • 声明性——更简洁,更易读;
  • 可复合——更灵活;
  • 可并行——性能更好。

在本章剩下的部分和下一章中,我们会使用这样一个例子:一个menu,它只是一张菜肴列表。

  1. List<Dish> menu = Arrays.asList(
  2. new Dish("pork", false, 800, Dish.Type.MEAT),
  3. new Dish("beef", false, 700, Dish.Type.MEAT),
  4. new Dish("chicken", false, 400, Dish.Type.MEAT),
  5. new Dish("french fries", true, 530, Dish.Type.OTHER),
  6. new Dish("rice", true, 350, Dish.Type.OTHER),
  7. new Dish("season fruit", true, 120, Dish.Type.OTHER),
  8. new Dish("pizza", true, 550, Dish.Type.OTHER),
  9. new Dish("prawns", false, 300, Dish.Type.FISH),
  10. new Dish("salmon", false, 450, Dish.Type.FISH) );

Dish类的定义是:

  1. public class Dish {
  2. private final String name;
  3. private final boolean vegetarian;
  4. private final int calories;
  5. private final Type type;
  6. public Dish(String name, boolean vegetarian, int calories, Type type) {
  7. this.name = name;
  8. this.vegetarian = vegetarian;
  9. this.calories = calories;
  10. this.type = type;
  11. }
  12. public String getName() {
  13. return name;
  14. }
  15. public boolean isVegetarian() {
  16. return vegetarian;
  17. }
  18. public int getCalories() {
  19. return calories;
  20. }
  21. public Type getType() {
  22. return type;
  23. }
  24. @Override
  25. public String toString() {
  26. return name;
  27. }
  28. public enum Type { MEAT, FISH, OTHER }
  29. }

现在就来仔细探讨一下怎么使用Stream API。我们会用流与集合做类比,做点儿铺垫。下一章会详细讨论可以用来表达复杂数据处理查询的流操作。我们会谈到很多模式,比如筛选、切片、查找、匹配、映射和归约,还会提供很多测验和练习来加深你的理解。

接下来会讨论如何创建和操纵数字流,比如生成一个偶数流,或是勾股数流。最后,我们会讨论如何从不同的源(比如文件)创建流。还会讨论如何生成一个具有无穷多元素的流——这用集合肯定是搞不定了!