[英]java 8 streams: complex stream processing
我想创建一种对流执行一些复杂操作的方法(例如,替换第7个元素, 删除最后一个元素, 删除相邻的重复项等),而不缓存整个流。
但是什么流API可以让我插入此方法? 我是否必须创建自己的收集器,以便在收集时将物品发射到其他流? 但这会改变数据流方向,从拉到推,对吧?
这种方法的可能签名是什么?
Stream<T> process(Stream<T> in)
可能是不可能的(在单线程代码中),因为只有在收集整个输入流之后才能返回结果
另一个想法:
void process(Stream<T> in, Stream<T> out)
也似乎有点缺陷,因为java不允许发出将项目插入现有流(作为out
参数提供)的声明。
那么我该如何在Java中进行一些复杂的流处理呢?
您用作示例的复杂操作全部遵循对流中一个元素的操作模式,具体取决于流中的其他元素。 Java流经过专门设计,不允许在没有收集或精简的情况下进行这些类型的操作。 流操作不允许直接访问其他成员,通常,具有副作用的非终端操作是个坏主意。
请注意Stream
javadoc中的以下内容:
集合和流虽然具有一些表面上的相似性,但它们具有不同的目标。 馆藏主要涉及对其元素的有效管理和访问。 相比之下,流不提供直接访问或操纵其元素的方法,而与声明性地描述其源以及将在该源上聚合执行的计算操作有关。
进一步来说:
大多数流操作接受描述用户指定行为的参数。为了保留正确的行为,这些行为参数:
必须是无干扰的(它们不修改流源); 并且在大多数情况下必须是无状态的(它们的结果不应依赖于在流管道执行期间可能改变的任何状态)。
和
如果流操作的行为参数是有状态的,则流管线结果可能不确定或不正确。 有状态的lambda(或其他实现适当功能接口的对象)是一种有状态的lambda,其结果取决于流管道执行期间可能更改的任何状态
在https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html和http:// docs中很好地描述了中间和终端无状态及有状态操作的所有复杂性.oracle.com / javase / 8 / docs / api / java / util / stream / Stream.html
这种方法既有优点也有缺点。 一个显着的优点是它允许并行处理流。 一个重大的缺点是,在某些其他语言中容易执行的操作(例如,跳过流中的每个第三个元素)在Java中很难实现。
请注意,您将看到很多代码(包括SO上公认的答案),忽略了有关流操作的行为参数应为无状态的建议。 为了工作,此代码依赖于语言规范未定义的Java实现的行为:即按顺序处理流。 规范中没有什么可以阻止Java处理元素以相反顺序或随机顺序实现。 这样的实现将使任何有状态的流操作立即表现出不同的行为。 无状态操作将继续表现完全相同。 因此,总而言之,有状态操作依赖于Java 实现的细节而不是规范 。
还请注意,可以进行安全的有状态中间操作。 需要对它们进行设计,以使它们明确不依赖于处理元素的顺序。 Stream.distinct
和Stream.sorted
就是很好的例子。 它们需要保持工作状态,但是设计目的是不管处理元素的顺序如何。
因此,要回答您的问题,可以在Java中完成这些类型的操作,但是它们不简单,安全(出于上一段给出的原因),也不自然适合于语言设计。 我建议使用缩减或收集或(请参见Tagir Valeev的答案)分离器创建新的流。 或者使用传统迭代。
您可以调用并返回任何标准流操作,例如filter
, map
, reduce
等,然后让它们执行一些复杂的操作,例如需要外部数据的操作。 例如, filterAdjacentDuplicates
和replaceNthElement
可以实现这样的:
public static <T> Stream<T> filterAdjacentDupes(Stream<T> stream) {
AtomicReference<T> last = new AtomicReference<>();
return stream.filter(t -> ! t.equals(last.getAndSet(t)));
}
public static <T> Stream<T> replaceNthElement(Stream<T> stream, int n, T repl) {
AtomicInteger count = new AtomicInteger();
return stream.map(t -> count.incrementAndGet() == n ? repl : t);
}
用法示例:
List<String> lst = Arrays.asList("foo", "bar", "bar", "bar", "blub", "foo");
replaceNthElement(filterAdjacentDupes(lst.stream()), 3, "BAR").forEach(System.out::println);
// Output: foo bar BAR foo
但是,正如注释中指出的那样,这实际上并不是应该使用Stream API的方式。 特别地,当给定并行流时,诸如这两种操作将失败。
正确的方法(尽管不是很容易)是编写自己的Spliterator
。 常用算法如下:
stream.spliterator()
获取现有的Stream Spliterator StreamSupport.stream(spliterator, stream.isParallel())
基于您的分隔器创建一个新流 close()
调用委托给原始流,如.onClose(stream::close)
。 编写良好的并行化良好的分离器通常是一项非常艰巨的任务。 但是,如果您不关心并行化,则可以将AbstractSpliterator
子类AbstractSpliterator
,这更简单。 这是一个示例,该示例如何编写新的Stream操作以删除给定位置的元素:
public static <T> Stream<T> removeAt(Stream<T> src, int idx) {
Spliterator<T> spltr = src.spliterator();
Spliterator<T> res = new AbstractSpliterator<T>(Math.max(0, spltr.estimateSize()-1),
spltr.characteristics()) {
long cnt = 0;
@Override
public boolean tryAdvance(Consumer<? super T> action) {
if(cnt++ == idx && !spltr.tryAdvance(x -> {}))
return false;
return spltr.tryAdvance(action);
}
};
return StreamSupport.stream(res, src.isParallel()).onClose(src::close);
}
这是最少的实现,可以进行改进以显示更好的性能和并行性。
在我的StreamEx库中,我尝试通过headTail
简化此类自定义流操作的headTail
。 这是使用StreamEx
进行相同操作的StreamEx
:
public static <T> StreamEx<T> removeAt(StreamEx<T> src, int idx) {
// head is the first stream element
// tail is the stream of the rest elements
// want to remove first element? ok, just remove tail
// otherwise call itself with decremented idx and prepend the head element to the result
return src.headTail(
(head, tail) -> idx == 0 ? tail : removeAt(tail, idx-1).prepend(head));
}
您甚至可以使用chain()
方法支持链接:
public static <T> Function<StreamEx<T>, StreamEx<T>> removeAt(int idx) {
return s -> removeAt(s, idx);
}
用法示例:
StreamEx.of("Java 8", "Stream", "API", "is", "not", "great")
.chain(removeAt(4)).forEach(System.out::println);
最后要注意的是,即使没有headTail
也有一些方法可以使用StreamEx解决您的问题。 要删除特定索引,您可以使用递增的数字进行压缩,然后像这样过滤和删除索引:
StreamEx.of(stream)
.zipWith(IntStreamEx.ints().boxed())
.removeValues(pos -> pos == idx)
.keys();
要折叠相邻的重复项,有专用的collapse
方法(甚至可以很好地并行化!):
StreamEx.of(stream).collapse(Object::equals);
在此问题/更新2中表达的tobias_k答案和思想的基础上,我们可能只返回捕获其局部变量的适当谓词和Map函数。 (因此,这些函数是有状态的,这对于流而言并不理想,但是流API中的distinct()
方法也可能是有状态的)。
这是修改后的代码:
public class Foo {
public static void run() {
List<String> lst = Arrays.asList("foo", "bar", "bar", "bar", "blub", "foo");
lst.stream()
.filter(Foo.filterAdjacentDupes())
.map(Foo.replaceNthElement(3, "BAR"))
.forEach(System.out::println);
// Output: foo bar BAR foo
}
public static <T> Predicate<T> filterAdjacentDupes() {
final AtomicReference<T> last = new AtomicReference<>();
return t -> ! t.equals(last.getAndSet(t));
}
public static <T> UnaryOperator<T> replaceNthElement(int n, T repl) {
final AtomicInteger count = new AtomicInteger();
return t -> count.incrementAndGet() == n ? repl : t;
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.