Java8中流的归约

1. 收集器简介

收集器就是流的终端操作操作类,常用的就是collect操作(当然还有其他的),这个操作的参数就是一个收集器,他有很多官方定义好的用法,下文会一一介绍。

@收集器示意图

collect操作符一般都接受Collectors中定义的归约方法:

List transactions =
    transactionStream.collect(Collectors.toList());

2. 归约和汇总

归约为一个整数——流中所有元素个数:

long howManyDishes = menu.stream().collect(Collectors.counting());

还可以更简洁:

long howManyDishes = menu.stream().count();

查找流中最小值和最大值

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

汇总

对流中对象中的某个字段求和的归约操作叫做汇总操作

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
@summingInt的工作流程

汇总也指求某一字段的平均数:

double avgCalories =
    menu.stream().collect(averagingInt(Dish::getCalories));

求平均值、最大值等所有计算结果:

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

这个IntSummaryStatistics类的getter可以获取五个计算结果:

IntSummaryStatistics{count=9, sum=4300, min=120,
average=477.777778, max=800}

同样,有Int前缀的特化通常就有Long和Double特化。这里不赘述。

连接字符串

连接所有菜肴名字的joining操作:

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

如果对对象使用joining那么就会调用对象的toString

String shortMenu = menu.stream().collect(joining());

joining还可以添加分隔符:

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

广义的归约汇总

上述归约方法基本能满足程序员生理需求,但如果想定义更复杂的归约规则还是要用到reduce操作:

int totalCalories = menu.stream().collect(reducing(
    0, Dish::getCalories, (i, j) -> i + j));

同样reduce也有方便的重载:

Optional mostCalorieDish =
    menu.stream().collect(reducing(
    (d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));

它相当于:

reduce(0, Function.identity(), (Dish, Dish)-> Dish)

3. 分组

按类型将菜肴分组:

Map> dishesByType =
    menu.stream().collect(groupingBy(Dish::getType));

下面是输出:

{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza],
MEAT=[pork, beef, chicken]}
@分组流程图

分组不一定非要使用对象的属性,也可以使用如下这种lambda:

public enum CaloricLevel { DIET, NORMAL, FAT }

Map> dishesByCaloricLevel = menu.stream().collect(
    groupingBy(dish -> {
        if (dish.getCalories() <= 400) return CaloricLevel.DIET;
        else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
        else return CaloricLevel.FAT;
    } ));

多级分组

Map>> dishesByTypeCaloricLevel =
        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;
                        })
                )
        );
@多级映射和n维表是等价的

按子组收集数据

其实就是对每个分组进行归约:

Map typesCount = menu.stream().collect(
    groupingBy(Dish::getType, counting()));
// output: {MEAT=3, FISH=2, OTHER=4}

groupingBy(f)其实是groupingBy(f, toList())的简便写法,所以后续的归约对前文的归约方法都适用。

获取类菜中最高卡路里:

Map> mostCaloricByType =
        menu.stream()
                .collect(groupingBy(Dish::getType,
                        maxBy(comparingInt(Dish::getCalories))));

这里返回结果是一个Optional这其实并不太实用,所以Java8还有更简便的写法。

把收集器转换成另一种类型
Map mostCaloricByType =
        menu.stream()
                .collect(groupingBy(Dish::getType,
                        collectingAndThen(
                                maxBy(comparingInt(Dish::getCalories)),
                                Optional::get)));
分组还能干很多事

分组求和

Map totalCaloriesByType =
    menu.stream().collect(groupingBy(Dish::getType,
    summingInt(Dish::getCalories)));

4. 分区

分区是分组的一个特殊情况,只区分一判断式:

Map> partitionedMenu =
        menu.stream().collect(partitioningBy(Dish::isVegetarian));

// output: {false=[pork, beef, chicken, prawns, salmon],
// true=[french fries, rice, season fruit, pizza]}

分区的作用

上面的代码乍一看并没有什么鸟用,直接filter一下也可以,但分区毕竟也是一种分组,如果加入多级分组的分区就看起来有那么一点鸟用了:

Map>> vegetarianDishesByType =
        menu.stream().collect(
            partitioningBy(Dish::isVegetarian,
                groupingBy(Dish::getType)));

总之这就相当于SQL语句中的筛选条件,结合各种归约可以进行灵活应用。

Collectors类的静态工厂方法

工厂方法 返回类型 用途
toList List 把流中所有项目收集到一个 List
toSet Set 把流中所有项目收集到一个 Set,删除重复项
toCollection Collection 把流中所有项目收集到给定的供应源创建的集合
counting Long 计算流中元素的个数
summingInt Integer 对流中项目的一个整数属性求和
averagingInt Double 计算流中项目 Integer 属性的平均值
summarizingInt IntSummaryStatistics 收集关于流中项目 Integer 属性的统计值,例如最大、最小、总和与平均值
joining String 连接对流中每个项目调用 toString 方法所生成的字符串
maxBy Optional 一个包裹了流中按照给定比较器选出的最大元素的 Optional,或如果流为空则为 Optional.empty()
minBy Optional 一个包裹了流中按照给定比较器选出的最小元素的 Optional,或如果流为空则为 Optional.empty()
reducing 归约操作产生的类型 从一个作为累加器的初始值开始,利用 BinaryOperator 与流中的元素逐个结合,从而将流归约为单个值
collectingAndThen 转换函数返回的类型 包裹另一个收集器,对其结果应用转换函数
groupingBy Map> 根据项目的一个属性的值对流中的项目作问组,并将属性值作为结果 Map 的键
partitioningBy Map> 根据对流中每个项目应用谓词的结果来对项目进行分区

