我们在开发的过程中会大量的使用集合,集合可以将数据进行分组,处理,好多的处理数据的业务逻辑类似于数据库的操作,比如说对一系列的实体根据它其中的某个属性来分组,筛选,像这样的操作,数据库是允许你声明式的指定这些操作的。比如说:
SELECT name FROM apple WHERE weight < 400;
这样的业务逻辑,我们之前的代码实现都是for循环里面,填上一大堆的if判断,新建的临时变量,占用的代码空间很大,而且可读性也不好。比如下面的代码,在处理集合中的数据的时候,还产生了一个一次性的中间容器,代码中的appleIdList
。
List<Apple> appleList = new ArrayList<>();
List<Apple> wantedAppleList = new ArrayList<>();
for (Apple app : appleList) {
if (app.getWeight() < 400) { //筛选
wantedAppleList.add(app);
}
}
Collections.sort(wantedAppleList, new Comparator<Apple>() {
public int compare(Apple d1, Apple d2) { //排序
return Long.compare(d1.getWeight(), d2.getWeight());
}
});
List<Long> appleIdList = new ArrayList<>();
for (Apple d : wantedAppleList) {
appleIdList.add(d.getId()); //获取实体id
}
在java8之后,这样的语句可以不让它出现了,你不需要担心怎么去显式的实现如何筛选,你只需要说明你想要什么就行了。
如果要处理大量的元素,提高性能,你需要并行处理,利用多核架构,但是写并行代码更复杂,而调试起来也比较难受。
比如说,使用synchronized
来编写代码,这个代码是迫使代码顺序执行,也就违背了并行执行的初衷,这个在多核cpu上执行所需的成本会更大,多核的cpu的每个处理器内核都有自己独立高速缓存,加锁需要把这些同步缓存同步进行,需要在内核间进行缓慢的缓存一致性协议通信。
痛点说完了,接下来我们说下java8中的流是怎么使用的。
java8中的流
java8中的集合支持一个新的Stream方法,它会返回一个流,到底什么是流呢?
流:从支持数据处理的源生成的元素序列。让我们来咬文嚼字的来分别解释下,
-
元素序列:和集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值,集合是一种数据结构,它的目的是存储和访问,但是流的目的是表达计算。
-
源:当然就是提供数据的源头,大部分是集合、数组,由有序列表的产生的流顺序也是一致的。
-
数据处理操作:流的数据处理功能非常类似于数据库的操作,就是表达出来你要怎么处理。
流也有两个重要的特点:
-
流水线:Stream 中的很多操作会返回一个流,这样操作就可以链接起来,形成一个大的流水线。
-
内部迭代:与集合本身的显式迭代不同,Stream流的操作都是在背后进行的。
针对开头的代码,如果是java8的方式我们应该怎么写呢?
//如果是多核架构的话,可以将stream()换成parallelStream()
List<Long> appleIdList = appleList
.stream()
// .parallelStream() 并行处理
.filter(apple -> apple.getWeight() < 400)
.sorted(Comparator.comparing(Apple::getWeight))
.map(Apple::getId)
.collect(Collectors.toList());
经过对比,思考,我们可以发现,后者的代码是以声明的方式写的,就是陈述了你想要做什么,而不是一大堆的if、for
的去实现。这样我们再遇到别的需求的时候,不用再去复制代码了,你只要再按照这样的方式去陈述下你想要的就可以了,你可以把几个简单操作链接起来,来形成一个流水线,就表达复杂的数据处理。
集合和流的内在区别
那么集合和流的内在区别是什么呢?
-
比较粗略的说,两者的主要区别就是在于什么时间进行计算。
集合是一个内存中的数据结构,它存储包含着所有的值,每一个元素都是存放在内存里的,元素的计算结果才能成为集合的一部分。
而流呢,是概念上的固定的数据结构,元素都是按需计算的。从另一个角度来说,流就是延迟创建的集合,只要在需要的时候才会计算值,得到结果。套用管理学上的话:需求驱动,实时创造。
举一个例子:用浏览器进行搜索,当你输入一个关键字的时候,Google不会在所有的匹配结果都出来,所有的图片和都下载好之后才返回给你,而是首先给你10个或是20个,当你点击下一页的时候再给你接下来的10个,20个。这也就是只有在需要的时候才会去计算,好像有点类似于懒加载的意思。
有一点流和迭代器比较类似,就是流只能遍历一次,如果你还想在处理一遍,就只能从原始的数据源那里重新生成一个流来遍历(当然了,这里说的集合,不是I/O流)。 -
另一个关键的区别就是两者的遍历数据的方式。
Collection接口需要用户去做迭代,for-each,就是外部迭代,去显式的取出每个元素,去处理。而Stream使用的是内部迭代它把迭代已经做了,还把得到的流存储起来。内部迭代的时候,项目可以透明的并行处理,或者是用更好的顺序去处理,Stream库的内部迭代可以自己去选择一种适合你硬件的数据表示和并行实现。
流的操作:我们再来看下上面的代码:filter、sorted、map流水线式的称为中间操作。collect触发流水线操作的是终端操作。
流的使用
流的使用包括三件事:
-
数据源,集合
-
中间操作,流水线
-
终端操作,执行流水线,生成结果
其实流水线的背后理念类似于构建器模式,构建器模式就是用来设置一套配置,也就是这里的中间操作,接着调用built方法,也就是这里的终端操作。关于设计模式,这里就不细说了,以后也会专门的说下各个设计模式,各位小伙伴不要捉急。
筛选
filter:筛选出符合条件的
distinct:去除重复
limit:返回一个不超过给定长度的流,截短
skip:跳过给定长度,如果超过总量,返回空
List<Apple> red = appleList.stream().filter(apple -> apple.getColor().equals("red")).distinct().limit(3).collect(Collectors.toList());
map:对流的元素应用函数,接受一个函数作为参数,并且会把这个函数应用到每一个元素上,并映射到一个新的元素。
List<String> appleNameList = appleList.stream().map(Apple::getName).collect(Collectors.toList());
有的时候也会有这样情况,在使用map操作之后,会产生一个集合或者是数组,而你需要把所有的集合合并为一个集合,这被叫做流的扁平化,接着上代码:
List<String> collect = appleList.stream().map(Apple::getName).map(word -> word.split(" ")).flatMap(Arrays::stream).distinct().collect(Collectors.toList());
flatMap()方法让你把流中的每一个值都换成另一个流,然后把所有的流连接起来,成为一个流。
查找和匹配
anyMatch:流中是否有一个元素符合
allMatch:流中元素是否全部符合
noneMatch:流中无元素符合条件
findAny:查找当前流中的任意元素
Optional<String> any = appleList.stream().map(Apple::getName).map(word -> word.split("")).flatMap(Arrays::stream).distinct().findAny();
Optional是一个容器,代表一个值存在值或不存在,这样就避免出现null了。可以用isPresent()方法判断这个容器是否有值。
归约:
//源码中的reduce
T reduce(T identity, BinaryOperator<T> accumulator);
Integer allSum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce()方法有两个参数,总和变量的初始值,上面的0就是,后面的Lambda是加和的操作。你也可以操作相乘,Lambda反复的结合每个元素,一直到流被规约成一个值。
java8中Integer类现在有了一个静态的sum方法来求和,你还可以这么写:
Integer allInteger = numbers.stream().reduce(0, Integer::sum);
关于reduce,它还有一个重载的变体,看下面,没有初始值,返回一个Optional对象,你们知道为什么会是Optional吗?因为没有初始值,加和操作可能不会得到值。
Optional<Integer> result = numbers.stream().reduce(Integer::sum);
Optional<Integer> maxResult = numbers.stream().reduce(Integer::max);
Optional<Integer> minResult = numbers.stream().reduce(Integer::min);
flatMap
首先抛出一个问题,给你一张单词表,里面会有几个单词,现在的需求就是你把这些单词用到的字母找到,当然了,不包括重复的。
首先,你可能会这么做,
Arrays.stream(words)
.map(word ->word.split(""))
.distinct()
.collect(Collectors.toList());
这样你可能觉得是对的,但是当你运行之后,你才会发现,并不是你想要的那种,因为在map中传递的Lambda 为每个单词返回的是一个String []
,所有得到的集合是一个装满了 String[]
的。
java8中给我们提供了一个方法,就是 flatMap
,接下来,我们看
Arrays.stream(words)
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
使用这个方法的效果,就是可以将每个生成的流合并为一个流,也就是扁平化,在说明一下,flatMap方法让你把一个流中的每一个值都换成另一个流,然后把所有的流链接起来。
example
下面提一个问题:
给定两个数组列表,[1,2,3]和[3,4],要求返回一个所有的数组对,也就是说是这样的:[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)],相信不用java8的新特性,每个人都可以写的出来,用java8应该怎么写呢?
Integer[] arr1 = {1, 2, 3};
Integer[] arr2 = {3, 4};
Arrays.stream(arr1)
.flatMap(a -> Arrays.stream(arr2)
.map(b -> new Integer [] {a,b}))
.collect(Collectors.toList());
数值相关
java8还引入了三个原始类型特化流接口,IntStream、DoubleStream
和 LongStream
,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。常用方式是mapToInt、mapToDouble
和mapToLong
,执行这几个方法过后,他们会返回一个特化流,而不是Stream<T>
,而是IntStream,DoubleStream
等等。
数值范围
在和数值打交道的同时,我们通常会有数值范围的概念,比如你想生成从1到100之间的所有数字。java8引入了两个可以用于IntStream
和LongStream
的静态方法,range和rangeClosed,这两个方法都是接受第一个起始值,第二个是结束值,后者是包含结束值的。
long count = IntStream
.rangeClosed(1, 100).filter(i -> i % 2 == 0).count();
创建流的几种方式
接下来,总结下生成创建流的几种方式:
-
静态方法:
Stream.of
Stream<String> stream = Stream.of("Welcome ", "Follow ", "me ");
-
数组创建流
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
-
由文件生成流(
java.nio.file.Files
中的很多静态方法都会返回一个流)
long uniquesWords = 0l;
try (Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
uniquesWords = lines.flatMap(line -> Arrays.stream(line.split(" "))).distinct().count();
} catch (IOException e) {
e.printStackTrace();
}
-
由函数生成的流
Stream API
中提供了两个静态方法来从函数生成流,Stream.iterate
和Stream.generate
//迭代
Stream.iterate(0, n -> n +2).limit(10).forEach(System.out::println);
//生成
Stream.generate(Math::random).limit(5).forEach(System.out::println);
第一个iterate方法,接收一个初始值,在每次的基础上加2,这是形成了一个偶数流,这是一个无限流,所以我们可以用limit方法来限制流中元素的数量,用forEach来消费流,
第二个generate方法,它是接收一个Supplier<T>
来提供新的值。
收集器
在每个流的最后都会有一个终端操作,来将这个流装换为一个汇总的结果,其实传递给collect方法的参数是Collector
接口的一个实现,也就是给Stream中元素做汇总的方法,比如我们最常用的,toList,groupingBy。
toList也是我们经常使用的,就是将流中的元素汇总到一个集合中。
最大值、最小值
我们在获取集合中元素的最大,最小值的时候可以用Collectors.maxBy
和 Collectors.minBy
,我们需要自己创建一个Comparator
来作为参数
Comparator<Apple> tComparator = Comparator.comparingLong(Apple::getWeight);
Optional<Apple> collect = appleAllList.stream().max(tComparator);
求和操作
Long allWeight = appleAllList.stream().collect(summingLong(Apple::getWeight));
//平均数
Double collectAvg = appleAllList.stream().collect(averagingLong(Apple::getWeight));
在数据统计中,最大值,最小值,求和,平均值可能你都想得到,这样一次次的获取可能不太方便,想着一次操作就可以,你可以使用summarizingInt工厂方法返回的收集器。例如,通过一次summarizing操作你可以就得到以上的结果
LongSummaryStatistics summaryStatistics = appleAllList.stream().collect(summarizingLong(Apple::getWeight));
连接字符串
String nameStr = appleAllList.stream().map(Apple::getName).collect(joining());
//还有一个重载的版本
String nameStr = appleAllList.stream().map(Apple::getName).collect(joining(","));
以上的收集器都是可以用reducing工厂方法定义的归约过程的特殊情况而已
appleAllList.stream().map(Apple::getWeight).reduce((i, j) -> i + j);
appleAllList.stream().collect(reducing(0,(a1, a2) -> a1.getWeight() > a2.getWeight() ? a1 : a2));
reduce操作需要3个参数:起始值;转换的函数;一个BinaryOperator,将两个元素合成一个同类型的值。
分组
groupingBy 方法,简单理解来说,就是分组,返回的是一个Map。在它之前的分组是这样的:
List<Apple> appleAllList = new ArrayList<>();
Map<String, List<Apple>> appleByColorMap = new HashMap<>();
for (Apple apple : appleAllList) {
String color = apple.getColor();
if (appleByColorMap.get(color) == null) {
List<Apple> appList = new ArrayList<>();
appList.add(apple);
appleByColorMap.put(color, appList);
} else {
List<Apple> apples = appleByColorMap.get(color);
apples.add(apple);
}
}
在有了groupingBy 方法之后就是这样的,不言自明了吧
appleByColorMap = appleAllList.stream().collect(groupingBy(Apple::getColor));
在这里,你给groupingBy传递了一个方法,它是获取苹果的颜色的,这个叫分类函数。按照函数的结果来进行分组,函数的结果也就作为了key值。当然了,有时候业务需求上的分组并不向这样简单,这个时候我们也可以自定义分组函数:
list.stream().collect(groupingBy(apple -> {
if (apple.getWeight() > 300) {
return "Heavy";
} else if (apple.getWeight() < 200) {
return "little";
} else {
return "normal";
}
}));
ps:(这里需要强调一下,在开发过程中千万不要有上面代码的坏习惯,对于300,200这种要定义言简意赅的常量去表示,不要写这种带有魔法数字,我这里只是举个例子,不要关注错重点哦)
除了自定义分组,我们还会多级分组就是我们可以把一个内层groupingBy传递给外层groupingBy,就像下面这样:
Map<Integer, Map<String, List<Apple>>> appleMap = list.stream().collect(
groupingBy(Apple::getType,
groupingBy(dish -> {
if (dish.getWeight() <= 400)
return "DIET";
else if (dish.getWeight() <= 700)
return "NORMAL";
else return "FAT";
})));
当然了出来的结果也会是两层Map。
到了这里,第二个groupingBy是一个收集器,所以说我们也可以传别的收集器。
Map<Integer, Long> typeNum = list.stream().collect(
groupingBy(Apple::getType, counting()));