Java8-Stream API
在Collection
Stream
这一节的代码在
ch03 包中
为什么我们需要一个新的数据处理抽象
在我的观点中,主要有两个原因:
Collection
API没有提供高层的概念来查询数据,所以开发者被迫为琐碎的工作编写很多重复的代码。 - 对
Collection
的并行操作在语言支持方面受到了限制。只能让开发者用 Java 的并发机制来让数据并行处理快速而有效。
在 Java8 之前的数据处理
看下面的代码并试图说出它的作用。
1 |
|
Java8 中的数据处理
如下所示,上述的代码可以通过Stream
API
1 |
|
- stream(): 通过在一个原始的集合上调用
stream
方法来创建一个流式管道流,而 tasks
就是 List<Task>
类型的。 - filter(Predicate
) : 这个操作从流中抽取符合断言的判定条件的元素。一旦你有了一个数据流,你可以在其上不调用或者多次调用中间操作。lambda表达式 task -> task.getType() == TaskType.READING
定义了一个断言来过滤所有的阅读任务。该 lambda 表达式的类型为 java.util.function.Predicate<Task>
。 - sorted(Comparator
) :这个操作返回一个根据由 lambda 表达式定义的比较器进行排序后的元素组成的数据流。在上面的例子中,这个比较器是 (t1, t2) -> t1.getTitle().length() - t2.getTitle().length()
。 - map(Function<T,R>): 这个操作对数据流中的元素都进行
Fuction<T,R>
的操作,并返回新的数据流。 - collect(toList()):
这个操作将经过各种操作处理后的数据流中的元素收集到一个列表中。
为什么 Java8 编写的代码更好
我认为
- Java8
的代码清晰地展现出开发者的意图,如过滤、排序等。 - 开发者通过
Stream API 的形式能够在一个高层的抽象上来表现出他们想要做什么,而不是他们如何来作。 - Stream API
为数据处理提供了一个统一的语言。现在当程序员讨论到数据处理时,它们将会有共同的词汇。当两个开发者谈论到 filter
方法时,你可以肯定他们都在使用一个数据过滤操作。 - 处理数据时不需要重复的代码。用户不需要写专门的
for
循环,也不用创建临时集合来存储数据。所有的工作都可以通过 Stream API 来完成。 - Stream
不会修改你原来的集合——它们是免于变化的。
什么是 Stream?
Stream
1 |
|
of
、generate
iterate
generate
Supplier
Supplier
generate
1 |
|
1 |
|
Java8stream
Collection vs Stream
让我们来详细讨论一下外部迭代和内部迭代,以及延迟求值。
外部迭代 vs 内部迭代
上面所示的代码中,Java8for-each
有些人可能会争论,在for-each
for-each
for-each
延迟计算
Stream
1 |
|
在上面所示的代码中,我们将一个数字流中的元素除以ArithmeticException
1 |
|
你将会得到类似下面的堆栈信息。
1 |
|
使用 Stream API
Stream API
中间操作是从已有的filter
、map
、sorted
结束操作是从collect(toList())
、forEach
、count
中间操作允许你来构建管道流,它们会在你调用结束操作时被执行。下面是
示例域
在这个教程中,我们将使用任务管理域来解释概念。我们的示例域有一个叫做Task
1 |
|
1 |
|
我们在这一节中不会讨论
Java8 的日期时间 API。现在只要把它当做可读性高的日期相关的 API 即可。
例子 1:找到所有的阅读任务并按照它们的创建日期排序
我们讨论的第一个例子是找到所有的阅读任务,并按照它们的创建日期进行排序。我们需要执行的操作如下:
- 过滤所有的任务来找到任务类型为
READING
的任务。 - 将过滤后的结果按照
createdOn
域进行排序。 - 获得每个任务的标题。
- 将结果的标题收集到一个列表中。
上面的四个操作可以被容易地转换为如下的代码。
1 |
|
- filter:允许你指定一个断言来从数据流中排除一些元素。断言
task->task.getType() ==TaskType.READING 选择了所有任务类型为 READING
的任务。 - sorted:允许你指定一个比较器来对数据流进行排序。在这个例子中,你根据创建的日期来进行排序。lambda
表达式 **(t1,t2)->t1.getCreatedOn().compareTo(t2.getCreatedOn())** 提供了 Comparator
函数式接口的 compare
方法的实现。 - map:这需要一个实现了
Function<? super T, ? extends R>
接口的 lambda 表达式来将一个数据流转化为另一个数据流。lambda 表达式 **task->task.getTitle()** 将一个任务转化为一个标题。 - collect(toList()):这是一个结束操作,它收集结果中的阅读任务的标题并放入列表。
我们可以像下面一样通过使用Comparator
comparing
1 |
|
从
Java8 开始,接口可以以静态方法和默认方法的形式来拥有方法的实现。这些内容在第一节中。
在上面展示的代码中,我们用了一个由Comparator
comparing
,它以Function
Function
Comparable
Task::getCreatedOn
Function<Task, LocalDate>
。
如下所示,通过使用复合函数,我们可以轻易地通过在比较器上调用reversed
1 |
|
例子 2:找到唯一的任务
假设我们的数据集中有重复的任务。如下所示,我们可以在数据流上使用distinct
1 |
|
distinct
equals
equals
例子 3:找到根据创建日期排序的前 5 名的阅读任务
limit
limit
1 |
|
limit
skip
1 |
|
例子 4:计算所有阅读任务的数量
为了得到所有阅读任务的数量,我们可以在数据流上使用count
1 |
|
例子 5:从所有的任务中找出所有不同的标签
为了找出所有不同的标签,我们需要进行如下的操作:
- 为每一个任务提取标签。
- 将所有的标签收集进一个数据流。
- 将重复的标签除去。
- 最后将收集的结果放入一个列表。
第一个和第二个操作可以通过在tasks
flatMap
flatMap
tasks.getTags().stream()
distinct
1 |
|
例子 6:检查是否所有的阅读任务都有books
标签
Stream APIallMatch
、anyMatch
、findFirst
findAny
。为了检查是否所有的阅读任务都一个名叫books
1 |
|
java8
anyMatch
1 |
|
例子 7:创建一个所有标题的总结
假设你想创建一个所有标题的总结。使用reduce
reduce
1 |
|
例子 8:与原始流一同工作
除了作用在对象上的通用的数据流,Java8
为了创建一个范围的值,我们可以使用range
1 |
|
rangeClosed
1 |
|
你也可以像下面这样通过iterate
1 |
|
为过滤掉无限数据流中所有的偶数,我们可以编写如下的代码。
1 |
|
我们可以像下面这样通过使用limit
1 |
|
例子 9:从 Arrays 来创建数据流
如下所示,你可以通过使用Arrays
stream
1 |
|
你也可像下面这样从一个数组特定的起始下标到结束下标来创建一个数据流。在这里,起始下标被包含在内,而结束下标没有。
1 |
|
并行数据流
你使用Stream
parallel
parallel
fork-join
API。默认地,它会将线程数上升到与主机collect
groupingBy
1 |
|
1 |
|
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "2")
fork-join
如下所示,另一个你可以使用到parallel
1 |
|
如果你要理解何时来使用并行数据流,我建议你阅读