【Java8新特性】4.Stream API

Java8开始,在java.util.stream包下引入了Stream流及IntStream、LongStream、DoubleStream等数值流,这些流式接口支持顺序和并行的聚合操作一系列元素。从原始数据的流动方向角度来分析,可以将流式操作分为三个阶段:构建流、中间操作、终端操作,这些阶段的操作在Java8提供了许多API。按照这个思路,整理并总结下Stream 相关的API。

目录

一、构建流

二、中间操作

三、终端操作

1、遍历操作

2、归集操作

3、组合操作

小结


一、构建流

比如Java集合进行流转换,转换方式有两种:.stream()表示串行流,.parallelStream()表示并行流,串行流是使用一个线程顺序的处理数据,而并行流则使用多个线程进行并发的处理数据操作。

Stream API提供的流构建方式有:

  • Stream接口的静态方式 —— empty()、concat()、of()、iterate()、generate()、Stream.builder()等;
  • Stream接口的中间操作方式 —— filter()、distinct()、limit()、skip()、sorted()、peek()、map()、mapToXxx()、flatMap()、flatMapToXxx()等;
  • 其他接口或类的方式 —— 如Arrays类的stream()、Files类lines()、Collection接口stream() 或parallelStream()等。
public class XxxTest {
    /** 测试 Stream接口的静态方式 和 其他接口或类的stream()方法 */
    public static void main(String[] args) throws IOException {
        // 返回一个空的顺序Stream 
        Stream<Object> emptyStream = Stream.empty(); 
        // 返回其元素是指定值的顺序排序Stream 
        Stream<String> stringStream = Stream.of("a", "b", "c"); 
        // 返回迭代流(起始元素为1,每次+2递增)
        Stream<Double> iterateStream = Stream.iterate(1.0, n -> n + 2.0);
        // 随机生成无限流
        Stream<Double> generateStream = Stream.generate(Math::random);
        // 创建一个懒惰连接的流,其元素是第一个流的所有元素,后跟第二个流的所有元素
        Stream<Double> concatStream = Stream.concat(iterateStream, generateStream);
        // 创建可变构建器,通过add()方法将元素加入Stream
        Stream.Builder<String> builder = Stream.builder();
        builder.add("weixiangxiang");
        builder.add("xxwei3");
        
        // 通过文件类生成Stream
        Stream<String> filesStream = Files.lines(Paths.get("data.txt")); 
        // 通过数组类生成Stream
        DoubleStream doubleStream = Arrays.stream(new double[]{3.14, 0.89, 7.56, 3.25}); 
        // 通过集合接口生成Stream
        List<String> list = Arrays.asList("习友路", "翡翠路", "习友路", "创新大道", "南北一号高架", "天柱路");
        Stream<String> listStream = list.stream(); // 顺序流
        Stream<String> listParallelStream= list.parallelStream(); // 并行流
    }
}

二、中间操作

在上面提及到了许多stream流的中间操作方法API,以示例中的List集合为例来 一 一 分析:

filter(Predicate<? super T> predicate) 

  • 含义:返回由与此给定谓词匹配的此流的元素组成的流,换句话说 —— 返回一个根据给定条件过滤的元素组成的流;
  • 示例:Stream<String> filterStream = listStream.filter(str -> str.contains("路")); 。

distinct()

  • 含义:返回由该流的不同元素(根据 Object.equals(Object) )组成的流,换句话说 —— 去重;
  • 示例:Stream<String> distinctStream = listStream.distinct();  。

limit(long maxSize) 与 skip(long n) 

  • 含义:简单的说 —— limit()选择前maxSize个元素,skip()跳过前n个元素,二者搭配常用于分页;
  • 示例:以项目中分页数据处理场景为例 。

sorted() 与  sorted(Comparator<? super T> comparator) 

  • 含义:无参,返回由此流的元素组成的流,根据自然顺序排序。有参,返回由该流的元素组成的流,根据提供的 Comparator进行排序;
  • 示例:Stream<String> sortedStream = llistStream.sorted();   。

