美文网首页
Java8中流的归约

Java8中流的归约

作者: suikaJY | 来源:发表于2018-11-13 17:31 被阅读0次

1. 收集器简介

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

@收集器示意图

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

List<Transaction> transactions =
    transactionStream.collect(Collectors.toList());

2. 归约和汇总

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

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

还可以更简洁:

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

查找流中最小值和最大值

Comparator<Dish> dishCaloriesComparator =
        Comparator.comparingInt(Dish::getCalories);
Optional<Dish> 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<Dish> mostCalorieDish =
    menu.stream().collect(reducing(
    (d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));

它相当于:

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

3. 分组

按类型将菜肴分组:

Map<Dish.Type, List<Dish>> 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<CaloricLevel, List<Dish>> 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<Dish.Type, Map<CaloricLevel, List<Dish>>> 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<Dish.Type, Long> typesCount = menu.stream().collect(
    groupingBy(Dish::getType, counting()));
// output: {MEAT=3, FISH=2, OTHER=4}

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

获取类菜中最高卡路里:

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

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

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

分组求和

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

4. 分区

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

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

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

分区的作用

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

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

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

Collectors类的静态工厂方法

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

2. 收集器接口

收集器接口定义如下:

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

三个泛型定义如下:

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

理解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<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));
    }
}

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

toList方法的另一种写法

List<Dish> 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<Integer, Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> {

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

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

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

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

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

    private boolean isPrime(List<Integer> 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 <A> List<A> takeWhile(List<A> list, Predicate<A> 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中流的归约

    1. 收集器简介 收集器就是流的终端操作操作类,常用的就是collect操作(当然还有其他的),这个操作的参数就是...

  • 句柄与移入-归约分析的关系

    相关概念 移入-归约分析关于该分析的介绍可以查看 移入-归约分析 句柄此概念在进行移入-归约分析时很重要,能否正确...

  • Stream终止操作

    归约 收集 分组

  • 移入——归约技术

    归约 定义:我们可以将自底向上语法分析过程看成是建一个串w“归约”慰问发开始符号的过程,在归约中,一个与某产生式体...

  • 编译原理:规范归约、算符优先归约 Syntax Analysis

    规范归约: 规范推导:最右推导 从起始符号开始逐步推出给定的字符串,每次拓展最右边的非终结符。 栗子: 规范归约:...

  • 可归约性

    可规约性: 一个基本方法,可用来证明问题是计算上不可解的。规约是将一个问题转化为另一个问题的方法,使得可以用第二个...

  • 函数式编程

    函数过程 过滤: filter() ---> 映射: map() ---> 归约: reduce() 在Pyth...

  • Java8 学习笔记

    @(in action系列)[java8, lambda, stream] Java8 学习 java8 能高效的...

  • 【r<-高级|聚类】层次聚类与划分聚类

    聚类分析是一种数据归约技术,旨在揭露一个数据集中观测值的子集。它可以把大量的观测值归约为若干类。这里的类被定义为若...

  • 《R语言实战》学习笔记 -- Charpter16 聚类分析

    本章代码聚类分析是一种数据归约技术,旨在揭露一个数据集中观测值的子集。它可以把大量的观测 值归约为若干个类。这里的...

网友评论

      本文标题:Java8中流的归约

      本文链接:https://www.haomeiwen.com/subject/rlmlfqtx.html