Java JDK1.8 核心特性详解------Stream(流)的使用

在前面的章节(Java JDK1.8 核心特性详解------Stream(流)的基本介绍),我讲述了流的基本介绍,包括流的一些特性以及简单的用法。在下面这篇文章里,我们会更加具体的学习如何使用Stream对数据进行筛选、映射、查找、匹配、归约等基本功能,以及如何用新的方式创建流


目录

流的基本使用

筛选

映射

查找和匹配

 数值流

构建流


流的基本使用

下面这个List是后面用来筛选的基本数据。由于《Java 8 实战》在讲这部分内容的时候有配图,有利于大家的理解,因此我在举案例的时候采用的是书中的内容,然后在代码下面会贴上代码的解析图(其实还有一部分是懒,不想画图(●'◡'●)),代码中会用到Lambda表达式和方法引用,两种表达式作用是一样的,怎么时候可以看以前的博客(JDK1.8 总目录里有)。

        List<Dish> menu = Arrays.asList(
                //参数分别为食物名称,是否是蔬菜,卡路里,食物类型
                //每个参数都有对应get方法
                new Dish("猪肉", false, 800, "肉"),
                new Dish("牛肉", false, 700, "肉"),
                new Dish("鸡肉", false, 400, "肉"),
                new Dish("虾", false, 300, "鱼"),
                new Dish("三文鱼", false, 450, "鱼"),
                new Dish("米饭", false, 350, "其他"),
                new Dish("蔬菜", true, 530, "其他"),
                new Dish("水果", true, 120, "其他"),
                new Dish("披萨", true, 550, "其他"));

筛选

用boolean筛选

Stream提供了 filter 方法,该方法接收一个boolean作为参数,并返回包含所有符合谓语的流。当我们要判断数据是否满足某个条件(某个字段为boolean,或者某个字段表达式结果为boolean)来筛选时,可以使用这个方法。:

        //筛选出菜单中是蔬菜的食物
        List<Dish> dishList = menu.stream().filter(dish->dish.isVegetarian()).collect(Collectors.toList());
        List<Dish> dishList = menu.stream().filter(Dish::isVegetarian).collect(Collectors.toList());

筛选各异的元素

Stream提供了 distinct 方法,该方法会将一个包含重复元素的流转化为每个元素数量都为1的流(根据元素的equal和hashCode方法实现,类似数据库的DISTINCT方法):

        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
        //先筛选出集合中是偶数的元素,然后对元素做去重操作
        numbers.stream().filter(i -> i%2==0).distinct().forEach(System.out::print);
-----------------------------------------------------------------------------------------
打印结果:24

截短流

流支持 limit(n) 方法(还是类似于数据库的LIMIT关键字),他会给我们返回一个限定数量n的流,当原先流元素数量少于n时,就返回原先流的全部元素。

        //筛选菜单中卡路里大于300的食物的前三道菜
        List<Dish> dishList = menu.stream().filter(d -> d.getCalories() > 300).limit(3).collect(Collectors.toList());

注意,之前说过,Stream是把一个元素按照中间操作执行完以后才去执行下一个元素的。从上面的图我们可以看到,当对元素5执行完以后,已经满足了limit(3)这个操作(已经获得三个元素)。那么,就会马上返回limit(3)产生的流(元素2,3,5组成的数据流),不再会对元素6进行操作。这个就是Stream的优化。

跳过元素

流还支持 skip(n) 方法,返回一个扔掉了前n个元素的流。当流中元素不足n个,则返回一个空流。

        //筛选菜单中卡路里大于300的食物的菜,并跳过前两道
       List<Dish> dishList = menu.stream().filter(d -> d.getCalories() > 300).skip(2).collect(Collectors.toList());

表中可以看出,过滤以后有元素1、2、3、4满足要求,但是跳过了元素1、2,只取了后面的3、4。如果筛选过后只有元素1、2,那么最后会返回一个数量为0的空流。

映射

将一个类型元素转为另一个类型的元素

  流支持map方法,它接受一个函数作为参数。然后将函数应用到每个元素,将元素转为另一个类型,并将新的类型的元素转成一个新的流返回。

        //将获取菜单的食物名,相当于传入的是Dish类型的元素,然后返回String类型的参数
        List<String> stringList = menu.stream().map(dish -> dish.getName()).collect(Collectors.toList());
        List<String> stringList = menu.stream().map(Dish::getName).collect(Collectors.toList());

因为getName返回的是String类型的元素,因此,map返回的流也变成了Stream<String>类型。

流的扁平化

