Java 8实战-第六章

114 阅读5分钟

用流收集数据

Collector接口定义了很多规约操作

收集器简介

只需指出希望的结果——“做什么”,而不用操心执行的步骤——“如何做”。

收集器用作高级规约

一般来说,Collector会对元素应用一个转换函数,并将结果累积在一个数据结果中,从而产生这一过程的最终输出。

预定义收集器

主要有将流元素规约和汇总为一个值;元素分组;元素分区

规约和汇总

在需要将流项目重组成集合时,一般会使用收集器。

查找流中的最大值和最小值

Optional<Dish> mostCalorieDish = menu.stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));

汇总

Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt,可以接受一个对象映射为求和所需int的函数,Collectors.summingLong和Collectors.summingDouble方法的作用也一样。

int totalCalories = menu.stream().collect(Collectors.summingInt(Dish::getCalories));

如果想一次向得到总和、平均值、最大值和最小值:

IntSummaryStatistics menuStatistics = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));

连接字符串

joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串

String shortMenu = menu.stream().map(Dish::getName).collect(joining(","));

广义的规约汇总

函数式编程通常提供了多种方法来执行同一个操作,收集器在某种程度上比Stream接口上直接提供的方法用起来更复杂,但好处在于它们能提供更高水平的抽象和概括,也更容易重用和自定义。

分组

使用Collectors.groupingBy工厂方法返回的收集器。

多级分组

要实现多级分组,可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,可以把内层groupingBy传递给外层groupingBy。

Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishByTypeCaloricLevel = menu.stream().collect(groupingBy(Dish::getType,groupingBy(dish -> {
   if (dish.getCalories() <= 400) return CaloricLevel.DIET;
   else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
   else return CaloricLevel.FAT;
})));

按子组收集数据

Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
Map<Dish.Type, Optional<Dish>> mostCaloricByType = menu.stream().collect(groupingBy(Dish::getType,maxBy(comparingInt(Dish::getCalories))));

把收集器的结果转换为另一种类型

Map<Dish.Type, Dish> mostCaloricByType = menu.stream().collect(groupingBy(Dish::getType,collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get)));

分区

分区是分组的特殊情况:由一个谓词作为分类函数,称分区函数。

Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetatrian));

分区的优势

分区的好处在于保留了分区函数返回true或false的两套流元素列表。

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

public boolean isPrime(int candidate) {
    int candidateRoot = (int) Math.sqrt((double) candidate);
    return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i ==0);
}
public Map<Boolean,List<Integer>> partitionPrimes(int n) {
return IntStream.rangeClosed(2,n).boxed().collect(partitioningBy(candidate -> isPrime(candidate)));
}

收集器接口

可以为Collector接口提供自己的实现,从而自由地创建自定义规约操作。

public interface Collector<T,A,R> {
   Supplier<A> supplier();
   BiConsumer<A,T> accumulator();
   Function<A, R> finisher();
   BinaryOperator<A> combiner();
   Set<Characteristics> characteristics();
}

理解Collector接口声明的方法

1、建立新的结果容器:supplier方法 例如在ToListCollector中,supplier返回一个空的List,如下所示:

public Supplier<List<T>> supplier() {
     return () -> new ArrayList<T>();
}

2、将元素添加到结果容器:accmulator方法 accmulator方法会返回执行规约操作的函数。对于ToListCollector:

public BiConsumer<List<T>,T> accmulator() {
   return (list, item) -> list.add(item);
}

3、对结果容器应用最终转换:finisher方法 在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。对于ToListCollector:

public Function<List<T>, List<T>> finisher() {
    return Function.identity();
}

4、合并两个结果容器:combiner方法 combiner方法会返回一个供规约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分规约所得的累加器要如何合并。对于toList:

public BinaryOperator<List<T>> combiner() {
 return (list1, list2) -> {
   list1.addAll(list2);
   return list1;
 }
}

5、characteristics方法 characteristics会返回一个不可变的Characteristics集合,定义了收集器的行为,包含三个项目的枚举。 UNORDERED——规约结果不受流中项目的遍历和累计顺序的影响。 CONCURRENT——accumulator函数可以从多个线程同时调用,且该收集器可以并行规约。 IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。

全部融合到一起

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
  @Override
  public Supplier<List<T>> supplier() {
    return ArrayList::new;
  }
  
  @Override
  public BiConsumer<List<T>, T> accumulator() {
    return List::add;
  }
  
  @Override
  public Function<List<T>, List<T>> finisher() {
    return Function.indentity();
  }
  
  @Override
  public BinaryOperator<List<T>> combiner() {
    return (list1, list2) -> {
      list1.addAll(list2);
      return list1;
    }
  }
  
  @Override
  public Set<Characteristics> characteristics() {
    return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
  }
}

开发你自己的收集器以获得更好的性能

用Collectors类提供的一个方便的工厂方法创建了一个收集器,将前n个自然数划分为质数和非质数。

仅用质数做除数

只需要在质数大于被侧数平方根的时候捅下来。

比较收集器的性能

通过把实现的三个函数传给collect方法

public Map<Boolean, List<Integer>> partitionPrimesWithCustomCollector(int n){
  IntStream.rangeClosed(2, n).boxed().collect(
   () -> new HashMap<Boolean, List<Integer>> () {{
   put(true, new ArrayList<Integer>());
   put(false, new ArrayList<Integer>());
   }},
   (acc, candidate) -> {
     acc.get(isPrime(acc.get(true), candidate)).add(candidate);
   },
   (map1, map2) -> {
    map1.get(true).addAll(map2.get(true));
    map2.get(false).addAll(map2.get(false));
   });
}

小结

以下是你应从本章中学到的关键概念。

collect是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称为收集器)。

预定义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值。这些收集器总结在表6-1中。

预定义收集器可以用groupingBy对流中元素进行分组,或用partitioningBy进行分区。

收集器可以高效地复合起来,进行多级分组、分区和归约。

可以实现Collector接口中定义的方法来开发你自己的收集器。