前言
作为Java8添加的一个新特性,Stream流提供了一种声明的方式来处理数据。
其基于函数式编程思想,将复杂的语句代码通过简洁的方法调用来表示,让程序员写出的代码更加的高效、简洁并具备可读性。
先来看Javadoc对其的定义:
To perform a computation, stream operations are composed into a stream pipeline. A stream pipeline consists of a source (which might be an array, a collection, a generator function, an I/O channel, etc), zero or more intermediate operations (which transform a stream into another stream, such as filter(Predicate)), and a terminal operation (which produces a result or side-effect, such as count() or forEach(Consumer)).
为了执行计算,流操作被组合成一个流管道。流管道由源(这可能是一个数组,一个集合,一个生成器函数,一个I/O通道,等等),零个或多个中间业务(变换流到另一个流,如 filter(Predicate)),和一个终端操作(产生结果或副作用,如count()或forEach(Consumer))。
可以看到,官方对其的定义是一个处理管道,其将特定的元素经过一定的处理包装后“变为”一个新值并返回。(注意,这里的“变为”并不是真正意义上的改变,steam流并不会改变原有的参数,而是将变化后的新值作为返回值返回)
其将该操作分为三个步骤:
- 首先是源的创建,确定数据源类型并将其加入流管道中,从而围绕该数据源进行一系列操作;
- 其次是中间业务,其实也就是你所要进行的筛选、变换操作;
- 最后是终端操作,输出操作之后的结果。
我们可以把这三个步骤简便的分为源、变换、收集三个操作:
将一个特定的数据类型,经过一系列的“变换”,转化为一个新的数据并收集起来。
函数式编程
在讲Stream流对象之前,我们先来介绍一下函数式编程思想。
函数式编程是声明式的编程,其主要思想是将一系列的指令操作转为一系列的函数调用。
你不需要去提供一些数据处理的指令而只需要声明你想要做的事情,调用对应函数处理即可。
例如你去找出两个数中较大的一个并把它的值赋给另一个变量,不考虑三目运算符的话,正常的写法是:
int a = 1;
int b = 2;
int c = -1;
if(a > b) {
c = a;
} else {
c = b;
}
而用函数来表达的写法为:
int a = 1;
int b = 2;
int c = Math.max(a, b);
这就是函数式编程,逻辑封装在函数中,你不需要考虑你要怎么做,而只需要考虑你要做什么。
在 Stream 中,比如我们需要获得一个用户集合中年龄大于等于50的前十个用户并打印
正常的写法如下:
List<Person> filterPersonList = new ArrayList<>();
for (Person person : personList) {
if (person.getAge() >= 50) {
filterPersonList.add(person);
}
if (filterPersonList.size() == 10) {
break;
}
}
filterPersonList.forEach(person -> System.out.println("name: " + person.getName() + ", age: " + person.getAge()));
可以看到,筛选、迭代等操作杂糅在了一起,既暴露了过多细节,又不好进行修改
而使用 Stream ,则写法如下:
personList.stream()
.filter(person -> person.getAge() >= 70) // 筛选操作
.limit(10) // 取前10个元素
.forEach(person ->
System.out.println("name: " + person.getName() + ", age: " + person.getAge())); // 打印结果
对比看出,在 Stream 中我们只需要关注如何去做,并将筛选、迭代等操作清楚的分隔开来,以后要做其他变换只需要在其中加入方法即可。
流的类图
流的分类及其方法详解
这里我们主要介绍基本的对象引用流Stream中的方法,并简单介绍对其做了特定类型修改的IntStream、LongStream和DoubleStream
Stream 类
Stream类最常用的是其对各个数组以及集合的处理,它可以很方便的去对数组以及集合中的元素进行筛选、处理、聚合并返回一个全新的数组/集合,其中对该数据类型的处理并不会影响原数组/集合本身
源的创建
对于Stream流常见起始创建方法有3种:
- 由集合类进行创建
List<Integer> streamList = new ArrayList<>();
Stream<Integer> integerStream = streamList.stream();
- 由数组进行创建
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5)
int[] mArray = {1, 2, 3, 4, 5};
Stream<Integer> stream = Arrays.stream(mArray).boxed();
boxed()
方法的作用是装箱操作,将IntStream
特定类型的流转化为Stream<Integer>
普遍流操作,会在下文进行详细介绍
- 由
generate()
方法创建
Stream<Double> dStream = Stream.generate(Math::random);
generate()
方法根据传入参数生成一个无限的无序流
中间处理方法
1. filter()
方法
该方法接收一个判断式来对流中的元素进行筛选,将流中符合该判断的元素留下,不符合该判断的元素去除,并返回一个包含所有符合该判断的元素的新的流。
例子:
/**
* filter方法测试
*/
private static void filterTest() {
int[] fList = StreamTestUtils.randomGenerate();
System.out.println("筛选前的数组:");
Arrays.stream(fList).forEach(num -> System.out.print(num + ", "));
System.out.println("\n筛选后的数组:");
/* 筛选出50以内的数字 */
Arrays.stream(fList).filter(num -> num <= 50).forEach(num -> System.out.print(num + ", "));
System.out.println();
}
(randomGenerate()
方法用来生成一个区间为[1, 100],大小为10的随机数数组)
控制台输出:
筛选前的数组:
99, 18, 90, 85, 19, 5, 1, 54, 76, 6,
筛选后的数组:
18, 19, 5, 1, 6,
可以看到,filter()
方法成功将数组中小于等于50的数筛选了出来。
2. map()
方法
该方法用于对流中的元素进行转换操作,用于对流中的元素进行一系列的变换,返回一个变换后的新的流。
例子:
/**
* map方法测试
*/
private static void mapTest() {
int[] fList = StreamTestUtils.randomGenerate();
System.out.println("转换前的数组:");
Arrays.stream(fList).forEach(num -> System.out.print(num + ", "));
System.out.println("\n转换后的数组:");
/* 将每个数字加100后输出 */
Arrays.stream(fList).map(num -> num += 100).forEach(num -> System.out.print(num + ", "));
System.out.println();
}
控制台输出:
转换前的数组:
69, 44, 59, 96, 99, 40, 79, 33, 85, 76,
转换后的数组:
169, 144, 159, 196, 199, 140, 179, 133, 185, 176,
这里用 map()
方法使数组中的每个元素值增加100后输出新的数组值。
类似的方法还有mapToInt()
、mapToLong()
、mapToDouble()
,功能相同,只不过返回的是特定类型的流(IntStream、LongStream和DoubleStream)
3. flatMap()
方法
该方法用于将流的内容为一个个数组的合并为一整个数组
例子:
/**
* flatMap方法测试
*/
private static void flatMapTest() {
List<String[]> isList = new ArrayList<>();
for(int i=0; i<3; i++) {
String[] rNums = {"1", "2", "3", "4", "5"};
isList.add(rNums);
}
System.out.println("未合并前集合内容:");
isList.forEach(nums -> {
Arrays.stream(nums).forEach(num -> System.out.print(num + ", "));
System.out.println();
});
List<String> ifList = isList.stream().flatMap(Arrays::stream).collect(Collectors.toList());
System.out.println("合并后集合内容:\n" + ifList);
}
控制台输出:
未合并前集合内容:
1, 2, 3, 4, 5,
1, 2, 3, 4, 5,
1, 2, 3, 4, 5,
合并后集合内容:
[1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
这里生成了三个字符串数组并将其保存在了一个集合内,将该集合映射成流,其中每一个数组对应单个流,使用flatMap()
方法进行扁平化处理之后,各个数组合并,所有数组的内容合在一起映射为一个流
相似的方法还有flatToInt()
、flatToLong()
、flatToDouble()
,分别对应特定类型的流
4. distinct()
方法
该方法用于对流中的元素进行去重操作
例子:
/**
* distinct方法测试
*/
private static void distinctTest() {
int[] dList = {1, 1, 2, 5, 2, 4, 3, 1};
System.out.println("去重前的数组:");
Arrays.stream(dList).forEach(num -> System.out.print(num + ", "));
System.out.println("\n去重后的数组:");
Arrays.stream(dList).distinct().forEach(num -> System.out.print(num + ", "));
System.out.println();
}
控制台输出:
去重前的数组:
1, 1, 2, 5, 2, 4, 3, 1,
去重后的数组:
1, 2, 5, 4, 3,
要特别注意流中储存的内容是以数组为单位还是以数为单位,若以数组为单位的话调用distinct()
是不生效的(比如用split()
方法将字符串分割成一个个字符串数组再用 map()
方法进行变换),此时需要调用flatMap()
方法对流进行扁平化处理,之后再调用distinct()
方法去重
5. sorted()
方法
该方法用于对流中内容进行排序操作,默认为升序
例子:
/**
* sorted方法测试
*/
private static void sortedTest() {
List<Integer> isList = StreamTestUtils.intToList(StreamTestUtils.randomGenerate());
System.out.println("排序前的数组:");
isList.forEach(num -> System.out.print(num + ", "));
System.out.println("\n排序为升序的数组:");
isList.stream().sorted().forEach(num -> System.out.print(num + ", "));
System.out.println();
System.out.println("排序为降序的数组:");
isList.stream().sorted((o1, o2) -> Integer.compare(o2, o1)).forEach(num -> System.out.print(num + ", "));
System.out.println();
}
控制台输出:
排序前的数组:
79, 77, 36, 89, 32, 80, 12, 56, 78, 55,
排序为升序的数组:
12, 32, 36, 55, 56, 77, 78, 79, 80, 89,
排序为降序的数组:
89, 80, 79, 78, 77, 56, 55, 36, 32, 12,
可传入Comparator
表达式来自行拟定排序规则
小tips:对 map 的排序可以按键递增/递减
例子:
/**
* map排序测试
*/
private static void mapSortedTest() {
Map<Integer, Integer> map = new HashMap<>();
Random random = new Random();
for(int i=0; i<10; i++) {
map.put(random.nextInt(100)+1, random.nextInt(100)+1);
}
System.out.println("原Map为:\n" + map);
System.out.println("按键递升排序后的Map为:");
Map<Integer, Integer> kMap = map.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));
System.out.println(kMap);
System.out.println("按键递减排序后的Map为:");
Map<Integer, Integer> kMapDown = map.entrySet().stream().sorted(Map.Entry.<Integer, Integer>comparingByKey().reversed()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));
System.out.println(kMapDown);
控制台输出:
原Map为:
{82=33, 34=32, 2=4, 68=8, 25=90, 60=95, 45=21, 62=19, 47=91}
按键递升排序后的Map为:
{2=4, 25=90, 34=32, 45=21, 47=91, 60=95, 62=19, 68=8, 82=33}
按键递减排序后的Map为:
{82=33, 68=8, 62=19, 60=95, 47=91, 45=21, 34=32, 25=90, 2=4}
6. peek()
方法
该方法主要用于调试,对于中间流程某个步骤的执行元素情况进行打印输出
例子:
/**
* peek方法测试
*/
private static void peekTest() {
int[] pList = StreamTestUtils.randomGenerate();
System.out.println("原数组:");
Arrays.stream(pList).sorted().forEach(num -> System.out.print(num + ", "));
System.out.println("\n流程打印:");
int sum = Arrays.stream(pList).filter(num -> num <= 50).peek(num -> System.out.print("[" + num + ", ")).map(num -> num+=1).peek(num -> System.out.print(num + "], ")).sum();
System.out.println("\n和:" + sum);
}
控制台输出:
原数组:
7, 22, 35, 55, 69, 77, 82, 83, 83, 92,
流程打印:
[22, 23], [7, 8], [35, 36],
和:67
首先对数组中小于等于50的元素进行筛选,筛选结果为 22
、7
、35
之后使用peek()
方法对筛选结果进行打印查看,打印结果应为:[22,
、[7,
、[35,
调用map()
方法对流中的每个结果进行加一操作,并再次调用peek()
方法进行打印查看,得到最终结果
[22, 23], [7, 8], [35, 36]
此时流中的内容已经变为变换后的内容 23
、8
、36
,所以求和结果为 67
7. limit()
方法
该方法用于截取操作,可截取数组的前n个元素
例子:
/**
* limit方法测试
*/
private static void limitTest() {
int[] lList = StreamTestUtils.randomGenerate();
System.out.println("原数组:");
Arrays.stream(lList).forEach(num -> System.out.print(num + ", "));
System.out.println("\n截取5个字符数组:");
Arrays.stream(lList).limit(5).forEach(num -> System.out.print(num + ", "));
System.out.println();
}
控制台输出:
原数组:
69, 57, 13, 46, 66, 29, 78, 76, 95, 7,
截取5个字符数组:
69, 57, 13, 46, 66,
8. skip()
方法
该方法用于跳过元素操作,可以跳过前n个元素,返回剩余元素组成的流
(若元素数目不满n个,则返回空流)
例子:
/**
* skip方法测试
*/
private static void skipTest() {
int[] sList = StreamTestUtils.randomGenerate();
System.out.println("原数组:");
Arrays.stream(sList).forEach(num -> System.out.print(num + ", "));
System.out.println("\n跳过5个字符后数组:");
Arrays.stream(sList).skip(5).forEach(num -> System.out.print(num + ", "));
System.out.println();
}
控制台输出:
原数组:
55, 13, 36, 43, 1, 5, 64, 11, 75, 57,
跳过5个字符后数组:
5, 64, 11, 75, 57,
终端收集方法
1. forEach()/forEachOrdered()
方法
该方法用于对流中内容的循环遍历,而 forEachOrdered()
方法用于保持原有元素的既定顺序遍历
例子:
/**
* forEachOrdered方法测试
*/
private static void forEachOrderedTest() {
int[] fList = StreamTestUtils.randomGenerate();
System.out.println("原数组:");
Arrays.stream(fList).forEach(num -> System.out.print(num + ", "));
System.out.println("\n并行流打印数组:");
Arrays.stream(fList).parallel().forEach(num -> System.out.print(num + ", "));
System.out.println("\n并行流顺序打印数组:");
Arrays.stream(fList).parallel().forEachOrdered(num -> System.out.print(num + ", "));
System.out.println();
}
控制台输出:
原数组:
77, 49, 36, 21, 64, 36, 75, 100, 36, 46,
并行流打印数组:
75, 36, 100, 49, 77, 36, 46, 64, 36, 21,
并行流顺序打印数组:
77, 49, 36, 21, 64, 36, 75, 100, 36, 46,
用普通的forEach()
通过并行流打印数组,可以明显看到打印顺序为乱序的
但是利用forEachOrdered()
方法即可保持数组的原有顺序打印
2. toArray()
方法
主要用于将给定数据源转换为数组返回,空参方法返回的是 Object[]
类型的数组,可以传入一个指定数组类型以返回特定类型数组
例子:
/**
* toArray方法测试
*/
private static void toArrayTest() {
List<Integer> tList = StreamTestUtils.intToList(StreamTestUtils.randomGenerate());
System.out.println("原集合:");
tList.forEach(num -> System.out.print(num + ", "));
System.out.println("\n正常方法转换Integer数组:");
Integer[] tIns = tList.toArray(new Integer[0]);
Arrays.stream(tIns).forEach(num -> System.out.print(num + ", "));
System.out.println("\n经过Stream流筛选不大于50的元素后转换的Integer数组:");
Integer[] tStreamToIns = tList.stream().filter(num -> num <= 50).toArray(Integer[]::new);
Arrays.stream(tStreamToIns).forEach(num -> System.out.print(num + ", "));
System.out.println();
}
控制台输出:
原集合:
74, 17, 74, 14, 92, 36, 65, 93, 29, 26,
正常方法转换Integer数组:
74, 17, 74, 14, 92, 36, 65, 93, 29, 26,
经过Stream流筛选不大于50的元素后转换的Integer数组:
17, 14, 36, 29, 26,
集合本身提供了toArray()
方法来将集合转化为数组,但是中间如果涉及过滤、变换等操作,该方法就显得不是那么方便,而使用流操作可以直接在中间声明过滤、变换的方法,并在最后用toArray()
方法来对操作后的流进行收集操作
3. reduce()
方法
该方法主要用于对流中的数据进行归约计算,其接收一个组合两个值之间操作的方法(或单纯对两个数操作的lambda表达式),如min()
、max()
、sum()
等,并对流中的所有数进行操作,其操作不限于顺序执行
例子:
/**
* reduce方法测试
*/
private static void reduceTest() {
int[] rList = StreamTestUtils.randomGenerate();
System.out.println("原数组:");
Arrays.stream(rList).forEach(num -> System.out.print(num + ", "));
System.out.print("\n累加之后的和:");
System.out.println(Arrays.stream(rList).boxed().reduce(Integer::sum).get());
System.out.print("加上初始值1000后累加之后的和:");
System.out.println(Arrays.stream(rList).boxed().reduce(1000, Integer::sum));
}
控制台输出:
原数组:
74, 11, 99, 9, 78, 49, 83, 85, 65, 43,
累加之后的和:596
加上初始值1000后累加之后的和:1596
该方法会返回一个Optional<T>
容器对象,该容器对象是一个基于值的类,可以进行一些简单的如orElse()
(如果值不存在则返回默认值)、ifPresent()
(如果值存在则执行代码块)等方法,在此处使用该容器对象的get()
方法获得其储存的泛型T类型的值
该方法可以接收一个初始值,并将该值加入对流的操作中,此时返回值的类型为传入的初始值的类型,将直接返回该类型的对象
(不带初始值的方法会将第一个值作为初始值)
reduce()
不仅仅能接收一个方法,还可以接收一个对数据之间进行操作的lambda表达式,如:
Arrays.stream(rList).boxed().reduce((a, b) -> a + b).get()
// 上述与下列写法作用一致
Arrays.stream(rList).boxed().reduce(Integer::sum).get()
另外,在官方的源码中还有以下一段介绍:
While this may seem a more roundabout way to perform an aggregation compared to simply mutating a running total in a loop, reduction operations parallelize more gracefully, without needing additional synchronization and with greatly reduced risk of data races.
虽然与简单地在循环中改变运行总数相比,这似乎是一种更迂回的方式来执行聚合,但归约操作更优雅地并行化,不需要额外的同步,并且大大降低了数据竞争的风险。
官方介绍说,归约操作可以更优雅地并行化,那么具体是如何操作的呢?
我们先来探究一下,并行流操作结果是否和单线程的操作结果一致,简单的进行一个归约操作:
int[] rList = {1, 2, 3};
System.out.println("原数组:");
Arrays.stream(rList).forEach(num -> System.out.print(num + ", "));
System.out.println("\n主线程执行归约操作:");
System.out.println(Arrays.stream(rList).boxed().reduce((a, b) -> a + b * 2).get());
System.out.println("并行流执行归约操作:");
System.out.println(Arrays.stream(rList).boxed().parallel().reduce((a, b) -> a + b * 2).get());
按照正常来说,计算流程应该为:
- a = 1,b = 2, 1 + 2 * 2 = 5
- a = 5,b = 3, 5 + 3 * 2 = 11
最终结果应为 11
,可真的是这样吗?看下控制台输出:
原数组:
1, 2, 3,
主线程执行归约操作:
11
并行流执行归约操作:
17
并行流最终计算结果为17
,多余的6
是哪里出来的?
我们可以将工作流程打印出来,看一下具体并行流是如何工作的:
System.out.println("\n主线程执行归约操作:");
Arrays.stream(rList).boxed().reduce((a, b) -> {
System.out.println("线程名称:" + Thread.currentThread().getName() + ";元素值:a = " + a + ",b = " + b);
return a + b * 2;
});
System.out.println("并行流执行归约操作:");
Arrays.stream(rList).boxed().parallel().reduce((a, b) -> {
System.out.println("线程名称:" + Thread.currentThread().getName() + ";元素值:a = " + a + ",b = " + b);
return a + b * 2;
});
看一下控制台输出:
主线程执行归约操作:
线程名称:main;元素值:a = 1,b = 2
线程名称:main;元素值:a = 5,b = 3
并行流执行归约操作:
线程名称:main;元素值:a = 2,b = 3
线程名称:main;元素值:a = 1,b = 8
可以看到,单线程下的操作和我们预想的过程一致,可是在并行流下,操作流程却是2
和3
先执行,之后再与1
进行操作,它的分组方式是(1,(2,3)),到了此时,并行流的操作方式初见端倪
我们将元素个数增多,再来分析它是如何进行分组的:
int[] rList = {1, 2, 3, 4, 5};
System.out.println("原数组:");
Arrays.stream(rList).forEach(num -> System.out.print(num + ", "));
System.out.println("\n并行流执行归约操作:");
Arrays.stream(rList).boxed().parallel().reduce((a, b) -> {
System.out.println("线程名称:" + Thread.currentThread().getName() + ";元素值:a = " + a + ",b = " + b);
return a + b * 2;
});
控制台输出:
原数组:
1, 2, 3, 4, 5,
并行流执行归约操作:
线程名称:ForkJoinPool.commonPool-worker-5;元素值:a = 1,b = 2
线程名称:ForkJoinPool.commonPool-worker-4;元素值:a = 4,b = 5
线程名称:ForkJoinPool.commonPool-worker-4;元素值:a = 3,b = 14
线程名称:ForkJoinPool.commonPool-worker-4;元素值:a = 5,b = 31
可以看到,它的分组方式为:((1,2),(3,(4,5)))
从这里就可以看出,其实并行流计算分组是采用的对半分组,即不断的将数组对半平分,直到结果为两个数一组,而若数组总数为奇数,则后一组比前一组分得元素个数多 1
以这个逻辑来想,对于上述数组的分组过程其实为:
- (1,2) ,(3, 4, 5)
- (1,2) ,(3,(4,5))
我们可以进一步增加元素数量来验证这个分组逻辑的正确性:
// 数组元素为 1, 2, 3, 4, 5, 6, 7 下,控制台输出结果
并行流执行归约操作:
线程名称:main;元素值:a = 4,b = 5
线程名称:ForkJoinPool.commonPool-worker-2;元素值:a = 6,b = 7
线程名称:ForkJoinPool.commonPool-worker-2;元素值:a = 14,b = 20
线程名称:ForkJoinPool.commonPool-worker-3;元素值:a = 2,b = 3
线程名称:ForkJoinPool.commonPool-worker-3;元素值:a = 1,b = 8
线程名称:ForkJoinPool.commonPool-worker-3;元素值:a = 17,b = 54
可以从上述流程看到,其分组流程为:
- (1,2,3,(4,5),6,7)
- (1,2,3,(4,5),(6,7))
- (1,2,3,((4,5),(6,7)))
- (1,(2,3),((4,5),(6,7)))
- (1,(2,3)),((4,5),(6,7))
与我们猜想的一致,在不断的进行对半分组之后进行计算操作
(计算顺序不一定一致,但分组是固定的,所以最终结果是一致的,如在上述流程中,可能先执行 (2, 3),再执行 (6, 7), (4, 5) 等)
由于并行流是多线程运行,所以当方法带有初始值时,各个线程的累加器都会带有该初始值,会对每个元素进行一次初始值的计算转换
比如对于 a + b * 2
这个操作来说,对于数组{1, 2, 3}
,如果带有初始值1
进行计算
正常流程应为:
1 + 1 * 2 + 2 * 2 + 3 * 2 = 13
而对于并行流,首先对每个元素利用初始值进行计算:
{1, 2, 3}
=> {1+1*2, 1+2*2, 1+3*2}
=> {3, 5, 7}
之后再分组计算:
3 + (5 + 7 * 2) * 2 = 41
可能会有人好奇开启的线程数量,我们可以用以下两个方法来查看总共有多少线程运行:
Runtime.getRuntime().availableProcessors()
该方法可以 输出CPU可用核心数
ForkJoinPool.getCommonPoolParallelism()
该方法可以 输出公共池默认并发线程数,通常是比CPU可用核心数少1,原因是CPU可用核心数包括了主线程,主线程要占用一个名额
System.out.println("CPU可用核心数:" + Runtime.getRuntime().availableProcessors());
System.out.println("公共池默认并发线程数:" + ForkJoinPool.getCommonPoolParallelism());
// 控制台输出
CPU可用核心数:6
公共池默认并发线程数:5
因为我电脑的CPU可用核心为6
,所以公共池默认并发线程数为5
4. collect()
方法
该方法主要对数组内元素进行一个可变归约操作,通常用于对数组元素的收集转化与归纳
比如可以用来将数组元素累积到集合中:
/* 基本类型数组 -> 集合 */
int[] arr = {2, 5, 1, 3, 4};
List<Integer> list = Arrays.stream(arr).boxed().collect(Collectors.toList());
也可以对数组元素进行归纳:
List<Person> personList = Arrays.asList(
new Person("a", "北京"),
new Person("b", "北京"),
new Person("c", "上海")
);
Map<String, List<Person>> peopleByCity
= personList.stream().collect(Collectors.groupingBy(Person::getCity));
System.out.println(peopleByCity);
控制台输出:
{上海=[Person{name='c', city='上海'}], 北京=[Person{name='a', city='北京'}, Person{name='b', city='北京'}]}
究其根本是利用了Collectors
类的方法,其会返回一个Collector
带有泛型的收集器,利用collect()
方法对其中数据进行提取
小tips:可以用 Collectors
的 toMap
方法转换 map 并进行比如去除重复键的操作
例子:
System.out.println("==================重复键测试==================");
List<User> users = Arrays.asList(
new User(1, random.nextInt(100)+1),
new User(7, random.nextInt(100)+1),
new User(4, random.nextInt(100)+1),
new User(1, random.nextInt(100)+1),
new User(2, random.nextInt(100)+1),
new User(2, random.nextInt(100)+1),
new User(9, random.nextInt(100)+1),
new User(8, random.nextInt(100)+1),
new User(7, random.nextInt(100)+1),
new User(1, random.nextInt(100)+1)
);
System.out.println("未处理重复键:");
System.out.print("{");
users.forEach(user -> System.out.print(user.getKey() + "=" + user.getValue() + ", "));
System.out.println("}\n处理重复键后,保留前者:");
Map<Integer, Integer> repPreMap = users.stream().collect(Collectors.toMap(User::getKey, User::getValue, (oldValue, newValue) -> oldValue));
System.out.println(repPreMap);
System.out.println("处理重复键后,保留后者:");
Map<Integer, Integer> repNexMap = users.stream().collect(Collectors.toMap(User::getKey, User::getValue, (oldValue, newValue) -> newValue));
System.out.println(repNexMap);
控制台输出:
==================重复键测试==================
未处理重复键:
{1=73, 7=19, 4=37, 1=51, 2=69, 2=82, 9=57, 8=100, 7=7, 1=84, }
处理重复键后,保留前者:
{1=73, 2=69, 4=37, 7=19, 8=100, 9=57}
处理重复键后,保留后者:
{1=84, 2=82, 4=37, 7=7, 8=100, 9=57}
toMap()
前两个入参分别为key
和value
,而第三项是对重复键的处理,上述代码分别返回oldValue
和newValue
分别表示从前往后比较只保留第一个和只保留最后一个,如果返回oldValue + newValue
则返回的是对值的累加
5. max()
/min()
/count()
方法
这三个方法分别可获得流中元素的最大值、最小值以及长度
例子:
/**
* 用Stream流模拟Math类中的count、max、min方法
*/
private static void mathTest() {
int[] mList = StreamTestUtils.randomGenerate();
System.out.println("原数组:");
Arrays.stream(mList).sorted().forEach(num -> System.out.print(num + ", "));
int count = (int) Arrays.stream(mList).count();
int max = Arrays.stream(mList).max().orElse(-1);
int min = Arrays.stream(mList).min().orElse(-1);
System.out.println("\n长度:" + count);
System.out.println("最大值:" + max);
System.out.println("最小值:" + min);
}
控制台输出:
原数组:
15, 19, 30, 39, 48, 53, 92, 94, 95, 97,
长度:10
最大值:97
最小值:15
min()
和max()
方法分别接收一个比较器,通过比较器来判断传参类型的比较方式(比如传入的Person
类按名字取得最大/最小值,则比较器最后返回的是名字的比较值),并返回一个Optional<T>
类
这两个方法要求调用Optional<T>
类的isPresent()
判断流数据中是否存在该值,所以一般都会在其后跟orElse()
方法指定不存在时的默认值
(这里为了方便所以调用的是IntStream
特定流的方法,本质区别是将泛型设为Integer
,即返回OptionalInt
类,默认的比较器为比较两数之间的大小)
6. allMatch()
/anyMatch()
/noneMatch()
这三个方法是查看流中元素是否匹配某个特定逻辑
allMatch()
:全部匹配才返回true
anyMatch()
:任意元素匹配就返回true
noneMatch()
:全部不匹配才返回true
例子:
/**
* match()方法测试
*/
private static void matchTest() {
int[] mList = StreamTestUtils.randomGenerate();
System.out.println("原数组:");
Arrays.stream(mList).sorted().forEach(num -> System.out.print(num + ", "));
System.out.println("\n全部匹配测试:");
boolean match = Arrays.stream(mList).allMatch(num -> num > 0);
System.out.println("结果:" + match);
System.out.println("部分匹配测试:");
match = Arrays.stream(mList).anyMatch(num -> num > 50);
System.out.println("结果:" + match);
System.out.println("不匹配测试:");
match = Arrays.stream(mList).noneMatch(num -> num < 0);
System.out.println("结果:" + match);
}
控制台打印:
原数组:
3, 8, 8, 46, 47, 52, 59, 67, 68, 77,
全部匹配测试:
结果:true
部分匹配测试:
结果:true
不匹配测试:
结果:true
数组全部大于0
,且有至少一项大于50
,并且全部大于等于0
,所以三个均返回true
7. findFirst()/findAny()
这两个方法返回流中数据的第一个元素,注意是流中数据操作的顺序,也就是说该流操作会以此数据作为第一个,而不是传入流的数据的顺序
例子:
/**
* find()方法测试
*/
private static void findTest() {
int[] mList = StreamTestUtils.randomGenerate();
System.out.println("原数组:");
Arrays.stream(mList).sorted().forEach(num -> System.out.print(num + ", "));
int firstNum = Arrays.stream(mList).findFirst().orElse(-1);
System.out.println("\n第一个数为:" + firstNum);
int anyNum = Arrays.stream(mList).findAny().orElse(-1);
System.out.println("任意一个数为:" + anyNum);
}
控制台打印:
原数组:
12, 19, 35, 41, 56, 58, 62, 67, 71, 80,
第一个数为:41
任意一个数为:41
findFirst()
和findAny()
方法的主要区别是前者对并行流的限制很多,而后者对并行流的限制较少,一般采用findAny()
方法,因为使用场景较少,所以这里不做深究,有兴趣的可以自行研究
其他特定类型的流
其他特定类型的流主要有IntStream
、DoubleStream
、LongStream
三种
关于这三种基本类型要讲的其实不多,因为其与Stream
流的基本方法大致相同,只是对于个别类似于map()
、min()
、max()
等方法做了自身的优化,比如在Stream
流中此类方法一般返回的是特定泛型,有的还需要自行加入比较器,而在IntStream
流中,指定了泛型为Integer
类型,且比较器已经默认生成好
其次就是boxed()
方法,该方法是将特定类型的流转换为Stream
流使用,比如当你使用Arrays.stream()
传入的是一个int[]
数组类型的话,那么返回的就是IntStream
类型的流,而你想要调用Stream
流中的方法,就需要boxed()
进行转换
此外,如IntStream
中也可以使用asLongStream()
或asDoubleStream()
方法转换为其他类型的流
另外就是在IntStream
和LongStream
中新加入了方法range()
,可以对流中元素进行截取操作,左闭右开
后续要考虑的问题及总结
本篇文章只是讲解了浅层的一些知识,对于一些深层次的东西并未过多探究
比如:
- 并行流线程安全是怎么保证的?
- 各个方法具体是怎么实现的,时间复杂度如何?
- 很多方法其实在
collection
中已经有实现,为什么还要引入Stream
? - 类的继承链是怎么设计的以及为什么这么设计?
Stream
作为懒加载,终端操作时才执行计算是为什么,怎么做到的?
等等,诸如此类问题,以后我会专门发篇文章探究其底层源码与逻辑
而对于平常是否要用到Stream
流来代替所有的操作,我的意见是,Stream
流会使得代码更清晰、方便,但可能不够灵活,有些需要特定算法或者效率优先的环境下,还是要斟酌选择