栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

java 8 新特性之二 —— 流

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

java 8 新特性之二 —— 流

一、流的引入

在引入流之前,先看看一段流的代码吧

List lowCaloricDishesName = menu.stream()
        .filter(d -> d.getCalories() < 400)
        .sorted(Comparator.comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(Collectors.toList());

 你能猜出这段代码做了几件事嘛?你可能根据函数名略能猜出一二,让我来告诉你吧

1.选出400卡路里以下的菜肴

2.按照卡路里排序

3.提取菜肴的名称

4.将所有名称保存在List中

完成四个步骤用一句代码完成,牛不牛?这就是java 8 新特性之一 ——流

二、流的简介

在《java 8 实战》对流有一个简短的定义:从支持数据处理操作的源生成的元素序列。

这句话可能对于刚接触流的小伙伴不知道他在说啥,没关系,学完流,在这几话一定会理解的。解释一下加粗的文字

源:流会使用一个提供数据的源,比如集合、数据和I/O资源

数据处理操作:流的数据处理操作功能支持类似于数据库的操作,以及函数式编程语言中的操作,比如filter、map、reduce、find、match、sort等,这些操作也可以并行执行

元素序列:就像集合,流也提供了一个接口,可以访问特定元素类型的一组有序值

拿前面的代码再解释一下:menu.stream(),由菜单得到一个流,数据源是菜肴列表,它给流提供了一个元素序列,接下来,对流应用进行一系列的数据操作:filter、sorted和map,这些操作返回的还是流,最后由collect处理,返回一个列表。

 三、流的使用

流的使用一般包括三件事:

(1)一个数据源(如集合)来执行一个查询

(2)一个中间操作链,形成一条流的流水线(filter,map,limit,sorted)

(3)一个终端操作,执行流水线,生成结果(forEach,count,collect)

注意:中间操作返回的还是流,终端操作返回的结果,不是流

 思考一个问题:终端操作可以操作多次吗?下面的代码可以执行成功吗?小伙伴试一试就知道啦

        List list = Arrays.asList("sacs", "scsac");
        Stream stream = list.stream();
        stream.forEach(System.out::println);
        stream.forEach(System.out::println);
3.1 筛选

流使用filter进行筛选操作,filter操作接收一个谓词(一个返回boolean的函数)作为参数,返回一个包括所有符合谓词的元素的流。举个栗子,给你一张菜单,筛选出所有的素菜。

List vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(toList());

筛选出所有的素菜,扔掉重复的素菜,这么办呢?(现实中不要这样做哦)

List vegetarianMenu = menu.stream().filter(Dish::isVegetarian).distinct().collect(toList());

不管你筛选出多少道菜,我只要前三道(欠揍)

List vegetarianMenu = menu.stream().filter(Dish::isVegetarian).
distinct().limit(3).collect(toList());

 注意:limit(n)方法,返回另一个不超过给定长度的流,此方法作用于有序流,则最多返回前n个元素,作用于无须流,结果不会以任何顺序排序

 不管你筛选出多少道菜,我要扔掉前两道菜(找打)

List vegetarianMenu = menu.stream().filter(Dish::isVegetarian).skip(2).collect(toList());

注意:skip()扔掉前n个元素的流,流中元素不足n个,则返回一个空流

3.2 映射

老板让你说出所有菜肴的名字,你能做到吗?必须做到

List dishNames = menu.stream().map(Dish::getName).collect(toList());

注意:这里写直接调用toList,是因为这句代码import static java.util.stream.Collectors.toList;

不是写错哦!

流的扁平化 

直接说流的扁平化难以理解,先给大家抛出个问题,给你一个单词列表["Hello","World"],如何得到["H","e","l","o","w","r","d"]呢,不要使用笨方法哦,要求使用流

给你一种写法,小伙伴判断一下对不对呢?

List words = Arrays.asList("Hello", "World");
words.stream().map(word -> word.split("")).distinct().collect(toList());

上面的代码返回的类型是List,很显然不是我们想要的类型,我们需要的类型List,那么只需要将String[] 映射成String即可,java 8提供了一个函数flatMap可以完成,在使用flatMap之前,我们先来研究一下该方法

 Stream flatMap(Function> mapper);

flatMap,接收一个Function类型的参数,返回一个Stream;对于熟悉java 8 提供的函数式接口的小伙伴(如果不熟悉,可以看一下我的另一篇文章,对java的函数式接口的使用有详细的介绍),Function接口有一个R apply(T t)方法,将传入的T类型参数映射成R类型的参数。Stream stream = words.stream().map(word -> word.split(""));从这句代码,我们发现流里的内容是字符串数组,在使用flatmap之前,我们需要将流里的内容转换成一个流,也就是让字符串数组转换成流,需要使用Arrays.stream()。

List words = Arrays.asList("Hello", "World");
        List list = words.stream().map(word -> word.split("")).flatMap(Arrays::stream).distinct().collect(toList());

至此,我们解决了问题,flatMap本身有点难理解,需要小伙伴多多练习才能掌握,再给你们一个例子,给你一个二维数组,转换成一维数组,并排除重复元素,例如Integer[][] arr = {{1,2},{3,4},{2},{3,4,5,8}};结果为[1, 2, 3, 4, 5, 8],这里的Integer[][],不能写成int[][],因为泛型的缘故,可以先思考一下的,再看答案。

Integer[][] arr = {{1,2},{3,4},{2},{3,4,5,8}};
        List list = Arrays.stream(arr).flatMap(Arrays::stream).distinct().collect(toList());
3.3 查找和匹配 

给你一个菜单,让你看看是否有素菜可以选择(至少有一个素菜)

private static boolean isVegetarian() {
    return menu.stream().anyMatch(Dish::isVegetarian);
  }

注意:anyMatch方法返回一个boolean,是一个终端操作

给你一个菜单,看看是否所有的菜的热量都低于1000卡路里呢?

private static boolean isHealthyMenu() {
    return menu.stream().allMatch(d -> d.getCalories() < 1000);
  }

所有的菜的热量都低于1000卡路里,换句话说,就是不存在这样的菜,它的热量大于等于1000

private static boolean isHealthyMenu2() {
    return menu.stream().noneMatch(d -> d.getCalories() >= 1000);
  }

 注意:anyMatch、allMatch和noneMatch这三个操作都用到了所谓的短路,也包括后面介绍的findFirst和findAny,不用处理整个流,只要找到一个元素,就可以有结果了。

查找元素 

给你一个菜单,让你找到任意一个素菜,你可以使用findAny()

private static Optional findVegetarianDish() {
    return menu.stream().filter(Dish::isVegetarian).findAny();
  }

细心的读者可能会发现,为何返回一个Optional类型呢,既然是查找,就有可能找不到,java 8的库设计人员引入了Optional,就可以避免出现null问题了。关于Optional的使用有兴趣的读者可以先查一下相关资料,后面有时间我会更新一下的。

 查找第一个素菜,你可以使用findFirst()

private static Optional findFirstVegetarianDish() {
    return menu.stream().filter(Dish::isVegetarian).findFirst();
  }
3.4 归约

归约简单的说就是将流归约成一个值,比如你想计算菜单中的总卡路里,或者求菜单中卡路里最高的菜是哪一个,请看下面代码

int calories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);

