几年前, Streams API随lambda表达式一起在Java 8中引入。 作为一个熟练的Java专家,我尝试在我的一些项目中使用此新功能,例如here和here 。 我不是很喜欢它,然后又回到了好的老房子里。 此外,我创建了装饰库Cactoos来取代Guava ,而Guava在很多地方都不是很好。
这是一个原始示例。 假设我们有一些来自某些数据源的测量值,它们都是零到一之间的数字:
Iterable<Double> probes;
现在,我们只需要显示其中的前10个,忽略零和一,然后将它们重新缩放为(0..100)
。 听起来很容易,对吧? 有三种方法可以实现:过程式,面向对象和Java 8方法。 让我们从过程的方式开始:
int pos = 0;
for (Double probe : probes) {if (probe == 0.0d || probe == 1.0d) {continue;}if (++pos > 10) {break;}System.out.printf("Probe #%d: %f", pos, probe * 100.0d);
}
为什么这是一种程序方式? 因为这势在必行。 为什么势在必行? 因为它是程序性的。 不,我在开玩笑。
这是当务之急,因为我们正在向计算机发出有关将哪些数据放入何处以及如何对其进行迭代的指令。 我们不是在声明结果,而是必须构建它。 它可以工作,但不是真正可扩展的。 我们无法参与该算法并将其应用于另一个用例。 我们不能真正轻松地对其进行修改,例如从两个来源而不是一个来源获取数字,等等。这是程序性的。 说够了。 不要这样
现在,Java 8为我们提供了Streams API ,该API应该提供一种实现此目的的功能方法。 让我们尝试使用它。
首先,我们需要创建一个Stream
的实例, Iterable
不允许我们直接获取它。 然后,我们使用流API来完成这项工作:
StreamSupport.stream(probes.spliterator(), false).filter(p -> p == 0.0d || p == 1.0d).limit(10L).forEach(probe -> System.out.printf("Probe #%d: %f", 0, probe * 100.0d));
这将起作用,但是将对所有探针说出Probe #0
,因为forEach()
不适用于索引。 目前是没有这样的事forEachWithIndex()
在Stream
界面的Java 8(和Java 9的太 )。 这是使用原子计数器的解决方法 :
AtomicInteger index = new AtomicInteger();
StreamSupport.stream(probes.spliterator(), false).filter(probe -> probe == 0.0d || probe == 1.0d).limit(10L).forEach(probe -> System.out.printf("Probe #%d: %f",index.getAndIncrement(),probe * 100.0d));
“那是怎么了?” 你可能会问。 首先,看看在Stream
接口中找不到正确的方法时,我们遇到麻烦的Stream
。 我们立即摆脱了“流式”范式,回到了良好的旧程序全局变量(计数器)。 其次,我们真的看不到那些filter()
, limit()
和forEach()
方法内部发生了什么。 它们如何工作? 该文档说,这种方法是“声明性的”,并且Stream
接口中的每个方法都返回某个类的实例。 他们是什么班? 只看这段代码,我们一无所知。
此流API的最大问题是接口Stream,它非常庞大!
这两个问题是联系在一起的。 此流API的最大问题是接口Stream
–很大。 在撰写本文时,有43种方法。 在单个界面中四十三! 从SOLID到后来的更严格的原则 ,这都违反了面向对象编程的每一个原则 。
实现相同算法的面向对象方式是什么? 这就是我如何用Cactoos做到的 ,这只是一个集合 原始 简单的Java类:
new And(new Mapped<Double, Scalar<Boolean>>(new Limited<Double>(new Filtered<Double>(probes,probe -> probe == 0.0d || probe == 1.0d),10),probe -> () -> {System.out.printf("Probe #%d: %f", 0, probe * 100.0d);return true;}),
).value();
让我们看看这里发生了什么。 首先, Filtered
装饰了我们的可迭代probes
以从中取出某些项。 注意, Filtered
实现了Iterable
。 然后, Limited
(也是一个Iterable
)仅取出前十个项目。 然后, Mapped
将每个探针转换为Scalar<Boolean>
的实例,该实例执行行打印。
最后, And
的实例遍历“标量”列表,并要求每个标量返回boolean
。 他们打印行并返回true
。 由于它是true
, And
使用下一个标量进行下一次尝试。 最后,其方法value()
返回true
。
但是,等等,没有索引。 让我们添加它们。 为了做到这一点,我们仅使用另一个名为AndWithIndex
类:
new AndWithIndex(new Mapped<Double, Func<Integer, Boolean>>(new Limited<Double>(new Filtered<Double>(probes,probe -> probe == 0.0d || probe == 1.0d),10),probe -> index -> {System.out.printf("Probe #%d: %f", index, probe * 100.0d);return true;}),
).value();
现在,我们将探针映射到Func<Integer, Boolean>
,而不是Scalar<Boolean>
Func<Integer, Boolean>
以使其接受索引。
这种方法的优点在于所有类和接口都很小,这就是为什么它们很容易组合的原因。 为了限制探针的迭代,我们用Limited
装饰它; 为了使它过滤,我们用Filtered
装饰它; 为了做其他事情,我们创建一个新的装饰器并使用它。 我们并没有像Stream
这样的单一接口。
最重要的是,装饰器是一种用于修改集合行为的面向对象的工具,而流是我什至找不到名称的其他东西。
PS顺便说一下,这就是在Guava的Iterables
的帮助下可以实现相同算法的方式:
Iterable<Double> ready = Iterables.limit(Iterables.filter(probes,probe -> probe == 0.0d || probe == 1.0d),10
);
int pos = 0;
for (Double probe : probes) {System.out.printf("Probe #%d: %f", pos++, probe * 100.0d);
}
这是一些面向对象和功能样式的怪异组合。
翻译自: https://www.javacodegeeks.com/2017/10/streams-vs-decorators.html