peek(Consumer<? super T> action) 

  • 含义:返回由该流的元素组成的流,另外在从生成的流中消耗元素时对每个元素执行提供的操作,换句话说 —— 改变流中元素的状态 或维持元素的现状,与map()方法功能相似;
  • 示例:Stream<String> peekStream = listStream.peek(System.out::println);  // 比如只打印不做任何操作  。

map(Function<? super T,? extends R> mapper) 

  • 含义:返回由给定函数应用于此流的元素的结果组成的流,换句话说 —— 将流中的每一个元素T映射为元素R;
  • 示例:Stream<String> mapStream = listStream.map(str -> str.concat("-1"));  。

mapToXxx(ToXxxFunction<? super T,? extends R> mapper) 

  • 含义:返回一个 XxxStream ,其中包含将给定函数应用于此流的元素的结果, Xxx有int、long、double类型;
  • 用于映射不同类型元素。

flatMap(Function<? super T,? extends Stream<? extends R>> mapper) 

  • 含义:返回由通过将提供的映射函数应用于每个元素而产生的映射流的内容来替换该流的每个元素的结果的流,换句话说 —— 将流中的每一个元素T映射为一个流SR;
  • 示例:
public class XxxTest {
    public static void main(String[] args) throws IOException {
        // flatMap方式
        List<String> flatMapStream1 = listStream
            .flatMap(s ->Arrays.stream(s.split(";"))).collect(Collectors.toList());
        // map + flatMap方式
        List<String> flatMapStream2 = listStream
            .map(s -> s.split(";")).flatMap(Arrays::stream).collect(Collectors.toList());
    }
}

flatMapToXxx(Function<? super T,? extends Stream<? extends R>> mapper) 

  • 含义:返回一个 IntStream ,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果, Xxx有int、long、double类型;
  • 用于映射不同类型的流。

此外,需要注意还有两类方法:findXxx()、xxxMatch()

  • findXxx():寻找流中的元素,如果有多个相同的元素时,findAny返回一个不一定是第一个,findFirst寻找第一个;
  • xxxMatch():流中的元素是否匹配满足条件(all-所有元素,any-任意一个,none-无)。
public class XxxTest {
    public static void main(String[] args) throws IOException {
        // findXxx
        Optional<Integer> any = integerList.stream().filter(a -> a == 88).findAny();
        Optional<Integer> first = integerList.stream().filter(a -> a == 88).findFirst();
        logger.info("orElse方法赋默认值,避免使用get方法出现空指针异常:" + any.orElse(null));

        // xxxMatch 
        boolean allMatch = integerList.stream().allMatch(a -> a > 0);
        boolean anyMatch = integerList.stream().anyMatch(a -> a == 89);
        boolean noneMatch = integerList.stream().noneMatch(a -> a > 90);
    }
}

小结一下:

以上总结的API是生成Stream流的中间操作方法,而中间操作都有着一个共同点 —— 惰性!也就是说自Stream流构建起,经过中间操作对元素进行处理后,如果没有后续的终端操作的话,任何的中间操作都不会产生预期的结果,例如Stream<String> peekStream = listStream.peek(System.out::println);  ,这段代码不会按照预期打印流中的每个元素。

对于常见的数据处理:如求和、求均值、求最值、统计元素数量等,demo演示下Stream流是如何操作的:(暂不考虑使用Collectors接口的方法实现,Collectors接口方法的用法后面再说~)

