文章目录
从外部迭代到内部迭代
在操作集合时,通常的做法是在集合上进行迭代,处理返回的每一个返回的元素。
Device Entity:
@Data
public class Device {
private String name;//设备名称
private Double cpuUsed;//CPU使用率
}
问题:传统的for循环:求设备CPU使用率大于80%的设备个数:
int count = 0;
for (Device device : devices) {
if ((device.getCpuUsed()>0.8)){
count ++;
}
}
上述代码的缺陷:每次迭代集合时候,都需要些很多样代码,将for循环改成并行方式也比较麻烦,需要修改你每个for循环才能实现,for循环的代码也无法传达程序员的意图,模糊了代码的本意,程序员必须阅读整个循环体才能理解。(for嵌套循环体时,负担很重了)
使用迭代器改进上述的代码:
devices.stream().filter(device -> device.getCpuUsed() > 0.8).count();
两种迭代的比较:
for循环其实就是一个封装了迭代的语法糖,首先调用iterator方法产生一个iterator对象,进而控制整个迭代过程,这叫做外部迭代,迭代过程显示调用了Iterator对象的hasnext和next方法,它从本质上讲是一种串行化操作,for循环将行为和方法混为一谈。
后一种方法是内部迭代,首先调用stream方法,和调用iterator方法的作用一样,但是该方法返回的不是一个控制迭代的Iterator对象,而是返回内部迭代中的相应接口Stream。
Stream简介
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
惰性求值方法:所谓的中间操作,产生新的Stream配方,不产生集合的方法叫做惰性求值。
及早求值方法:最终操作得到结果,最终从Stream中产生值的方法叫做及早求值方法。
Stream流函数式编程其实就是形成一个惰性求值的链,最终用一个及早求值的方法返回想要的结果。
特性
- 不是数据结构,没有内部存储。
- 不支持索引访问。
- 延迟计算
- 支持并行
- 很容易生成数据或集合
- 支持过滤,查找,转换,汇总,聚合等操作。
Stream流的创建
- 通过数组,Stream.of()
- 通过集合
- 通过Stream.generate方法来创建
- 通过Stram.iterate方法
- 其他API
代码示例:
//通过数组,Stream.of()
String[] str = {"a","b","c"};
Stream<String> str1 = Stream.of(str);
//通过集合
List<String> strings = Arrays.asList("a", "b", "c");
Stream<String> stream = strings.stream();
//通过Stream.generate方法来创建
//这是一个无限流,通过这种方法创建在操作的时候最好加上limit进行限制
Stream<Integer> generate = Stream.generate(() -> -100);
generate.limit(10).forEach(System.out::println);
//通过Stram.iterate方法
Stream<Integer> iterate = Stream.iterate(30, x -> x +1);
iterate.limit(20).forEach(System.out::println);
//其他API
String str = "abc";
IntStream chars = str.chars();
chars.forEach(System.out::println);
执行顺序
Laziness(延迟加载)是中间操作(intermediate operations)的一个重要特性。如下面这个例子:中间操作(terminal operation)缺失,当执行这个代码片段的时候,并不会在控制台打印相应的内容,这是因为只有最终操作(terminal operation)存在的时候,中间操作(intermediate operations)才会执行。
下面代码不执行(没有最终操作):
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
});
加上最终操作,试想代码的结果?
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
})
.forEach(s -> System.out.println("forEach: " + s));
执行结果如下:
filter: d2
forEach: d2
filter: a2
forEach: a2
filter: b1
forEach: b1
filter: b3
forEach: b3
filter: c
forEach: c
是不是和你想的不一样?
我们可能想的是水平执行此流上的所有元素。但是实际上是每一个元素沿着链垂直移动,第一个字符串"d2"执行完filter和forEach后第二个元素"a2"才开始执行。这种沿着链垂直移动的行为可以降低每一个元素上进行操作的数量,如我们在下面的例子中所示:
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.anyMatch(s -> {
System.out.println("anyMatch: " + s);
return s.startsWith("A");
});
结果:
map: d2
anyMatch: D2
map: a2
anyMatch: A2
执行效率与steream执行链顺序的关系
下面的例子由两个中间操作(intermediate operations)map和filter以及一个最终操作(terminal operation)forEach构成,我们观察这些动作是如何执行的。
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A");
})
.forEach(s -> System.out.println("forEach: " + s));
map: d2
filter: D2
map: a2
filter: A2
forEach: A2
map: b1
filter: B1
map: b3
filter: B3
map: c
filter: C
你可能已经猜想到:map和filter操作被执行了5次,但是forEach操作只被执行了1次。我们可以通过修改操作的执行顺序(如:将filter操作移到操作链的头部),大幅度降低执行次数
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
filter: d2
filter: a2
map: a2
forEach: A2
filter: b1中间操作
filter: b3
filter: c
修改后map只被执行了1次,如果此时数据量比较大则操作管道的执行效率会有较大的提升,在处理复杂方法链的时候需要注意执行顺序对执行效率的影响。
常用的流操作
collect()
collect方法是一个及早求值方法,将stream流内通过种种操作之后的数据最终收集起来,产生一个指定的集合,list,set,或者map,是最常用的最终操作API之一。
List<Device> deviceList = devices.stream().filter(d -> d.getCpuUsed() > 0.7).collect(Collectors.toList());
System.out.println(deviceList);
map()
该方法会接受一个函数作为参数,这个函数会被应用到每个元素上,并将其映射成一个新的元素。就是根据指定函数获取流中得每个元素得数据并重新组合成一个新的元素。
devices.stream().map(Device::getCpuUsed).forEach(System.out::println);
filter()
该操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。说白了就是给一个条件,filter会根据这个条件截取流中得数据。
List<Device> list = devices.stream().filter(d -> d.getCpuUsed() > 0.8).collect(Collectors.toList());
flatmap()
该操作可以让你把一个流中的每个值都换成另一个流,然后把所有的流都链接起来成为一个流,即扁平化。
Integer[] nums1 = {1,2,3};
Integer[] nums2 = {4,5,6};
List<Integer[]> collect = Stream.of(nums1, nums2).collect(Collectors.toList());
System.out.println(collect);
//你不想得到List<Object<>>这样的数据结构或者更深的嵌套结构可以使用faltmap方法
//flatmap
List<Integer> collect1 = Stream.of(nums1, nums2).flatMap(Arrays::stream).collect(Collectors.toList());
System.out.println(collect1);
调用stream方法,将每个列表转成stream对象,其余部分交给flatmap方法来处理就行了。flatmap方法相关的函数接口和map方法的接口一样都是Function接口,知识返回的值是Stream类型。
max() 和min()
Stream上最常用的操作之一就是求最大值最小值问题,Stream API中的max和min方法就可以解决这一问题。
Device device = devices.stream().max(Comparator.comparing(Device::getCpuUsed)).get();
System.out.println(device);
为了获取CPU使用率的最大值的设备,需要给他传入一个Comparator对象,java8给我们提供一个新的静态方法comparing,你可以很方便的实现一个比较器,放在java8以前,我们需要比较两个对象属性的值。
max方法返回的是一个Optional对象,Optional对象也是java8新实现的一个对象,这里我只介绍Stream流,有时间后续再介绍。
reduce()
reduce可以从一组值中生成一个值,count,min,max其实都是reduce操作。
使用reduce求和:
Integer reduce = Stream.of(1, 2, 3, 4, 5).reduce(0, (acc, element) -> acc + element);
System.out.println(reduce);//15
用reduce方法实现求最大值:
Integer reduce1 = Stream.of(1, 2, 3, 4, 5).reduce(0, (a, b) -> a > b ? a : b);
System.out.println(reduce1);
distinct ()
该操作会返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流。
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
List<Integer> collect = numbers.stream().distinct().collect(Collectors.toList());
System.out.println("collect = " + collect);//collect = [1, 2, 3, 4]
}
skip()
该操作会生成舍弃前n个元素的流,若n值大于元素值,则返回空流。
List<Integer> integers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
List<Integer> collect = integers.stream().skip(3).collect(Collectors.toList());
System.out.println("collect = " + collect);//丢掉流中得前三个元素 collect = [3, 3, 2, 4]
limit()
该操作会生成限制元素的返回个数的流。
List<Integer> integers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
List<Integer> collect = integers.stream().limit(3).collect(Collectors.toList());
System.out.println("collect = " + collect);//截取流中得前三个元素 collect = [1, 2, 1]
关于Stream API,不做一一讲述,API参数大都是传入一个函数式接口,我们根据接口的抽象方法,来传入Lambda表达式即可。
思考:Stream既然很好的封装了内部实现的数据结构,用户可以很好的利用Stream接口来进行各种很方便的操作,那我们需要对外暴露List或者Set集合吗?会不会暴露stream接口是更好的选择呢???