先看这样一个问题:
定义一个数组或者集合(source),包含 0 ~ 9 十个数字,筛选大于 5 的数组,返回一个新的数组或者集合(result)
当你熟练的打开编辑器,噼里啪啦一顿操作猛如虎:
List<Integer> result = new ArrayList<>();
for(Integer i : source){
if(i > 5)
result.add(i);
}
return result;
小小嘚瑟一下,简单,五六行代码,十秒钟搞定。
回头看看左边,
python
程序员一脸嫌弃的看过来,偌大的屏幕上,函数中只有孤零零的一行代码
np.where(source > 5)
What ??? 这就可以了?
仔细一想,也难怪,
Python
一向以语法简单著称,可这,还是有点太简单了……郁闷中 ╮(╯﹏╰)╭
回头看一眼右边, 目光正好碰上
C#
程序员鄙视的眼神投来,顿时有点忐忑,难道他也……,弱弱的瞄一眼屏幕,也是一行!!! 瞬间 …(⊙_⊙)…
source.Where(t => t > 5);
当他告诉你,他还有一种语法也可以一行实现该功能
from i in source where i > 5 select i;
天哪, 有木有一种要崩溃的感觉 ~
这… 这到底是是什么语法? SQL 的风格直接一行代码处理数据, 一分钟之内连续被两个同行鄙视,
Java
,你怎可如此low
?
事实上,你还真错怪
Java
了
它
low
吗? 不,一点也不!
它没有类似的语法吗? 不,只是你不会而已! 兄弟,该学习了!!!
Java 8 的新特性 —— Stream 流。
上面的问题,我们同样可以避免冗余的 for
循环, 乏味的 if
判断,利用一行代码轻松解决。
source.stream().filter( i -> i > 5).collect(toList());
在 Java 8 中,数据集合只需要调用 stream
方法转换为 stream
流,就可以轻松使用各种 声明性方式 处理数据集合。
何为声明性方式?
**声明性方式:**即只需要说明想要完成的动作,而不需要关心实现动作的具体操作。如上例,我们只需要说明我们的需求(过滤数组,以 i > 5 为条件),而不需要具体的操作(遍历数组,如果 i > 5,添加到新数组 …)
流的简介和特点
流其实就是从一组数据源中生成的元素序列。流与集合最大的不同在于流的作用是来表达计算。简单来说,通常情况下集合是用来存储数据的,流是用来处理数据的。
-
链式操作
大多数流操作的返回值依然是流,所以这样的多个操作就可以链接起来,形成一个流水线式操作,也称之为链式操作。
-
顺序保留
一个数据源转换为流时,如果数据源是有序的,那么生成流的时候也会保留原有顺序。流在概念上是固定的数据结构,因此不能进行增删等操作。
-
数据处理
数据处理是流的核心功能,不仅支持顺序操作,还支持并行操作。这种类
SQL
的声明式操作,极大的方便了集合数据的处理。 -
内部迭代
流和迭代和迭代器的显式迭代类似,都只能遍历一次,但是流的迭代操作是在背后进行的,这使得流操作相对于显式迭代操作来说,安全性和性能都有了很大的提升。
流的基本操作
流的基本操作主要分三个步骤:初始操作、中间操作、终端操作。
-
初始操作
- stream
- 方法一:Collection 提供的 stream 方法,将数据源转换成顺序流返回
List<String> list = new ArrayList<>(); Stream<String> stream = list.stream();
- 方法二:Collection 提供的 parallelStream 方法,将数据源转换成并行流返回
List<String> list = new ArrayList<>(); Stream<String> parallelStream = list.parallelStream();
- 方法三:数组创建流 —— Arrays 的 stream 静态方法获取数据流
int[] array = new int[] {}; IntStream intStream = Arrays.stream(array); IntStream intStream1 = Arrays.stream(array, 0, array.length); /* 与 Java 8 提供的函数式接口同样,为了避免频繁的拆装箱引起不必要的开销,Stream 也扩展了原始类型的流:如 IntStream, DoubleStream 等 */
- 方法四:值创建流 —— Stream 的 of 静态方法获取数据流
Stream stream = Stream.of("hello", "every one", "good", "morning");
- 方法五:文件创建流 —— Files 的 lines 方法
Stream<String> lines = Files.lines(Paths.get("123.txt"), Charset.defaultCharset());
- 方法六:空流 —— Stream 的 empty 方法
Stream<Object> empty = Stream.empty();
- 方法七:函数创建流(无限流)
-
迭代函数
该流接收一个初始值和一个依次应用在每个新值上的 UnaryOperator 类型的 Lambda 表达式,这种流没有结尾,可以永远计算下去,因此需要 limit 截断
Stream<Integer> stream = Stream.iterate(0, n -> n + 2).limit(10);
-
生成函数
与迭代函数不同,生成函数不是对每个新生成的值应用函数的,它接收一个 Supplier 类型的 Lambda 表达式来提供新值,但同样它也是无限的,因此需要截断Stream<Double> stream = Stream.generate(Math::random).limit(10);
-
- 方法一:Collection 提供的 stream 方法,将数据源转换成顺序流返回
- stream
-
中间操作
- filter
- 作用:过滤
- 参数类型:
Predicate<T>
- 返回类型:
Stream<T>
- 示例:
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0); nums.stream().filter(t-> t % 2 == 0) //nums.stream().filter(t-> t % 2 == 0).forEach(System.out::println); //forEach 为了将结果打印到控制台上 //结果:2 4 6 8 0
- map
- 作用:映射
- 参数类型:
Function<T, R>
- 返回类型:
Stream<R>
- 示例:
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0); nums.stream().map(t -> t * 2); //结果:2 4 6 8 10 12 14 16 18 0
- sorted
- 作用:排序
- 参数类型:
Comparator<T>
- 返回类型:
Stream<T>
- 示例:
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); nums.stream().sorted();//默认排序 4 5 7 7 7 8 9 10 11 12 nums.stream().sorted(Comparator.reverseOrder());// 逆序排序 12 11 10 9 8 7 7 7 5 4 nums.stream().sorted(Comparator.comparing(i -> i % 5)); //自定义排序(除以 5 的余数大小排序) 10 5 11 12 7 7 7 8 9 4
- distinct
- 作用:去重
- 返回类型:
Stream<T>
- 示例:
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); nums.stream().distinct(); // 10 12 9 5 7 4 8 11
- limit
- 作用:截取
- 参数类型:
long
- 返回类型:
Stream<T>
- 示例:
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); nums.stream().limit(3); // 10 12 9
- skip
- 作用:跳过
- 参数类型:
long
- 返回类型:
Stream<T>
- 示例:
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); nums.stream().skip(3); // 5 7 4 7 8 11 7
- flatMap
- 作用:扁平化处理(一个流中的每个值都需要换成另一个流时,将所有流链接成一个流)
- 参数类型:
Function<T, Stream<R>>
- 返回类型:
Stream<R>
- 示例:
List<Integer> num1 = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); List<Integer> num2 = Arrays.asList(3, 4, 5); Stream <int[]> stream = num1.stream().flatMap(i -> num2.stream().filter(j -> i != j && i % j == 0).map(j -> new int[]{i, j})); List<int[]> list = stream.collect(Collectors.toList()); //list: [[6, 3], [8, 4], [9, 3]]
- filter
-
终端操作
-
collect
- 作用:规约
- 扩展:规约操作还可以自定义收集器,实现更为复杂的规约,这里暂用预定义的收集器来写两个最常用的示例,
- 参数类型:
Collector<T, A, R>
- 返回类型:
R
- 示例:
- 规约成集合
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); List<Integer> result = nums.stream().skip(3).collect(Collectors.toList());
- 规约成分组
private List<User> source = new ArrayList<>(); source.add(new User("张", "三", "male", 18)); source.add(new User("李", "四", "female", 14)); source.add(new User("王", "五", "female", 24)); source.add(new User("赵", "六", "male", 16)); Map<String, List<User>> groupByGender = source.stream().collect(groupingBy(User::getGender)); /* 根据性别分组,返回一个 Map 类型集合 */ /* 结果类似如下结构: {"male" : ["张三", "赵六"], "female" : ["李四", "王五"]} */ /* 如果需要多级分组,groupingBy 还可以传递第二个参数,为内层 groupingBy 语法: groupingBy(一级分组表达式, groupingBy(二级分组表达式, groupingBy(三级分组表达式))) */
- 规约成集合
-
count
- 作用:返回流中元素的个数
- 返回类型:
long
. - 示例:
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); long count = nums.stream().count();
-
forEach
- 作用:使用对应的表达式消费流中的每个元素
- 参数类型:
Consumer<T>
- 返回类型:
void
- 示例:
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); nums.stream().skip(3).forEach(System.out::print);
-
reduce
- 作用:规约
- 参数类型:
BinaryOperator<T>
- 返回类型:
Optional<T>
- 示例:
reduce 中 Lambda 表达式是一步一步将上一次计算的结果返回,然后继续和下一个元素运算,并产生一个新的结果返回,继续和下一个元素运算,直到流结束。
- 无初始值
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); Optional<Integer> sum = nums.stream().reduce((a, b) -> a + b); //无初始值,默认 0 // 0 + 10 返回 10 // 上一次返回结果和流的下一个元素传入 Lambda 表达式 : 10 + 12 返回 22 // 上一次返回结果和流的下一个元素传入 Lambda 表达式 : 22 + 9 返回 31 // ...... // sum: 80
- 有初始值
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); Integer sum = nums.stream().reduce(10, Integer::sum); // Integer::sum 在这里与 (a, b) -> a + b 等价 //无初始值,默认 10 // 10 + 10 返回 20 // 上一次返回结果和流的下一个元素传入 Lambda 表达式 : 20 + 12 返回 32 // 上一次返回结果和流的下一个元素传入 Lambda 表达式 : 32 + 9 返回 41 // ...... // sum: 90
- 无初始值
-
allMatch
- 作用:检查谓词是否匹配所有元素
- 参数类型:
Predicate<T>
- 返回类型:
boolean
- 示例:
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); boolean isAllOdd = nums.stream().allMatch(i -> i % 2 == 1); // false
-
anyMatch
- 作用:检查谓词是否至少匹配一个元素
- 参数类型:
Predicate<T>
- 返回类型:
boolean
- 示例:
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); boolean hasOdd = nums.stream().anyMatch(i -> i % 2 == 1); // true
-
noneMatch
- 作用:检查谓词是否不匹配所有元素(与allMatch相对)
- 参数类型:
Predicate<T>
- 返回类型:
boolean
- 示例:
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); boolean isAllEven = nums.stream().noneMatch(i -> i % 2 == 1); // true
-
findAny
- 作用:返回流中任意元素,将利用短路找到结果后立即结束
- 返回类型:
Optional<T>
- 示例:
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); Optional<Integer> firstOdd = nums.stream().filter(i -> i % 2 == 1).findAny(); // 9
-
findFirst
-
作用:返回流中第一个元素
-
返回类型:
Optional<T>
-
示例:
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); Optional<Integer> firstOdd = nums.stream().filter(i -> i % 2 == 1).findFirst(); // 9
一眼望去,findAny 和 findFirst 似乎没有什么区别。
实际上,findAny 其返回的结果是不确定的。如上例,如果是并行情况,或者数据量比较大的时候,多调用几次,可能返回 9,也可能返回 5、 7 或者 11。
而 findFirst 返回的一定是第一个元素。
所以,如果侧重于效率,只需要一个满足条件的任意结果,那么建议使用 finaAny ,因为并行搜索效率更高;
如果侧重于元素,必须要满足条件的第一个元素,那么就要使用 findFirst。
-
再看两个例子:
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); while (true) System.out.println(nums.parallelStream().filter(i -> i % 2 == 1).findFirst().get()); // 输出结果一直为 9
List<Integer> nums = Arrays.asList(10, 12, 9, 5, 7, 4, 7, 8, 11, 7); while (true) System.out.println(nums.parallelStream().filter(i -> i % 2 == 1).findAny().get()); // 输出结果不唯一。7 9 5 11
-
-
-
最后再看一组完整的基本使用示例代码:
public class Test { //先初始化一组数据 private List<User> source = new ArrayList<>(); { source.add(new User("张", "三", "male", 18)); source.add(new User("李", "四", "female", 14)); source.add(new User("王", "五", "female", 24)); source.add(new User("赵", "六", "male", 16)); } /* 流的基本三步骤: 创建、中间操作、终端操作 */ @Test public void test1() { // 初始操作 Stream<User> stream = source.stream(); // 中间操作 Stream<User> men = stream.filter(t -> "male".equals(t.getGender())); Stream<String> menNamesStream = men.map(t -> t.getFirstName() + t.getLastName()); //终端操作 List<String> menNames = menNamesStream.collect(Collectors.toList());// ["张三", "赵六"] /** 流只能被消费一次, 如果再次调用将会异常 **/ List<User> women = stream.filter(t -> "female".equals(t.getGender())).collect(Collectors.toList()); /** java.lang.IllegalStateException: stream has already been operated upon or closed **/ } /* 流的链式操作 */ @Test public void test2(){ List<String> names = source.stream() .filter(p -> p.getAge() >= 18) //年龄大于 18 的人 .map(p-> p.getFirstName()) //获取姓氏 .limit(3) //截取前 3 个 .collect(Collectors.toList()); //终端操作:返回集合 } }
流的性能问题
我们可以在初始流的时候通过 stream 方法将集合转换为顺序流,也可以通过 parallelStream 方法将集合直接转换为并行流。
当然,我们也可以在操作中使用 parallel 方法将流转换为并行流,使用 sequential 方法将流转换为顺序流。
但是,切记,不要妄想使用者两个方法随时切换流的状态去控制每一个中间操作,因为,最后一次调用会影响整个流水线。
关于性能这个问题,我们潜意识会认为并行流的性能肯定是优于顺序流的,因为是并行执行嘛,但事实上真的如此吗?
来做一组测试
首先我们定义一个测试求和函数性能的方法:
/* 求前 n 个自然数的和,返回执行时间 */
public long sumPerfTesting(Function<Long, Long> sumFun, long n){
long fastest = Long.MAX_VALUE;
/*
这里重复执行 5 次,返回最快的用时。
为什么呢?因为编译器在某些情况下需要预热,简单来说,就是因为某些时候,第一次执行速度会比较偏慢,后面的执行速度才会正常。
所以我们一般做性能测试是不取第一次运行结果的,因为误差较大,参考价值不大,
另外,多运行,取平均(有时候也会取最小或最大,根据情况而定)也是做性能测试的一个原则。
*/
for(int i = 0; i < 5; i++){
long start = System.nanoTime();
long sum = sumFun.apply(n);
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.println("Result: " + sum);
fastest = fastest > duration ? duration : fastest;
}
return fastest;
}
接下来,我们在 DoSum 类中定义三种求和的方法:分别是传统循环、顺序流、并行流,来做性能测试。
public class DoSum{
/* 迭代求和 */
public static long iteratorSum(long n) {
long result = 0;
for (long i = 1L; i <= n; i++) {
result += i;
}
return result;
}
/* 顺序流求和 */
public static long sequentSum(long n) {
return Stream.iterate(1L, i -> i + 1).limit(n).reduce(0L, Long::sum);
}
/* 并行流求和 */
public static long parallelSum(long n) {
return Stream.iterate(1L, i -> i + 1).limit(n).parallel().reduce(0L, Long::sum);
}
}
最后,来分别输出三中求和方法的执行时间
System.out.println("Iterator Sum : " + sumPerfTesting(DoSum::iteratorSum, 10_000_000));
System.out.println("Sequent Sum : " + sumPerfTesting(DoSum::sequentSum, 10_000_000));
System.out.println("Parallel Sum : " + sumPerfTesting(DoSum::parallelSum, 10_000_000));
输出结果如下:
Iterator Sum : 6
Sequent Sum : 119
Parallel Sum : 500
有没有觉得很不可思议?
迭代版本速度最快,是可以理解的,因为迭代是最底层的操作。但是,并行版本的耗时居然是顺序版本的近 5 倍,迭代版本的 80 倍之多。
这是为什么呢?
原因主要有两点:
-
频繁的拆装箱耗时过多。
iterator 生成的是装箱对象,每一个都需要拆箱后才可以求和,上千万次拆箱装箱的动作,耗时非常严重
-
数据的依赖造成并行的优势无法体现。
类似这种顺序累加的求和功能,每次都是使用上一次求和的结果与下一个元素相加并返回,也就是说每次的执行都依赖上一次的结果,所以无法体现出并行的优势。
综合所述,这个并行流相当于不仅始终在顺序执行,而且还有频繁的拆装箱耗时。
分析出以上两点原因,我们来做两个大胆的猜测:
猜测一: 使用之前提到的 Stream 原始类型流 LongStream 避免拆装箱操作,得到的最终耗时应该与顺序执行差不多。
猜测二: 使用 LongStream 避免拆装箱,并且使用 LongStream.rangeClosed 生成范围数字,并行的优势就会体现出来,耗时会大大减小。
接下来,验证一下刚才的猜测:
验证一:
修改 parallelSum
方法,使用 LongStream
生成流,返回 long
类型数字,避免拆装箱
/* 并行流求和 */
public static long parallelSum(long n) {
return LongStream.iterate(1L, i -> i + 1).limit(n).parallel().reduce(0L, Long::sum);
}
测试结果如下:
Iterator Sum : 6
Sequent Sum : 140
Parallel Sum : 167
可以看到,耗时 167 与 顺序指定 140 仅相差 27 ms,说明我们的猜测是正确的。
验证二:
修改 parallelSum
方法,使用 LongStream
的 rangeClosed
方法,生成独立数字范围返回,有利于并行执行。
/* 并行流求和 */
public static long parallelSum(long n) {
return LongStream.rangeClosed(1L, n).reduce(0L, Long::sum);
}
测试结果如下:
Iterator Sum : 6
Sequent Sum : 114
Parallel Sum : 6
终于,并行流的速度远快于顺序流了,甚至有时候还快于迭代速度。
通过上述例子可以看出,并行流的性能,还是很优秀的。但是,一定要注意正确的使用并行流。
并行流一旦使用的场景不对,轻则影响性能,耗时更慢(如上);重则影响数据,不仅耗时慢,还会导致返回错误的数据。
同样以求和为例,这次,我们使用一个共享累加器:
class Accumulator{
public long total = 0;
public void add(long value) {total += value;}
}
在 DoSum 中创建求和方法
public static sideEffectSum(long n){
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).forEach(accumulator::add);
return accumulator.total;
}
调用:
System.out.println("sideEffect Sum : " + sumPerfTesting(DoSum::sideEffectSum, 10_000_000));
结果:
Result: 50000005000000
Result: 50000005000000
Result: 50000005000000
Result: 50000005000000
Result: 50000005000000
sideEffect Sum : 7
貌似没有问题,来,将其修改为并行,再测试一次,
public static sideEffectSum(long n){
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add);
return accumulator.total;
}
调用:
System.out.println("sideEffectSum Sum : " + sumPerfTesting(DoSum::sideEffectSum, 10_000_000));
结果:
Result: 27850481869918
Result: 25948841007030
Result: 7263318432858
Result: 23731343106789
Result: 17107187037081
sideEffect Sum : 2
惊喜吗?速度是够快,可却反回了一堆毫无意义的错误数字,这是因为,多个线程同时访问累加器
这并不是一个原子操作,所以一定要避免这种情况。