完成归约主要使用reduce方法,reduce方法接收两个参数:一个初始值,一个是BinaryOperator来将两个元素结合起来产生一个新值,可以将Integer::sum等价于(a,b) -> a + b

List numbers = Arrays.asList(3, 4, 5, 1, 2);
Optional min = numbers.stream().reduce(Integer::min);
    min.ifPresent(System.out::println);

reduce也可以只传一个值,但是会返回一个Optional对象,因为不提供初始值,可能流中不含任何元素,就可能出现null的情况

 为了加深对reduce的使用,给你一个菜单,你如何计算出菜单中有多少个菜呢?

menu.stream().map(d -> 1).reduce(0,(a,b) -> a + b);

当然你也可以使用count直接计算出流中元素个数

menu.stream().count();
 3.5 数值流

在引入数值流之前,先来看前面的一个栗子,看这个栗子读者觉得是否有地方可以优化呢?

int calories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);

这段代码的问题,它暗含一个装箱成本,每一个Integer都必须拆箱成一个基本数据类型,再进行求和。java 8 引入了三个基本数据类型流化接口来解决这个问题:IntStream、DoubleStream和LongStream,它们可以将流的元素特化为int、double和long

基本类型特化 

将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapToLong,这些返回的流不是Stream ,而是一个XxxStream,因此前面代码可以优化为下面的例子

int sum = menu.stream().mapToInt(Dish::getCalories).sum();

注意:如果流为空时,sum则默认返回0

转回对象流 

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream stream = intStream.boxed();

使用box()方法可以将原始流转换成一般流

 给你一个菜单,你是否可以求出菜单中哪个菜含有的卡路里最大呢?

OptionalInt max = menu.stream().mapToInt(Dish::getCalories).max();

细心读者会发现返回的类型是OptionalInt,同样是为了避免流中元素为空的情况,java 8 提供了Optional版的基本类型特化,分别是OptionalInt、OptionalDouble和OptionalLong。你可以使用 int num = max.orElse(1);取得最大值,如果没有,就给默认值1,在这里为了方便后面的介绍,暂时讲一下Optional类几个常用方法

(1)isPresent()将在Optional包含值的时候返回true,否则返回false

(2)ifPresent(Consumer block)会在值存在的时候执行给定的代码块

(3)T  get()会在值存在时返回值,否则抛出一个NoSuchElement异常

(4)T  orElse(T other) 会在值存在时返回值,否则返回一个默认值

数值范围 

和数字打交道,避免不了需要使用数值范围,IntStream和LongStream提供了静态方法:range(start,end)和rangeClosed(start,end),range是不包含结束值,rangeClosed包含结束值。给你一个例子,计算一下1到100包含多少个偶数呢?