上面的map方法可以帮我们把流中的某一个元素转成另一个类型的不同元素,但是不能把流中的所有元素转成另一个类型的某一个元素。举个例子,我们想得到hello world这个两个单词中有哪些不一样的字母,我们可能会这样写。

        Arrays.asList("hello", "word").stream().map(s -> s.split("")).distinct().collect(Collectors.toList());

 没错,这样写是错的。map返回的是Stream<String[]>,就像下面图所示。

遇到这种问题,我们可以是使用 flatMap 来解决这个问题。

//Arrays有一个静态方法Arrays.stream()可以将数组[T]转成Stream<T>。
List<String> collect = Arrays.asList("hello", "word").stream().map(s -> s.split("")).flatMap(Arrays::stream).distinct()
                .collect(Collectors.toList());

 我们可以这样理解,flatMap 的作用是把一种流转成另一种流,并将转换后的流合并成一个流,还是不太清楚的可以对比上面和下面的代码。

//这个方法跟上面的方法是等价的,flatMap把Stream<Stream<String>>流转成了Stream<Integer>流
Stream<Stream<String>> collect1 = Arrays.asList("hello", "word").stream().map(s -> s.split("")).map(Arrays::stream).distinct();
Stream<String> stringStream = collect1.flatMap(a -> a);

查找和匹配

匹配

Stream提供了多种根据谓语(表达式为boolean)来匹配的方法:allMatchanyMatchnoneMatch。分别对应是否匹配所有元素是否匹配至少一个元素是否没有任何元素匹配,这些方法返回的也是boolean。

        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 4, 2);
        if (numbers.stream().allMatch(i -> {
            System.out.print(i);
            return i < 4;
        })) {
            System.out.println("");
            System.out.println("所有的元素都小于4");
        } else {
            System.out.println("");
            System.out.println("存在元素不小于4");
        }

        if (numbers.stream().anyMatch(i -> {
            System.out.print(i);
            return i > 2;
        })) {
            System.out.println("");
            System.out.println("最少有一个元素大于2");
        }

        if (numbers.stream().noneMatch(i -> {
            System.out.print(i);
            return i > 3;
        })) {
            System.out.println("");
            System.out.println("没有一个元素都大于3");
        } else {
            System.out.println("");
            System.out.println("存在一个元素大于3");
        }
-------------------------------------------------------------------------------
运行结果:
121334
存在元素不小于4
1213
最少有一个元素大于2
121334
存在一个元素大于3

大家可以通过debug的方式感受一下运行方式。这三个操作都用到了短路(类似 || 和 &&),当有一个条件不满足条件时,就直接返回false。

查找

Stream存在两个查找方法  findFirst() findAny() ,这两个方法分别是返回流中的第一个元素和流中的任意一个元素,可以配合其他流来使用。这两个方法会返回Optional<T>类型的结果。下面举个例子 :
 

        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 4, 2);
        Optional<Integer> first = numbers.stream().filter(i->i>2).findFirst();
        System.out.println(first.get());
        Optional<Integer> any = numbers.stream().filter(i->i>2).findAny();
        System.out.println(any.get());
-------------------------------------------------------------------------------
运行结果:
3
3

Optional<T>是JDK8的一个新的容器类,代表一个值存在或不存在,后面会讲这个内容(虽然我觉得这个类没什么用)。你们可能会奇怪  findFirst() findAny() 很像,有什么区别。原因是并行,我们之前说过,用流可以很方便的使用并行,当我们对有顺序的流执行并行处理时,如果我们想在并行的时候按照流的顺序返回第一个元素,那使用对并行限制更大一点的findFirst()。如果你只要返回一个元素,那就可以使用findAny(),他可能会返回顺序流中第二或者第三个满足要求的元素。

归约

之前的方法中,流之前的元素是没有互动的,例如加、减、乘、除、比较等。但是,往往现实中我们需要对集合中的元素进行组合。举个栗子:我们要对一个流里的所有元素求总和,在以前我们可能使用for循环来实现。在JDK1.8中,Stream提供了reduce 操作来表达更复杂的查询,例如对int集合里的元素求和,或者查询int集合中最大的一个数。这些操作要把流中的所有元素反复结合起来。这类操作被归类为 归约操作 。下面举个例子:

        //计算数组的和
        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 4, 2);
        //传统求和方式
        int s = 0;
        for (int i = 0; i < numbers.size(); i++) {
            s += numbers.get(i);
        }
        //使用流求和
        Integer sum = numbers.stream().reduce(0, (a, b) -> a + b);
        //与上面的等价
        Integer sum2 = numbers.stream().reduce(0, Integer::sum);

