函数式数据处理
流Stream
概念:从支持数据处理操作的源生成的元素序列
元素序列——比如集合,集合是数据结构,目的是以特定的时间/空间复杂度存储和访问元素,流会对集合进行计算获取元素。
源——流会使用一个提供数据的源,比如集合、数组或者输入/输出资源。 从有序集合生产流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、 map、 reduce、 find、 match、 sort等。流操作可以顺序执行,也可并行执行。
流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。
内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
中间操作——对流进行操作的时候,比如下面的示例filter,sorted返回的是另外一个流,那么就是中间操作,中间操作连接起来就是一个流水线。
终端操作——对流操作的时候想collect、forEache返回的是任何不适流的值,终端操作也是流水线的最后结果。
通过一个示例感受一下流的作用
I、java8以前可能会按照下面的方式处理需求
/**
* 获取红色富士苹果并且重量要降序排列,获取重量大于130的重量集合
*/
public static void main(String[] args) {
List<Apple> apples = getApples();
List<Apple> redApples = new ArrayList<>();
//筛选红色富士苹果
for(Apple apple:apples){
if (StringUtils.equals("red",apple.getColor()) && StringUtils.equals("富士",apple.getType())){
redApples.add(apple);
}
}
//按照重量降序排列
redApples.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o2.getWeight().compareTo(o1.getWeight());
}
});
//获取重量集合
List<Integer> weightList = new ArrayList<>();
for(Apple apple:redApples){
if(130<apple.getWeight()){
weightList.add(apple.getWeight());
}
}
System.out.println(weightList.toString());
}
private static List<Apple> getApples(){
List<Apple> appleList = new ArrayList<>();
appleList.add(new Apple("red",120,"富士"));
appleList.add(new Apple("red",150,"富士"));
appleList.add(new Apple("red",140,"富士"));
appleList.add(new Apple("green",125,"富士"));
appleList.add(new Apple("green",140,"富士"));
appleList.add(new Apple("green",125,"富士"));
appleList.add(new Apple("green",100,"青苹果"));
appleList.add(new Apple("green",140,"青苹果"));
appleList.add(new Apple("green",130,"青苹果"));
appleList.add(new Apple("red",110,"蛇果"));
appleList.add(new Apple("red",170,"蛇果"));
appleList.add(new Apple("red",130,"蛇果"));
appleList.add(new Apple("red",120,"蛇果"));
return appleList;
}
II、java8通过流的方式处理
public static void main(String[] args) {
List<Integer> collect = getApples().stream()
.filter(apple -> StringUtils.equals("red", apple.getColor()) && StringUtils.equals("富士", apple.getType()))
.sorted(Comparator.comparing(Apple::getWeight).reversed())
.map(Apple::getWeight)
.collect(Collectors.toList());
System.out.println(collect.toString());
}
输出结果都是:
[150, 140, 120]
当然上面的示例用到了一些流的操作方法,后面会一一讲到,现在可以体会到java8流的操作的简单、干净。
如果相对apples集合根据苹果类型分类,那么又会如何处理呢?
//java8以前
public static void main(String[] args) {
List<Apple> apples = getApples();
Map<String, List<Apple>> typeMap = new HashMap<>();
for(Apple apple:apples){
if (CollectionUtils.isEmpty(typeMap.get(apple.getType()))){
List<Apple> appleList = new ArrayList<>();
appleList.add(apple);
typeMap.put(apple.getType(),appleList);
}else {
typeMap.get(apple.getType()).add(apple);
}
}
System.out.println(typeMap);
}
//java8流操作
public static void main(String[] args) {
Map<String, List<Apple>> typeMap = getApples().stream()
.collect(Collectors.groupingBy(Apple::getType));
System.out.println(typeMap.toString());
}
注意:此处的流Stream只能遍历一次,遍历完就被消费了,与I/O流类似。
流的操作
筛选和切片
- 谓词筛选
方法filter(Predicate)
谓词:一个返回boolean的函数
示例:
List<Apple> appleList = apples.stream().filter((Apple a) -> StringUtils.equals("green", a.getColor())).collect(toList());
- 去重
方法distinct()
如果是引用对象需要去重需要重写hashCode和equals方法
示例:
List<Integer> list = Arrays.asList(1, 2, 4, 4, 5, 2, 1, 6);
List<Integer> collect = list.stream().distinct().collect(Collectors.toList());
System.out.println(collect.toString());
结果:
[1, 2, 4, 5, 6]
- 截取
方法limit(n)
该方法返回一个不超过给定长度的流。所需长度作为参数传递给limit,如果流是有序的,则最多返回前n个元素,如果是无序的,返回结果为任意排列
示例:
List<Integer> list = Arrays.asList(1, 2, 4, 4, 5, 2, 1, 6);
List<Integer> collect = list.stream().limit(3).collect(Collectors.toList());
System.out.println(collect);
结果:
[1, 2, 4]
- 跳过元素
方法skip(n)
返回一个从n+1到结尾的流,如果流不足n个,返回一个空流,limit(n)和skip(n)可以一起使用
示例:
List<Integer> list = Arrays.asList(1, 2, 4, 4, 5, 2, 1, 6);
List<Integer> collect = list.stream()
.skip(2)
.limit(3)
.collect(Collectors.toList());
System.out.println(collect);
结果:
[4, 4, 5]
映射
- 从对象中获取信息
方法map()
返回流的泛型根据map()的参数返回的结果决定
示例:
List<Apple> appleList = new ArrayList<>();
appleList.add(new Apple("red", 120, "富士"));
appleList.add(new Apple("red", 150, "富士"));
List<Integer> collect = appleList.stream()
.map(Apple::getWeight)
.collect(Collectors.toList());
System.out.println(collect);
结果:
[120, 150]
- 流的扁平化
方法flatmap()
将多个流合并成一个流就是流的扁平化
示例:
给定单词列表
["Hello","World"],你想要返回列表["H","e","l", "o","W","r","d"]
List<String> list = Arrays.asList("Hello","World");
Stream<String[]> stream = list.stream().map(arr -> arr.split(""));
List<String[]> collect = stream.distinct()
.collect(Collectors.toList());
System.out.println(collect.toString());
结果:
[[Ljava.lang.String;@685f4c2e, [Ljava.lang.String;@7daf6ecc]
结果并不是我们想要的,其实通过上面示例的返回泛型能看出来结果是一个数组对象集合,
map方法返回的不是Stream<String>而是Stream<String[]>
List<String> list = Arrays.asList("hello word");
Stream<String[]> stream = list.stream().map(arr -> arr.split(""));
List<String[]> collect = stream.distinct()
.collect(Collectors.toList());
for(String[] arr:collect){
for(String a:arr){
System.out.print(a + ",");
}
System.out.println(";");
}
结果:
H,e,l,l,o,;
W,o,r,l,d,;
通过视图可能更容易理解
想要得到一个字符流,而不是数组流,那么可以使用 Arrays.stream() 的方法可以接受一个数组并产生一个流
List<String> list = Arrays.asList("Hello","World");
Stream<String[]> stream = list.stream()
.map(arr -> arr.split(""));
Stream<Stream<String>> streamStream = stream.map(Arrays::stream);
List<Stream<String>> collect = streamStream.distinct()
.collect(Collectors.toList());
但是发现最终的结果仍然不是List<String>而是List<Stream<String>>,也就是需要将流集合List<Stream<String>>转成List<String>,那么就需要将流合并,也就是扁平化
List<String> list = Arrays.asList("Hello","World");
Stream<String[]> stream = list.stream()
.map(arr -> arr.split(""));
Stream<String> stringStream = stream.flatMap(Arrays::stream);
List<String> collect = stringStream.distinct().collect(Collectors.toList());
System.out.println(collect.toString());
结果:
[H, e, l, o, W, r, d]
以上合适并不好,应该写成下面这种
List<String> list = Arrays.asList("Hello","World");
List<String> collect = list.stream()
.map(arr -> arr.split(""))
.flatMap(Arrays::stream)
.distinct().collect(Collectors.toList());
System.out.println(collect.toString());
匹配
通常从一个集合或者数组中获取满足某个条件的数据需要经过循环、条件判断来获取。Stream API提供了一些方法allMatch、anyMatch、noneMatch、findFirst、findAny,通过方法名称大概能猜到每个方法的功能。
allMatch
检查谓词是否匹配所有元素
boolean allMatch(Predicate<? super T> predicate);
示例:
private static List<Apple> getApples() {
List<Apple> appleList = new ArrayList<>();
appleList.add(new Apple("red", 120, "富士"));
appleList.add(new Apple("red", 150, "富士"));
appleList.add(new Apple("green", 125, "富士"));
appleList.add(new Apple("green", 140, "富士"));
appleList.add(new Apple("green", 140, "富士"));
appleList.add(new Apple("green", 125, "富士"));
appleList.add(new Apple("green", 100, "青苹果"));
appleList.add(new Apple("green", 140, "青苹果"));
appleList.add(new Apple("green", 130, "青苹果"));
appleList.add(new Apple("red", 110, "蛇果"));
appleList.add(new Apple("red", 170, "蛇果"));
appleList.add(new Apple("red", 130, "蛇果"));
appleList.add(new Apple("red", 120, "蛇果"));
return appleList;
}
private static void test() {
List<Apple> apples = getApples();
if (apples.stream().allMatch(apple -> StringUtils.equals("red",apple.getColor()))){
System.out.println(true);
}else {
System.out.println(false);
}
}
结果:
false
anyMatch
检查谓词是否至少匹配一个元素
boolean anyMatch(Predicate<? super T> predicate);
同样是上面的示例,如果使用anyMatch则结果不一样了
private static void test() {
List<Apple> apples = getApples();
if (apples.stream().anyMatch(apple -> StringUtils.equals("red",apple.getColor()))){
System.out.println(true);
}else {
System.out.println(false);
}
}
结果:
true
noneMatch
检查谓词没有匹配到任何一个元素
boolean noneMatch(Predicate<? super T> predicate);
private static void test() {
List<Apple> apples = getApples();
if (apples.stream().noneMatch(apple -> StringUtils.equals("red",apple.getColor()))){
System.out.println(true);
}else {
System.out.println(false);
}
}
结果:
false
查找
findAny
返回当前流中的任意元素。返回结果为Optional。
Optional<T> findAny();
大概说一下Optional后面详细说明。
Optional类是一个容器类,代表一个值存在或者不存在。常用的方法:
- isPresent()将在Optional包含值的时候返回true,否则返回false
- ifPresent(Consumer block)会在值存在的时候执行给定的代码块。Consumer函数式接口,传递T类型的参数,返回void
- T get() 会在值存在时返回值,否则抛出一个NoSuchElement异常
- T orElse(T other)会在值存在时返回值,否则返回一个默认值。
private static void test() {
List<Apple> apples = getApples();
Optional<Apple> any = apples.stream()
.filter(apple -> 100 < apple.getWeight())
.findAny();
any.ifPresent(d-> System.out.println(d.toString()));
}
findFirst
获取第一个流元素
Optional<T> findFirst();
示例:
private static void test() {
List<Apple> apples = getApples();
Optional<Apple> any = apples.stream()
.filter(apple -> 100 < apple.getWeight())
.findFirst();
any.ifPresent(d-> System.out.println(d.toString()));
}
何时使用findFirst和findAny?
你可能会想,为什么会同时有findFirst和findAny呢?答案是并行。找到第一个元素
在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流
时限制较少
归约
概念
把一个流中的元素组合起来,使用reduce操作来表达更复杂的查
询,比如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作
(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠( fold),因为你可以将这个操
作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。
元素求和
- 有初始化值
如下:初始化值0,reduce()有两个参数a,b ,求numbers元素之和
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
在Java 8中, Integer类现在有了一个静态的sum
方法来对两个数求和
int sum = numbers.stream().reduce(0, Integer::sum);
- 无初始化值
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
numbers可能为空,所以Optional也可能是空
最大值和最小值
计算方式与求和类似
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
构建流
由值创建流
使用静态方法Stream.of,通过显示值创建一个流。它可以接受任意数量的参数
示例:
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
结果:
JAVA 8
LAMBDAS
IN
ACTION
创建一个空流
Stream<Object> empty = Stream.empty();
由数组创建流
使用静态方法Arrays.stream从数组创建一个流。Arrays.stream()根据数组元素类型调用不同的方法。
public static <T> Stream<T> stream(T[] array) {
return stream(array, 0, array.length);
}
示例:
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
结果:
41
此时调用的是
public static IntStream stream(int[] array) {
return stream(array, 0, array.length);
}
由文件生成流
Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。
java.nio.file.Files中的很多静态方法都会返回一个流
示例如下:
一个很有用的方法是
Files.lines,它会返回一个由指定文件中的各行构成的字符串流。使用你迄今所学的内容,
你可以用这个方法看看一个文件中有多少各不相同的词
由函数生成流
Stream API提供了两个静态方法来从函数生成流: Stream.iterate和Stream.generate。
这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。但是使用处理无限流必须用limit限制。
示例:
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
收集器
Collectors类提供的工厂方法创建收集器
主要提供了三大功能:
- 将流元素归约和汇总为一个值
- 元素分组
- 元素分区
归约和汇总
I.获取集合的元素数量:Collectors.counting()以及方法count(),其方法与集合和size()方法功能相同。
示例:
Long collect = getApples().stream()
.filter(apple -> StringUtils.equals("red", apple.getColor()))
.count();
Long collect1 = getApples().stream()
.collect(Collectors.counting());
int size = getApples().size();
II.获取集合中最大和最小的元素,返回的结果是Optional也就是可能为空:Collectors.maxBy() 和 Collectors.minBy 分别对应有方法max() 和min()。
示例:
Optional<Apple> collect = getApples().stream()
.collect(Collectors.maxBy(Comparator.comparing(Apple::getWeight)));
Optional<Apple> collect1 = getApples().stream()
.max(Comparator.comparing(Apple::getWeight));
System.out.println(collect1);
Optional<Apple> min = getApples().stream()
.min(Comparator.comparing(Apple::getWeight));
System.out.println(min);
III.获取所有元素的总和:Collectors.summingInt()
示例:
Integer collect1 = getApples().stream()
.collect(Collectors.summingInt(Apple::getWeight));
System.out.println(collect1);
int sum = getApples().stream()
.mapToInt(Apple::getWeight)
.sum();
System.out.println(sum);
通过方法summingInt就能查到此类方法还会有除了int类型以外的,Collectors.summingLong和Collectors.summingDouble,这些方法是计算和的,还有计算平均数的Collectors.averagingInt、Collectors.averagingLong、Collectors.averagingDouble。
IntSummaryStatistics 收集器返回的了
数据长度、总和、最小值、最大值、平均数,并且通过get方法
可以获取到这些值
示例:
IntSummaryStatistics collect = getApples().stream()
.collect(Collectors.summarizingInt(Apple::getWeight));
IV.拼接字符串:Collectors.joining(char)与StringBuild或者StringBuffy的append()方法功能相同。
示例:
String collect = getApples().stream()
.map(Apple::getType)
.collect(Collectors.toList())
.stream()
.distinct()
.collect(Collectors.joining(","));
System.out.println(collect);
结果:富士,青苹果,蛇果
分组
分组的方式其实在前面示例就用到了:Collectors.groupingBy()
示例:
//首先对苹果集合通过类型进行分组,获取key是苹果类型,value是对应苹果集合
//然后对value通过重量比较获取最大的苹果对象
//最后转换对象类型为苹果
Map<String, Apple> collect = getApples().stream()
.collect(Collectors.groupingBy(Apple::getType,
Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(Apple::getWeight)),
Optional::get)));
System.out.println(collect.toString());
Map<String, Apple> collect1 = getApples().stream()
.collect(Collectors.toMap(Apple::getType, //类型分组
Function.identity(),
BinaryOperator.maxBy(
Comparator.comparing(Apple::getWeight)))); //比较重量获取最大值的苹果
System.out.println(collect1.toString());
分区
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函
数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以
分为两组——true是一组, false是一组。Collectors.partitioningBy()
示例:
Map<Boolean, List<Apple>> red = getApples().stream()
.collect(Collectors.partitioningBy(apple -> StringUtils.equals("red", apple.getColor())));
System.out.println(red);
Collectors静态方法
并行流
并行流:收集源通过调用parallelStream方法来吧集合转换为并行流。并行流就是一个吧内容分为多个数据块,并用不同的线程分别处理每个数据块的流。这样一来,你就可以自动把给定操作的工作负荷分配给多核处理的所有内核,让它们都忙起来。
顺序流和并行流转换
顺序流-> 并行流 parallel()
并行流-> 顺序流 sequntial()
注意:影响整个流水线的是最后一次调用的是哪个方法
示例:
stream.parallel()
.filter(...)
.sequential()
.map(...)
.parallel()
.reduce();
这样多次调用方法转换流类型,但是最后调用的是parallel(),所以此流水线并行流。
并行流内部使用了默认的ForkJoinPool,它默认的线程数量就是你的处理器数量,这个值是由Runtime.getRuntime().avaliableProcessors()得到的。
但 是 你 可 以 通 过 系 统 属 性 java.util.concurrent.ForkJoinPool.common.
parallelism来改变线程池大小,如下所示:
System.setProperty(“java.util.concurrent.ForkJoinPool.common.parallelism”,“12”);
这是一个全局设置,因此它将影响代码中所有的并行流。反过来说,目前还无法专为某个
并行流指定这个值。一般而言,让ForkJoinPool的大小等于处理器数量是个不错的默认值,
除非你有很好的理由,否则我们强烈建议你不要修改它。
正确使用并行流
并不是只要使用并行流性能就好,效率就高。
正确使用并行流:
- 通过测试,使用顺序流和并行流相比较,测试那种方式更适合;
- 避免装箱和拆箱;
- 依赖有序元素的操作,比如limit和findFirst;
- 数量较少的情况;
- 考虑数据结构,比如:ArrayList的拆分效率比LinkedList
高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历;