long evenNumbers = IntStream.rangeClosed(1, 100).count();
3.6 创建流

讲了这么多流的使用,好像忘了讲流如何创建,赶紧补上

1.由值创建流 

Stream stream = Stream.of("Java 8", "Lambdas", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);

2.由可空对象创建流

Stream emptyStream = Stream.empty();

3.由文件生成流

long uniqueWords = Files.lines(Paths.get("1.txt"), Charset.defaultCharset())
                .flatMap(line -> Arrays.stream(line.split(" ")))
                .distinct()
                .count();

File.lines,它会返回一个由指定文件中的各行构成的字符串流,因为流的源头是一个I/O资源,所以使用完之后必须关闭,而Stream接口通过实行AutoCloseable接口,资源的管理都由try代码块全权负责。

4.由函数生成流

Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate,这个两个函数可以生成无限流,需要使用limit(n)来限制,例如下面的栗子。

Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);

 iterate方法接受一个初始值,还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator类型),例子是打印所有正偶数的前十个,小伙伴不妨把limit(10)去掉,看会发生什么,接下来看看使用generate方法生成一个无限流。

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

generate接受一个Supplier类型的Lambda提供的新值。

注意:不了解java 8函数式接口的小伙伴,可能不懂我上面的例子,一定要看看我之前的文章关于Lambda表达式的使用。

3.7 用流收集数据 

归约和汇总

在前面的讲解中,有一个问题是让求流中的元素个数,在这里在提供一种方式求得

    //方式一
    menu.stream().collect(Collectors.counting());
    //方式二
    menu.stream().count()

求流中的最大值和最小值

 Optional dish = menu.stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));
dish.ifPresent(System.out::println);

求流中最大值使用Collectors.maxBy,求最小值使用Collectors.minBy,这两个函数接收一个Comparator类型的参数,我已经给出一个求最大值的例子,读者可以试试求最小值。

汇总 

    //求菜单卡路里总和
    menu.stream().collect(Collectors.summingInt(Dish::getCalories);
    //求菜单卡路里平均值
    menu.stream().collect(Collectors.averagingInt(Dish::getCalories));

Collectors.summingLong和Collectors.summingDouble,可以用于求和字段为long或者double的情况,你是否有过这样的想法:我不想单个求总和、平均值等,我想一句话把这些都得到,野心不小,满足你。

    IntSummaryStatistics statistics = 
    menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
    double average = statistics.getAverage();
    long count = statistics.getCount();
    int max = statistics.getMax();
    int min = statistics.getMin();
    long sum = statistics.getSum();

连接字符串

老板让你将菜单中所有菜肴的名称连接起来,你可以做到吗?请看例子。

menu.stream().map(Dish::getName).collect(Collectors.joining());

join内部使用StringBuilder来把生成的字符串拼接起来,上面的拼接是没有分隔符的,当然你也可以给join传个参数,像下面的例子

menu.stream().map(Dish::getName).collect(Collectors.joining(","))

广义的归约汇总

上面求最大值、平均值等都可以被一个函数替代:reducing,可以将上面求最大值、平均值看做

reducing的特殊化,你可能会问,既然有这么一个强大的方法,为何还会有前面的方法呢?主要是为了程序的可读性,下面给出求最大值、总和的一般化。

  //计算菜单总热量
    menu.stream().collect(Collectors.reducing(0,Dish::getCalories,(i,j) -> i + j));
    //计算菜单热量最高的菜
    menu.stream().collect(Collectors.reducing((d1,d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2))
            .ifPresent(System.out::println);

reducing需要三个参数

(1)第一个参数是归约操作的起始值,也是流中没有元素时的返回值

(2)第二个参数是将Function类型的参数,将一个值的类型映射成另一个类型

(3)第三个参数BinaryOperator,将两个项目累积成一个同类型的值

分组 

小伙伴在开发当中是否遇到对数据分组呢?比如说老板让你把菜单中的菜按照类型进行分类,将有肉的放在一组,有鱼的放在一组,其他的都放另一组,我们使用Collectors.groupingBy可以轻松完成这项任务

 private static Map> groupDishesByType() {
    return menu.stream().collect(Collectors.groupingBy(Dish::getType));
  }

groupingBy方法传一个Function类型的参数,分组操作的结果是一个Map,把分组函数返回的值作为映射的键,把流中所具有这个分类值的项目的列表作为对应的映射值。

分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,意味着分组Map的键类型是Boolean,于是它最多可以分为两组——true是一组,false是一组。你可能是一位素食主义者,那么需要将菜单按照素食和非素食分开。

 private static Map> partitionByVegeterian() {
    return menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
  }
四、总结

终于将流的使用写完了,但是流本身不止这些内容,还有并行流等,有兴趣的小伙伴可以看看《java 8 实战》,我介绍的内容对平时开发的都很有帮助,希望对读者有益处。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/571883.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号