2. 收集器接口

收集器接口定义如下:

public interface Collector {
    Supplier supplier();
    BiConsumer accumulator();
    Function finisher();
    BinaryOperator combiner();
    Set characteristics();
}

三个泛型定义如下:

  • T是流中收集项目的泛型(一般就是Stream的尖括号里那个)
  • A是累加器类型,求和就是int,打包成集合就是List
  • R是收集操作最终返回的类型
    比如toList()对应的实现类ToListCollector的声明如下:
public class ToListCollector implements Collector, List>

理解Collector接口声明的方法

要想自定义一个Collector一般就是实现如下四个方法:

  1. 建立新的结果容器:supplier方法
  2. 将元素归约到第一步的结果容器:accumulator方法
  3. 将归约好的最终结果进行转换:finisher方法
  4. 合并两个结果容器:combiner方法
    @归约的逻辑步骤

@使用combiner方法来并行化归约过程

在Collector还有最后一个方法,characteristics。它用来定义收集器的行为,关于流是否可以并行归约,以及哪些可以优化的提示。Characteristics是一个包含三个项目的枚举:

  • UNORDERED:归约结果不受遍历顺序和累计顺序影响
  • CONCURRENT:在accumulator时是否可以多个线程同时调用。如果没有标为UNORDERED则只会在无序流上使用并行。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约。
  • IDENTITY_FINISH:这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查地转换为结果R是安全的。
成型后的ToListCollector
public class ToListCollector implements Collector, List> {
    @Override
    public Supplier> supplier() {
        return ArrayList::new;
    }
    @Override
    public BiConsumer, T> accumulator() {
        return List::add;
    }
    @Override
    public Function, List> finisher() {
        return Function.indentity();
    }
    @Override
    public BinaryOperator> combiner() {
        return (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        };
    }
    @Override
    public Set characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(
                IDENTITY_FINISH, CONCURRENT));
    }
}

一种不需要实现Collector接口的自定义收集

toList方法的另一种写法

List dishes = menuStream.collect(
        ArrayList::new,
        List::add,
        List::addAll);

这种方式其实很方便,但没那么易读,实现个Collector接口也容易扩展。

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

自定义Collector:

package com.suikajy.java8note.ch6_collect;

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;

import static java.util.stream.Collector.Characteristics.*;

public class PrimeCollector implements Collector>, Map>> {

    @Override
    public Supplier>> supplier() {
        return () -> {
            Map> map = new HashMap<>();
            map.put(true, new ArrayList<>());
            map.put(false, new ArrayList<>());
            return map;
        };
    }

    @Override
    public BiConsumer>, Integer> accumulator() {
        return (map, i) -> map
                .get(isPrime(map.get(true), i))
                .add(i);
    }

    @Override
    public BinaryOperator>> combiner() {
        return (map1, map2) -> {
            map1.get(true).addAll(map2.get(true));
            map2.get(false).addAll(map2.get(false));
            return map1;
        };
    }

    @Override
    public Function>, Map>> finisher() {
        return Function.identity();
    }

    @Override
    public Set characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
    }

    private boolean isPrime(List acc, Integer candidate) {
        int candidateRoot = (int) Math.sqrt(candidate);
        return takeWhile(acc, i -> i < candidateRoot)
                .stream()
                .noneMatch(i -> candidate % i == 0);

//        return acc.stream()
//                .filter(i -> i < candidateRoot)
//                .noneMatch(i -> candidate % i == 0);
    }

    // 使用takeWhile来加速filter。因为filter会对流中的每一个数字判断,但其实到了候选数字的根即可停止了。
    private static  List takeWhile(List list, Predicate p) {
        int i = 0;
        for (A item : list) {
            if (!p.test(item)) {
                return list.subList(0, i);
            }
            i++;
        }
        return list;
    }
}

主函数:

public static void main(String[] args) {
    //IntStream.rangeClosed(2, 100).filter(i -> {
    //    int iRoot = (int) Math.sqrt(i);
    //    return IntStream.rangeClosed(2, iRoot)
    //            .noneMatch(divider -> i % divider == 0);
    //}).forEach(System.out::println);

    IntStream.rangeClosed(2, 100).boxed()
            .collect(new PrimeCollector())
            .get(true)
            .forEach(System.out::println);
}

7. 小结

  • collect是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称为收集器)。
  • 预定义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值。
  • 预定义收集器可以用groupingBy对流中元素进行分组,或用partitioningBy进行分区。
  • 收集器可以高效地复合起来,进行多级分组、分区和归约。
  • 你可以实现Collector接口中定义的方法来开发你自己的收集器。

整理自《Java 8 实战》

你可能感兴趣的:(Java8中流的归约)