public class XxxTest {
    public static void main(String[] args) throws IOException {
        List<Milk> milkList = new ArrayList<>();
        milkList.add(new Milk("0", "伊利老酸奶", "mn10001", 5.0));
        milkList.add(new Milk("1", "蒙牛纯牛奶", "mn10001", 4.2));
        milkList.add(new Milk("2", "蒙牛儿童奶", "mn10002", 6.0));
        milkList.add(new Milk("3", "伊利豆奶", "mn10002", 4.0));

        // count()
        logger.info(String.valueOf(milkList.stream().count()));
        // max()
        Optional<Milk> maxOptional = milkList.stream().max(Comparator.comparing(Milk::getMilkPrice));
        maxOptional.ifPresent(x -> System.out.println(x));
        // min()
        Optional<Milk> minOptional = milkList.stream().min(Comparator.comparingDouble(Milk::getMilkPrice));
        minOptional.ifPresent(x -> System.out.println(x.getMilkPrice()));
        // mapToXxx() + sum()
        double sum3 = milkList.stream().mapToDouble(Milk::getMilkPrice).sum();
        // map() + reduce(Xxx带有的sum())
        Double sum4 = milkList.stream().map(Milk::getMilkPrice).reduce(Double::sum).get();
        // mapToXxx() + average()
        OptionalDouble a2 = milkList.stream().mapToDouble(Milk::getMilkPrice).average();
        
        // XxxStream 用数值流求和、求最值,求均值等
        DoubleStream doubleStreams = milkList.stream().mapToDouble(Milk::getMilkPrice);
        logger.info("求和:" + doubleStreams.sum());        
        // 数值流只能使用一次,先求和再求其他的,会报错:stream has already been operated upon or closed
//        logger.info("求均值:" + doubleStreams.average());
//        logger.info("最大值:" + doubleStreams.max());
//        logger.info("最小值:" + doubleStreams.min());

        // 对应IntStream 与 LongStream 拥有 range 和 rangeClosed 方法用于数值范围处理
        int intSum = IntStream.range(1, 100).sum();
        double intAverage = IntStream.range(1, 100).average().getAsDouble();
    }
}

class Milk implements Serializable {
    public String milkId;
    public String milkName;
    public String milkNbr;
    public Double milkPrice;

    public Milk() {
    }

    public Milk(String milkId, String milkName, String milkNbr, Double milkPrice) {
        this.milkId = milkId;
        this.milkName = milkName;
        this.milkNbr = milkNbr;
        this.milkPrice = milkPrice;
    }

    /** 省略get/set/toString方法... */
}

三、终端操作

所谓终端操作就是流的结尾操作,常见的终端操作有:遍历操作foreach()、归集操作collect()、组合操作reduce()等。

1、遍历操作

  • forEach(Consumer<? super T> action)  :对此流的每个元素执行操作。
  • forEachOrdered(Consumer<? super T> action) :如果流具有定义的遇到顺序,则以流的遇到顺序对该流的每个元素执行操作。  

最常用的则是第一种操作,demo演示如下:

public class XxxTest {
    public static void main(String[] args) throws IOException {
        List<String> list = Arrays.asList("翡翠路", "习友路", "创新大道", "南北一号高架", "天柱路");
        Stream<String> listStream = list.stream();
        Stream<String> filterStream = listStream.filter(str -> str.contains("路"));
        filterStream.forEach(str -> System.out.println(str));

        // 使用链式
        list.stream().filter(str -> str.contains("路")).forEach(str -> System.out.println(str));
    }
}

2、归集操作

  • collect(Collector<? super T,A,R> collector):使用 Collectors接口对此流的元素执行归集操作。
  • collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner) :对此流的元素执行归集操作。

最常用的是第一种操作,其中Collectors接口静态方法有:(这些API非常重要!!!Xxx代表不同基本类型,有int、double、long)

  • 转集合或映射操作:toList()、toSet()、toMap()、toConcurrentMap()、toCollection()、collectingAndThen等;
  • 字符串连接操作:joining();
  • 分组操作:有简单分组、多级分组、分组求和 ,分组后返回Map类型,key为分组类别,groupingBy()、summingXxx()等;
  • 分区操作:按照true和false来分的,partitioningBy();
  • 统计流中元素数量:counting();
  • 求最值操作:maxBy()、minBy();
  • 求和操作:summingXxx()、summarizingXxx() + getSum() 等;
  • 求均值操作:averagingXxx();
  • 其他:mapping()、reduce()等

这些重要API的使用demo演示如下:

