[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-02uVmfnf-1628481106899)(http://ohwsf74ph.bkt.clouddn.com/image/banner/java8-logo.jpeg)]
本文首发于一书生VOID的博客。
原文链接:Java 8新特性(二):Stream API
本篇介绍Java 8的另一个新特性——Stream API。新增的Stream API与InputStream
和OutputStream
是完全不同的概念,Stream API是对Java中集合操作的增强,可以利用它进行各种过滤、排序、分组、聚合等操作。
Stream API配合Lambda表达式可以加大的简化代码,提升可读性。Stream API也支持并行操作(类似于Fork-Join),甚至不用手动编写多线程代码,Stream API已经帮我们做好了,并且能充分利用多核CPU的优势。借助Stream API和Lambda表达式,可以很容易的编写出高性能的并发处理程序。
Stream API简介
Stream API是Java 8中加入的一套新的API,主要用于处理集合操作,不过它的处理方式与传统的方式不同,称为“数据流处理”。流(Stream)类似于关系数据库的查询操作,是一种声明式操作。比如要从数据库中获取所有年龄大于20岁的用户的名称,并按照用户的创建时间进行排序,用一条SQL语句就可以搞定,不过使用Java程序实现就会显得有些繁琐,这时候可以使用流:
List<String> userNames =
users.stream()
.filter(user -> user.getAge() > 20)
.sorted(comparing(User::getCreationDate))
.map(User::getUserName)
.collect(toList());
可以把流跟集合做一个比较。在Java中,集合是一种数据结构,或者说是一种容器,用于存放数据,流不是容器,它不关心数据的存放,只关注如何处理。可以把流当做是Java中的Iterator
,不过它可比Iterator
强大多了。
流与集合另一个区别在于他们的遍历方式,遍历集合通常使用for-each
方式,这种方式称为外部迭代,而流使用内部迭代方式,也就是说它帮你把迭代的工作做了,你只需要给出一个函数来告诉它接下来要干什么:
// 外部迭代
List<String> list = Arrays.asList("A", "B", "C", "D");
for (String str : list) {
System.out.println(str);
}
// 内部迭代
list.stream().forEach(System.out::println);
在一些比较复杂的业务场景中,要对集合做一些统计、分组的操作,如果用传统的for-each
方式遍历集合,每次只能处理一个元素,并且是按顺序处理,这种方法是极其低效的。你可能会想到用多线程去并行处理,但是编写多线程代码并非易事,容易出错并且维护困难。不过在Java 8之后,你可以使用Stream API来处理这些操作。
Stream API将迭代操作封装到了内部,它会自动的选择最优的迭代方式,并且使用并行方式处理时,将集合分成多段,每一段分别使用不同的线程处理,最后将处理结果合并输出,有点类似于Fork-Join操作。
需要注意的是,流只能遍历一次,遍历结束后,这个流就被关闭掉了。如果要重新遍历,可以从数据源(集合)中重新获取一个流。如果你对一个流遍历两次,就会抛出java.lang.IllegalStateException
异常:
List<String> list = Arrays.asList("A", "B", "C", "D");
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println); // 这里会抛出java.lang.IllegalStateException异常,因为流已经被关闭
流通常由三部分构成:
- 数据源:数据源一般用于流的获取,比如本文开头那个过滤用户的例子中
users.stream()
方法。 - 中间处理:中间处理包括对流中元素的一系列处理,如:过滤(
filter()
),映射(map()
),排序(sorted()
)。 - 终端处理:终端处理会生成结果,结果可以是任何不是流值,如
List<String>
;也可以不返回结果,如stream.forEach(System.out::println)
就是将结果打印到控制台中,并没有返回。
创建流
创建流的方式有很多,具体可以划分为以下几种:
由值创建流
使用静态方法Stream.of()
创建流,该方法接收一个变长参数:
Stream<Stream> stream = Stream.of("A", "B", "C", "D");
也可以使用静态方法Stream.empty()
创建一个空的流:
Stream<Stream> stream = Stream.empty();
由数组创建流
使用静态方法Arrays.stream()
从数组创建一个流,该方法接收一个数组参数:
String[] strs = {
"A", "B", "C", "D"};
Stream<Stream> stream = Arrays.stream(strs);
通过文件生成流
使用java.nio.file.Files
类中的很多静态方法都可以获取流,比如Files.lines()
方法,该方法接收一个java.nio.file.Path
对象,返回一个由文件行构成的字符串流:
Stream<String> stream = Files.lines(Paths.get("text.txt"), Charset.defaultCharset());
通过函数创建流
java.util.stream.Stream
中有两个静态方法用于从函数生成流,他们分别是Stream.generate()
和Stream.iterate()
:
// iteartor
Stream.iterate(0, n -> n + 2).limit(51).forEach(System.out::println);
// generate
Stream.generate(() -> "Hello Man!").limit(10).forEach(System.out::println);
第一个方法会打印100以内的所有偶数,第二个方法打印10个Hello Man!
。需要注意的是,这两个方法生成的流都是无限流,没有固定大小,可以无穷的计算下去,在上面的代码中我们使用了limit()
来避免打印无穷个值。
一般来说,iterate()
用于生成一系列值,比如生成以当前时间开始之后的10天的日期:
Stream.iterate(LocalDate.now(), date -> date.plusDays(1)).limit(10).forEach(System.out::println);
generate()
方法用于生成一些随机数,比如生成10个UUID:
Stream.generate(() -> UUID.randomUUID().toString(