我们可以看到,使用流求和方便了很多。reduce(i,函数式接口 接收两个参数,一个i,就是总变量的初始值,一个是函数式接口的实现类,一般使用Lambda表达式。我们通过图来演示上面流的过程

reduce方法第一步会将初始化值 i 当成a,然后从流中获取第一个元素,当成b。之后将a和b进行操作(这里的操作是a+b)后的值重新指定成a,再去流中获取一个值当成b,就这样一直计算直到最后。同样的,我们可以用reduce来获取一个数组中的最大值。

        Optional<Integer> max = numbers.stream().reduce(Integer::max);

下面这个表是到目前为止我们提到过的Stream提供的方法:

 数值流

我们之前用reduce对数据进行归约(Integer sum2 = numbers.stream().reduce(0, Integer::sum);),但是,这个代码存在一个问题,这里暗含了拆箱的成本,每个Integer都会被拆箱成一个基本数据类型。Stream给我们提供了解决方法。

原始类型特化流

Java 8 提供了三个接口来帮助我们处理装箱,拆箱的成本,分别是:IntStream,DoubleStream和LongStream。这三个接口可以将流中的元素特化成基本数据类型,也可以把基本数据类型装箱为包装数据类型,使用方法也基本相同。

映射到数值流:

将流转化为特殊版本版本的常用方法是 mapToInt ,mapToDouble ,mapToLong  这三个方法会将Stream<T>流转成一个特定的流。举个例子:

        //计算卡路里总和
        int sum = menu.stream()
                      //这里返回的是一个IntStream,而不是Stream<Integer>
                      .mapToInt(Dish::getCalories)
                      .sum();

在上面这段代码中 ,我们将返回的数据转化成IntStream,并且调用IntStream接口的sum方法,求出卡路里的总和。除此之外,IntStream还支持max ,mix,average等方法。

转换回封装对象:

 如果我们要将特殊流转换回普通的流,可以使用boxed方法。

        Stream<Integer> boxed = intStream.boxed();

生成数据范围

在平时,我们可能需要生成一个范围内的数字,例如,想要生成1-100的数字。Java  8 提供了两个可以用于IntStream和LongStream的静态方法:range和rangeClosed。两个方法都是第一个参数接受起始值,第二个接受结束值。range不包括结束值,rangeClosed包含结束值。这两种方式生成的流都为原始数据类型,不存在拆箱和装箱的成本。举个例子

        //count=50,包含100
        long count = IntStream.rangeClosed(1, 100).filter(i -> i % 2 == 0).count();
        //count2=49,不包含100
        long count2 = IntStream.range(1, 100).filter(i -> i % 2 == 0).count();

构建流

之前一般是使用stream的方法生成流,接下来将介绍几种另外的生成流的方法。

由值创建流

可以使用Stream.of显性的创建一个流。

        Stream<String> stream = Stream.of("Java 8", "JDK 1.8", "Stream");

由数组创建流 

可以使用静态方法Arrays.stream从数组创建一个流。

        int[] number = {1, 2, 3, 4, 5};
        IntStream stream1 = Arrays.stream(number);

由文件生成流 

java.nio.file.Files有很多静态方法都能返回一个流,例如,Files.lines会返回给定文件中的一行。

        Stream<String> lines = Files.lines(Paths.get("data.txt"));

 由函数生成流:创建无限流

Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate,这两种方式产生的流都是包装类型,在使用时可能会包含拆箱和装箱的成本,很大程度上会影响程序的效率。

迭代

iterate接受一个初始值,还有一个依次应用在每个产生的新值上的Lambda,这有点类似reduce方法,但是reduce是对初始化值来回操作,而iterate会在初始化值的基础上生成新的值。举个例子:

        //这会生成从1到100的100个值,sum值为5050
        int sum = Stream.iterate(1, n -> n + 1).limit(100).mapToInt(n -> n).sum();

 iterate一般配合limit使用,输出我们限定的数量,不然会一直生成下去,我们认为这个流是无界的。再举一个复杂的例子。使用iterate生成斐波那契数列。

//该语句会生成斐波那契数列的前20个元素
Stream.iterate(new int[]{0,1}, n -> new int[]{n[1],n[0]+n[1]}).limit(20).map(n->n[0]).forEach(System.out::println);

 生成

generate方法是根据Supplier<T>的Lambda表达式(()->T)创建无限流。下面这个例子,将通过Math::random生成一个数量为5的流,并打印。

Stream.generate(Math::random).limit(5).forEach(System.out::println);

更多与JDK1.8相关的文章请看:Java JDK1.8 核心特性详解----(总目录篇)

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值