public class XxxTest {
    public static void main(String[] args) throws IOException {
        List<String> list = Arrays.asList("翡翠路", "习友路", "创新大道", "南北一号高架", "天柱路");
        // 默认转成ArrayList
        List<String> newList = list.stream().map(str -> str.concat("-1")).collect(Collectors.toList());
        // 默认转成hashSet
        Set<String> newSet = list.stream().map(str -> str.concat("-1")).collect(Collectors.toSet());

        List<Milk> milkList = new ArrayList<>();
        milkList.add(new Milk("0", "伊利老酸奶", "mn10001", 5.0));
        milkList.add(new Milk("1", "蒙牛纯牛奶", "mn10001", 4.2));
        milkList.add(new Milk("2", "蒙牛儿童奶", "mn10002", 6.0));
        milkList.add(new Milk("3", "伊利豆奶", "mn10002", 4.0));
//        milkList.add(new Milk("3", "伊利豆奶", "mn10002", 4.0)); //去重测试使用
        // 指定k-v
        Map<String, Milk> mapCollect1 = milkList.stream()
                .collect(Collectors.toConcurrentMap(Milk::getMilkId, Milk -> Milk)); 
        // 指定k-v
        Map<String, Milk> mapCollect2 = milkList.stream()
                .collect(Collectors.toMap(Milk::getMilkId, Function.identity()));
        // 为了防止key重复,传入一个合并的函数来解决key冲突问题
        Map<String, Milk> mapCollect3 = milkList.stream()
                .collect(Collectors.toMap(Milk::getMilkId, Function.identity(), (k1, k2) -> k1));
        // 拼接多个参数作为key
        Map<String, Milk> mapCollect4 = milkList.stream()
                .collect(Collectors.toMap(k -> k.getMilkId() + k.getMilkNbr(), Function.identity(), (k1, k2) -> k1));
        
        // 字符串连接 joining
        logger.info(milkList.stream().map(Milk::getMilkName).collect(Collectors.joining()));
        logger.info(milkList.stream().map(Milk::getMilkName).collect(Collectors.joining(",")));
        logger.info(milkList.stream().map(Milk::getMilkName).collect(Collectors.joining("和","勇哥,",",xxw")));

        // 分组 groupingBy
        Map<Double, List<Milk>> simpleMap = milkList.stream().collect(Collectors.groupingBy(Milk::getMilkPrice));
        simpleMap.forEach((k, v) -> logger.info(k + ":" + v));
        Map<String, Map<Double, List<Milk>>> moreMap = milkList.stream()
                .collect(Collectors.groupingBy(Milk::getMilkNbr, Collectors.groupingBy(Milk::getMilkPrice)));
        moreMap.forEach((k, v) -> logger.info(k + ":" + v));
        Map<String, Double> sumMap = milkList.stream()
                .collect(Collectors.groupingBy(Milk::getMilkNbr, Collectors.summingDouble(Milk::getMilkPrice)));
        sumMap.forEach((k, v) -> logger.info(k + ":" + v));

        // 分区:按照true和false来分的,如Milk按价格分区 ,支持多级分区  partitioningBy
        Map<Boolean, List<Milk>> booleanListMap = milkList.stream()
                .collect(Collectors.partitioningBy(p -> p.getMilkPrice() >= 5.0));
        //false:[...]  true:[...]
        booleanListMap.forEach((k, v) -> logger.info(k + ":" + v));
        Map<Boolean, Map<Boolean, List<Milk>>> moreBooleanListMap = milkList.stream()
                .collect(Collectors.partitioningBy(p -> p.getMilkPrice() >= 5.0, Collectors
                        .partitioningBy(x -> x.getMilkName().startsWith("蒙牛"))));
        //false:{false=[...], true=[...]}    true:{false=[...], true=[...]}
        moreBooleanListMap.forEach((k, v) -> logger.info(k + ":" + v));

        // 去重方法:id唯一+distinct()、id唯一+TreeSet
        List<String> distincts = milkList.stream().map(Milk::getMilkId).distinct().collect(Collectors.toList());
        List<Milk> sets = milkList.stream().collect(Collectors.collectingAndThen(Collectors
                .toCollection(() -> new TreeSet<>(Comparator.comparing(Milk::getMilkId))), ArrayList::new));
        sets.forEach(x -> logger.info(String.valueOf(x)));

        // counting
        logger.info(String.valueOf(milkList.stream().collect(Collectors.counting())));
        // summarizingXxx
        double sum1 = milkList.stream().collect(Collectors.summarizingDouble(Milk::getMilkPrice)).getSum();
        // summingXxx
        Double sum2 = milkList.stream().collect(Collectors.summingDouble(Milk::getMilkPrice));
        // averagingXxx
        Double a1 = milkList.stream().collect(Collectors.averagingDouble(Milk::getMilkPrice));
        // maxBy
        Optional<Milk> maxByOptional = milkList.stream()
                .collect(Collectors.maxBy(Comparator.comparing(Milk::getMilkPrice)));
        maxByOptional.ifPresent(System.out::println);
        // minBy
        Optional<Milk> minByOptional = milkList.stream()
                .collect(Collectors.minBy(Comparator.comparingDouble(Milk::getMilkPrice)));
        System.out.println(minByOptional.get().getMilkPrice());
    }
}

