我正在努力探索一些Java 8 Stream功能。三十年前,我已经写了一些Lisp了,我对FP非常熟悉,我想我可能会尝试做一些这个新设施并非真正针对的东西。无论如何,如果问题是愚蠢的,我很高兴能够了解我的方式的错误。
我会提出一个具体的问题,虽然这是我试图解决的一般概念。
假设我想从Stream的每个第三个元素获取一个Stream。在常规FP中,我将(大致)创建一个递归函数,该函数通过在删除两个元素之后将列表的第一个元素与列表的其余部分(call-thyself)连接起来进行操作。很容易。但要在流中执行此操作,我觉得我想要两种工具之一:
1)让一个操作从流中提取多个项目进行处理的方法(然后我只需要抓三个,使用第一个,然后转储其余的)
2)制作供应商的方法,该供应商接收物品和流并创建流。然后我觉得我可以从第一个项目和缩短的流中创建一个下游流,虽然我仍然不清楚这是否会为实际工作做必要的递归魔法。
开始编辑
所以,有一些有趣且有用的反馈;感谢大家。特别是,这些评论帮助我澄清了我的脑袋试图更好地解决的问题。
首先,人们可以 - 在概念上,至少 - 拥有/需要序列中的顺序知识不应该阻止人们允许完全可并行化的操作。想到了一个例子,那就是图形人员倾向于做的卷积操作。想象一下模糊图像。每个像素都靠近它的像素进行修改,但这些像素本身只能被读取,而不能被修改。
这是我的理解(在这个阶段非常不稳定,当然!)流机制是VM管理并行性的奇妙世界的主要入口点,迭代器仍然是它们一直以来的东西(是的) ?不?)如果这是正确的,那么使用迭代器来解决我不知道的问题域并不是很好。
所以,在这一点上,至少,创建一个分块分裂器的建议似乎是最有希望的,但男孩,支持该示例的代码看起来像是艰苦的工作!我认为我更倾向于使用ForkJoin机制,尽管它是"老帽子"现在:)
无论如何,仍然对人们希望提供的更多见解感兴趣。
结束编辑
有什么想法?我是否尝试使用这些Streams来做他们不打算做的事情,或者我错过了一些明显的东西?
干杯, 托比。
答案 0 :(得分:1)
要记住的一点是Stream
主要是为了利用并行处理的一种方式。这意味着它们具有与它们相关联的许多条件,这些条件旨在使VM以任何方便的顺序自由地处理元素。这方面的一个例子是坚持减少函数是关联的。另一个是被操纵的局部变量是最终的。这些类型的条件意味着可以按任何顺序评估和收集流项目。
这样做的一个自然结果是Stream
的最佳用例不涉及流的值之间的依赖关系。诸如将整数流映射到它们的累积值之类的事情在像LISP这样的语言中是微不足道的,但是对于Java流非常不自然(参见this question)。
通过使用sequential
迫使Stream
不平行,有一些聪明的方法来解决其中一些限制,但我的经验是,这些问题比它们的价值更麻烦。如果您的问题涉及基本上连续的一系列项目,其中需要状态来处理值,那么我建议使用传统的集合和迭代。代码最终会变得更清晰,并且无论如何都无法并行化,因此代码将不会更糟。
说了这么多,如果你真的想要这样做,那么最直接的方法是让一个收集器存储每三个项目,然后再将它们作为一个流发送出去:
class EveryThird {
private final List<Integer> list = new ArrayList<>();
private int count = 0;
public void accept(Integer i) {
if (count++ % 3 == 0)
list.add(i);
}
public EveryThird combine(EveryThird other) {
list.addAll(other.list);
count += other.count;
return this;
}
public Stream<Integer> stream() {
return list.stream();
}
}
然后可以使用它:
IntStream.range(0, 10000)
.collect(EveryThird::new, EveryThird::accept, EveryThird::combine)
.stream()
但这并不是收藏家的设计目标,而且由于它不必要地收集了流,因此效率很低。如上所述,我的建议是在这种情况下使用传统迭代。
答案 1 :(得分:1)
我的StreamEx库增强了标准的Stream API。特别是它添加了headTail
方法,该方法允许递归定义自定义操作。它需要一个接收流头(第一个元素)和尾部(其余元素的流)的函数,并且应该返回将使用的结果流而不是原始流。例如,您可以按如下方式定义every3
操作:
public static <T> StreamEx<T> every3(StreamEx<T> input) {
return input.headTail(
(first, tail1) -> tail1.<T>headTail(
(second, tail2) -> tail2.headTail(
(third, tail3) -> every3(tail3))).prepend(first));
}
这里也使用了prepend
,它只是将给定元素添加到流中(此操作只是headTail
的最好朋友。
通常使用headTail
,您几乎可以定义所需的任何中间操作,包括现有操作和新操作。您可以找到一些示例here。
请注意,我实现了一些在这种递归操作定义中优化尾部的机制,因此正确定义的操作在处理长流时不会占用整个堆栈。
答案 2 :(得分:0)
Java的流与FP(懒惰)序列完全不同。如果你熟悉Clojure,那么差别就像 lazy seq 和 reducer 之间的区别。虽然 lazy seq 分别对每个元素进行处理,因此允许获得单独处理的元素,但 reducer 会在单个原子操作中折叠整个序列。
特别是对于您所描述的示例,请考虑依赖于流分区转换,如详细描述here。然后你会轻松做到
partition(originalStream, 3).map(xs -> xs.get(0));
导致流中包含原始的每个第三个元素。
这将保持效率,懒惰,和可并行性。
答案 3 :(得分:0)
您可以BatchingSpliterator使用(或查看来源)
然后给定aStream
你可以创建一个由size = 3的列表组成的流(除了可能是最后一个)并使用该列表的第一个元素
Stream<T> aStream = ...;
Stream<List<T>> batchedStream =
new BatchingSpliterator.Builder().wrap(aStream).batchSize(3).stream();
batchedStream.map(l -> l.get(0) ). ...
你也可以“平行”:
batchedStream.parallel().map(l -> l.get(0) ). ....