6.4 分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组——true是一组,false是一组。例如,如果你是素食者或是请了一位素食的朋友来共进晚餐,可能会想要把菜单按照素食和非素食分开:

  1. Map<Boolean, List<Dish>> partitionedMenu =
  2. menu.stream().collect(partitioningBy(Dish::isVegetarian)); ←---- 分区函数

这会返回下面的Map

  1. {false=[pork, beef, chicken, prawns, salmon],
  2. true=[french fries, rice, season fruit, pizza]}

那么通过Map中键为true的值,就可以找出所有的素食菜肴了:

  1. List<Dish> vegetarianDishes = partitionedMenu.get(true);

请注意,用同样的分区谓词,对菜单List创建的流作筛选,然后把结果收集到另外一个List中也可以获得相同的结果:

  1. List<Dish> vegetarianDishes =
  2. menu.stream().filter(Dish::isVegetarian).collect(toList());

6.4.1 分区的优势

分区的好处在于保留了分区函数返回truefalse的两套流元素列表。在上一个例子中,要得到非素食DishList,你可以使用两个筛选操作来访问partitionedMenu这个Mapfalse键的值:一个利用谓词,一个利用该谓词的非。而且就像你在分组中看到的,partitioningBy工厂方法有一个重载版本,可以像下面这样传递第二个收集器:

  1. Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
  2. menu.stream().collect(
  3. partitioningBy(Dish::isVegetarian, ←---- 分区函数
  4. groupingBy(Dish::getType))); ←---- 第二个收集器

这将产生一个二级Map

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

这里,对于分区产生的素食和非素食子流,分别按类型对菜肴分组,得到了一个二级Map,和6.3.1节的二级分组得到的结果类似。再举一个例子,你可以重用前面的代码来找到素食和非素食中热量最高的菜:

  1. Map<Boolean, Dish> mostCaloricPartitionedByVegetarian =
  2. menu.stream().collect(
  3. partitioningBy(Dish::isVegetarian,
  4. collectingAndThen(maxBy(comparingInt(Dish::getCalories)),
  5. Optional::get)));

这将产生以下结果:

  1. {false=pork, true=pizza}

本节开始时说过,你可以把分区看作分组的一种特殊情况。值得一提的是,由partitioningBy返回的Map实现其结构更紧凑,也更高效,这是因为它只包含两个键:truefalse。实际上,它的内部实现就是一个特殊的Map,只有两个字段。groupingBypartitioningBy收集器之间的相似之处并不止于此。你在下一个测验中会看到,还可以按照和6.3.1节中分组类似的方式进行多级分区。

测验6.2:使用partitioningBy

我们已经看到,和groupingBy收集器类似,partitioningBy收集器也可以结合其他收集器使用。尤其是它可以与第二个partitioningBy收集器一起使用来实现多级分区。以下多级分区的结果会是什么呢?

(1)

  1. menu.stream().collect(partitioningBy(Dish::isVegetarian,
  2. partitioningBy(d -> d.getCalories() > 500)));

(2)

  1. menu.stream().collect(partitioningBy(Dish::isVegetarian,
  2. partitioningBy(Dish:: getType)));

(3)

  1. menu.stream().collect(partitioningBy(Dish::isVegetarian,
  2. counting()));

答案

(1) 这是一个有效的多级分区,产生以下二级Map

  1. {false={false=[chicken, prawns, salmon], true=[pork, beef]},
  2. true={false=[rice, season fruit], true=[french fries, pizza]}}

(2) 这无法编译,因为partitioningBy需要一个谓词,也就是返回一个布尔值的函数。方法引用Dish::getType不能用作谓词。

(3) 它会计算每个分区中项目的数目,得到以下Map

  1. {false=5, true=4}

作为使用partitioningBy收集器的最后一个例子,我们把菜单数据模型放在一边,来看一个更为复杂也更为有趣的例子:将数字分为质数和非质数。

6.4.2 将数字按质数和非质数分区

假设你要写一个方法,它接受参数nint类型),并将前n个自然数分为质数和非质数。但首先,找出能够测试某一个待测数字是否是质数的谓词会很有帮助:

  1. public boolean isPrime(int candidate) {
  2. return IntStream.range(2, candidate) ←---- 产生一个自然数范围,从2开始,直至但不包括待测数
  3. .noneMatch(i -> candidate % i == 0); ←---- 如果待测数字不能被流中任何数字整除则返回true
  4. }