3、组合操作

  • reduce(BinaryOperator<T> accumulator) :
  • reduce(T identity, BinaryOperator<T> accumulator) 
  • reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner) 

组合操作可以简单理解为:对流中的元素按照一定规则进行组合,生成需要的组合结果,如使用reduce()将流元素组合起来进行求和,求最值,求平均等操作,同时需要注意串行计算和并行计算的不同,用demo演示说明:

public class XxxTest {
    public static void main(String[] args) throws IOException {
        List<Milk> milkList = new ArrayList<>();
        milkList.add(new Milk("0", "伊利老酸奶", "mn10001", 5.0));
        milkList.add(new Milk("1", "蒙牛纯牛奶", "mn10001", 4.2));
        milkList.add(new Milk("2", "蒙牛儿童奶", "mn10002", 6.0));
        milkList.add(new Milk("3", "伊利豆奶", "mn10002", 4.0));

        Optional<Double> reduce1 = milkList.stream().map(Milk::getMilkPrice).reduce((a, b) -> a + b);
        Optional<Double> reduce2 = milkList.stream().map(Milk::getMilkPrice).reduce(Double::max);
        logger.info("求和:" + reduce1.get() + ",最值为:" + reduce2.get()); //23.0   6.0

        Double reduce3 = milkList.stream().map(Milk::getMilkPrice).reduce(0.0, (a, b) -> a + b);
        Double reduce4 = milkList.parallelStream().map(Milk::getMilkPrice).reduce(0.0, (a, b) -> a + b);
        logger.info("指定初始值0,串行的求和:" + reduce3 + ",并行求和:" + reduce4); // 23.0  23.0

        Double reduce5 = milkList.stream().map(Milk::getMilkPrice).reduce(10.0, (a, b) -> a + b);
        Double reduce6 = milkList.parallelStream().map(Milk::getMilkPrice).reduce(10.0, (a, b) -> a + b);
        // 注意并行计算时,每个线程的初始累加值都是10,最后5个线程加出来的结果就是73.0
        logger.info("指定非0初始值,串行的求和:" + reduce5 + ",并行求和:" + reduce6); // 33.0  73.0

        Double reduce7 = milkList.stream().map(Milk::getMilkPrice).reduce(10.0, (a, b) -> a + b, Double::sum);
        Double reduce8 = milkList.parallelStream().map(Milk::getMilkPrice).reduce(10.0, (a, b) -> a + b, Double::sum);
        logger.info("指定非0初始值,串行的求和:" + reduce7 + ",并行求和:" + reduce8);// 33.0  73.0
    }
}

小结

至此,按照原始数据在Stream流中的流动方向整理和分析了如何构建流、如何进行相关的中间操作及如何进行相关的终端操作,在这些过程中涉及的API基本都是Java8新增的,学习和使用它们是Java开发必备的基础。当然,我还发现一个有趣的现象,比如进行常见的求值运算,Stream API提供的实现方式就有很多种,还有专门的数值流可以操作,里面或多或少存在一些细节问题(如Stream流可以反复使用而数值流只能使用一次、调用reduce()进行并行计算和串行计算得到的结果不同 等等),需要在实际中使用后才能感受得到。

经过这一通整理和分析花费了不少时间,但也加深了对Stream API的理解,还是那句话吧,不积硅步无以至千里,点滴付出终将有所收获,共同进步吧 ~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值