在引入流之前,先看看一段流的代码吧
ListlowCaloricDishesName = 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)
注意:中间操作返回的还是流,终端操作返回的结果,不是流
思考一个问题:终端操作可以操作多次吗?下面的代码可以执行成功吗?小伙伴试一试就知道啦
List3.1 筛选list = Arrays.asList("sacs", "scsac"); Stream stream = list.stream(); stream.forEach(System.out::println); stream.forEach(System.out::println);
流使用filter进行筛选操作,filter操作接收一个谓词(一个返回boolean的函数)作为参数,返回一个包括所有符合谓词的元素的流。举个栗子,给你一张菜单,筛选出所有的素菜。
ListvegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(toList());
筛选出所有的素菜,扔掉重复的素菜,这么办呢?(现实中不要这样做哦)
ListvegetarianMenu = menu.stream().filter(Dish::isVegetarian).distinct().collect(toList());
不管你筛选出多少道菜,我只要前三道(欠揍)
ListvegetarianMenu = menu.stream().filter(Dish::isVegetarian). distinct().limit(3).collect(toList());
注意:limit(n)方法,返回另一个不超过给定长度的流,此方法作用于有序流,则最多返回前n个元素,作用于无须流,结果不会以任何顺序排序
不管你筛选出多少道菜,我要扔掉前两道菜(找打)
ListvegetarianMenu = menu.stream().filter(Dish::isVegetarian).skip(2).collect(toList());
3.2 映射注意:skip()扔掉前n个元素的流,流中元素不足n个,则返回一个空流
老板让你说出所有菜肴的名字,你能做到吗?必须做到
ListdishNames = menu.stream().map(Dish::getName).collect(toList());
注意:这里写直接调用toList,是因为这句代码import static java.util.stream.Collectors.toList;
不是写错哦!
流的扁平化
直接说流的扁平化难以理解,先给大家抛出个问题,给你一个单词列表["Hello","World"],如何得到["H","e","l","o","w","r","d"]呢,不要使用笨方法哦,要求使用流
给你一种写法,小伙伴判断一下对不对呢?
Listwords = Arrays.asList("Hello", "World"); words.stream().map(word -> word.split("")).distinct().collect(toList());
上面的代码返回的类型是List
,很显然不是我们想要的类型,我们需要的类型List ,那么只需要将String[] 映射成String即可,java 8提供了一个函数flatMap可以完成,在使用flatMap之前,我们先来研究一下该方法
Stream flatMap(Function super T, ? extends Stream extends R>> 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()。
Listwords = 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 OptionalfindVegetarianDish() { return menu.stream().filter(Dish::isVegetarian).findAny(); }
细心的读者可能会发现,为何返回一个Optional类型呢,既然是查找,就有可能找不到,java 8的库设计人员引入了Optional,就可以避免出现null问题了。关于Optional的使用有兴趣的读者可以先查一下相关资料,后面有时间我会更新一下的。
查找第一个素菜,你可以使用findFirst()
private static Optional3.4 归约findFirstVegetarianDish() { return menu.stream().filter(Dish::isVegetarian).findFirst(); }
归约简单的说就是将流归约成一个值,比如你想计算菜单中的总卡路里,或者求菜单中卡路里最高的菜是哪一个,请看下面代码
int calories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);
完成归约主要使用reduce方法,reduce方法接收两个参数:一个初始值,一个是BinaryOperator
来将两个元素结合起来产生一个新值,可以将Integer::sum等价于(a,b) -> a + b
Listnumbers = 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); Streamstream = 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.由值创建流
Streamstream = Stream.of("Java 8", "Lambdas", "In", "Action"); stream.map(String::toUpperCase).forEach(System.out::println);
2.由可空对象创建流
StreamemptyStream = 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);
3.7 用流收集数据generate接受一个Supplier
类型的Lambda提供的新值。 注意:不了解java 8函数式接口的小伙伴,可能不懂我上面的例子,一定要看看我之前的文章关于Lambda表达式的使用。
归约和汇总
在前面的讲解中,有一个问题是让求流中的元素个数,在这里在提供一种方式求得
//方式一
menu.stream().collect(Collectors.counting());
//方式二
menu.stream().count()
求流中的最大值和最小值
Optionaldish = 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 实战》,我介绍的内容对平时开发的都很有帮助,希望对读者有益处。