一个简单的优化是仅测试小于等于待测数平方根的因子:

  1. public boolean isPrime(int candidate) {
  2. int candidateRoot = (int) Math.sqrt((double) candidate);
  3. return IntStream.rangeClosed(2, candidateRoot)
  4. .noneMatch(i -> candidate % i == 0);
  5. }

现在最主要的一部分工作已经做好了。为了把前n个数字分为质数和非质数,只要创建一个包含这n个数的流,用刚刚写的isPrime方法作为谓词,再给partitioningBy收集器归约就好了:

  1. public Map<Boolean, List<Integer>> partitionPrimes(int n) {
  2. return IntStream.rangeClosed(2, n).boxed()
  3. .collect(
  4. partitioningBy(candidate -> isPrime(candidate)));
  5. }

现在我们已经讨论过了Collectors类的静态工厂方法能够创建的所有收集器,并介绍了使用它们的实际例子。表6-1将它们汇总到一起,给出了它们应用到Stream上返回的类型,以及它们用于一个叫作menuStreamStream上的实际例子。

表 6-1 Collectors类的静态工厂方法

工厂方法返回类型用于
toListList把流中所有项目收集到一个List
使用示例:
  1. List<Dish> dishes = menuStream.collect(toList());
toSetSet把流中所有项目收集到一个Set,删除重复项
使用示例:
  1. Set<Dish> dishes = menuStream.collect(toSet());
toCollectionCollection把流中所有项目收集到给定的供应源创建的集合
使用示例:
  1. Collection<Dish> dishes = menuStream.collect(toCollection(), ArrayList::new);
countingLong计算流中元素的个数
使用示例:
  1. long howManyDishes = menuStream.collect(counting());
summingIntInteger对流中项目的一个整数属性求和
使用示例:
  1. int totalCalories = menuStream.collect(summingInt(Dish::getCalories));
averagingIntDouble计算流中项目Integer属性的平均值
使用示例:
  1. double avgCalories = menuStream.collect(averagingInt(Dish::getCalories));
summarizingIntIntSummaryStatistics收集关于流中项目Integer属性的统计值,例如最大、最小、总和与平均值
使用示例:
  1. IntSummaryStatistics menuStatistics =
    menuStream.collect(summarizingInt(Dish::getCalories));
joiningString连接对流中每个项目调用toString方法所生成的字符串
使用示例:
  1. String shortMenu = menuStream.map(Dish::getName).collect(joining(", "));
maxByOptional一个包裹了流中按照给定比较器选出的最大元素的Optional,或如果流为空则为Optional.empty()
使用示例:
  1. Optional<Dish> fattest =
    menuStream.collect(maxBy(comparingInt(Dish::getCalories)));
minByOptional一个包裹了流中按照给定比较器选出的最小元素的Optional,或如果流为空则为Optional.empty()
使用示例:
  1. Optional<Dish> lightest =
    menuStream.collect(minBy(comparingInt(Dish::getCalories)));
reducing归约操作产生的类型从一个作为累加器的初始值开始,利用BinaryOperator与流中的元素逐个结合,从而将流归约为单个值
使用示例:
  1. int totalCalories =
    menuStream.collect(reducing(0, Dish::getCalories, Integer::sum));
collectingAndThen转换函数返回的类型包裹另一个收集器,对其结果应用转换函数
使用示例:
  1. int howManyDishes = menuStream.collect(collectingAndThen(toList(), List::size));
groupingByMap>根据项目的一个属性的值对流中的项目作分组,并将属性值作为结果Map的键
使用示例:
  1. Map<Dish.Type,List<Dish>> dishesByType =
    menuStream.collect(groupingBy(Dish::getType));
partitioningByMap>根据对流中每个项目应用谓词的结果来对项目进行分区
使用示例:
  1. Map<Boolean,List<Dish>> vegetarianDishes =
    menuStream.collect(partitioningBy(Dish::isVegetarian));

本章开头提到过,所有这些收集器都是对Collector接口的实现,因此本章剩余部分会详细讨论这个接口。我们会看看这个接口中的方法,然后探讨如何实现你自己的收集器。