Java Stream
1. 概念说明
Stream
这个词很容易造成误解,这里先做一个澄清,Java中的流:
- 非IO流,非数据流,而是集合的功能增强
- 非集合元素,非数据结构,也不保存数据,但却为数据操作而生
其实流式操作在动态语言中并不少见,map/filter/reduce...
在动态语言中都是耳熟能详的用法,又叫迭代器。
所以,在Java中也类似。但Java中已经有Iterator
的概念,所以Java Stream又像一个高级的Iterator
,一个定义了map/filter/reduce
的接口。查看源码可以看到相关定义:
public interface Stream<T> extends BaseStream<T, Stream<T>> {
Stream<T> filter(Predicate<? super T> predicate);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
IntStream mapToInt(ToIntFunction<? super T> mapper);
...
T reduce(T identity, BinaryOperator<T> accumulator);
...
}
所以我对Java Stream的理解是,一个聚合操作(map/filter/reduce...
)的迭代器载体。也是函数式编程的一个时代产物。
2. 几个优点
一个新产物,必定是有很多好处的:
- 张口就来的代码简洁(一行代码高低多个
for
循环),操作简单,逻辑清晰,可读性高等blabla - 快速实现并行操作,只需用
parallelStream()
即可 - 其他好处在下面的使用讲解中感受
3. 使用介绍
流的使用分为三步:构造流->操作流->返回操作结果(集合/数组)
3.1 构造流
在实际操作中,构造流的过程往往只是一个过程态,这里为了方便讲解,会进行声明和实例化。
构造流有以下几种方法:
- 直接实例化(使用少)
- 数组/集合转化(少用最多)
- 基本数值型转化
// 直接实例化
Stream stream_1 = Stream.of("aaa", "bbb", "ccc");
// Aarray转
String[] strs = new String[]{"aaa", "bbb", "ccc"};
Stream stream_2 = Stream.of(strs);
Stream stream_3 = Arrays.stream(strs);
// List转
List<String> list = Arrays.asList(strs);
Stream stream_4 = list.stream();
// 基本数值型,三种包装类型IntStream、LongStream、DoubleStream
Stream stream_5 = IntStream.of(1, 2, 3); //跟直接实例化类似
Stream stream_6 = (Stream) IntStream.of(intArr);
3.2 操作流
将数据转化为流之后,就可以进入主题,进行各种流式操作了。
直接来一个小例子:将字符串数组转换为大写并打印
String[] strs = {"aaa", "bbb", "ccc"};
Stream.of(strs).map(String::toUpperCase).forEach(System.out::println);
/*
AAA
BBB
CCC
*/
一行代码轻松搞定!完事!
当然啦,没这么简单。虽然一行代码,但里面还是藏着很多知识点的。
第一个问题:为什么要用forEach
不用map
Stream.of(strs).map(String::toUpperCase).map(System.out::println);
在javascript
中,a.map(e=>e.toUpperCase()).map(console.log)
这是允许的。
而到了Java,上面的代码将会编译报错。
Error:(43, 54) java: 不兼容的类型: 无法推断类型变量 R
(参数不匹配; 方法引用中的返回类型错误
void无法转换为R)
其实,在流式操作里面又分为下面两种:
- Intermediate: 中间。可以理解为对数据的操作,这种传的
lambda
表达式必须有返回值。就是我们常用的map/filter/reduce
- Terminal: 终止。可以理解为结束,操作完就结束了,无法继续操作了,这种传的
lambda
表达式必须没有返回值。比如我们用得最多的forEach
回到上面的例子:
- 编译报错就是因为最后一个
map
(中间操作)的参数是没有返回值的,因此要使用forEach - 而javascript中为什么又可以呢? 这是因为javascript中每一条语句都有一个返回值,
console.log
返回的是undefined
。
3.3 转换流
关键的操作都结束了,最后就是要使用流式操作结束的数据了。
上一节提到的Terminal
操作类型就是一个最常用的使用。可能不太好理解。
翻译一下
- 比如
.forEach(System.out::println)
就是对操作完的数据(转大写)进行使用(打印) - 还有最常用的将操作完的数据转成列表
.collect(Collectors.toList())
3.4 补充
在测试过程中,多写了一行代码:
String[] strs = new String[]{"aaa", "bbb", "ccc"};
Stream<String> stream = Stream.of(strs);
stream.map(String::toUpperCase).forEach(System.out::println);
stream.filter(e-> e.contains("a")).forEach(System.out::println);
结果:运行时报错:IllegalStateException: stream has already been operated upon or closed
知识点+1:
流在使用结束(即Terminal
)之后,这个流就已经关闭了。所以无法对一个流进行两次Terminal
运算。
而我们日常合并使用的filter().map()
的操作又是允许的,因为Intermediate
的操作结束其实还是返回一个流的。
总结
最后再说两句:
为什么同样是聚合操作,在js/python等动态语言中可以直接使用,而在Java中要再封装一个Stream
呢。
个人猜想,在Java 7里面已经有个各种各样的集合类,现在要对集合进行功能扩展,增加聚合才做的静态函数。
有两种选择:
Iterator
新增map/filter
等多个接口,每个集合类中去实现接口- 新增一个
Stream
类,Iterator
新增转化为Stream
的接口
Java 8自然选择了后者。
因此,可以把Java中的Stram
理解成一个包装。你要使用,就得按照它的包装规则来。
比如你要参加万圣节派对,就得先化一个可爱的妆。派对结束,你就可以卸妆了(当然也可以不卸)。
你要游泳,就得带泳装;
你要上班,就得带工卡;
你要编程,就得秃头…
Java Stram
还有很多更高级的用法,比如parallelStream
,比如Short-circuiting
,比如flatMap
等等,大家有兴趣的可以自行学习。
如有错误,欢迎指正~