我需要消耗一堆消息并生成通知。假设传入Stream<Message>
,我的处理器对其进行处理,然后根据一些计算生成Stream<Notification>
。这不是一个简单的映射操作,处理器具有状态,它需要记住一定数量的先前消息,计算滑动平均值并识别其他一些模式才能生成通知流。
我无法从Stream中间操作中找到合适的操作-filter()
,map()
等。一种方法是使用forEach()
。但这是终端操作,我无法生成结果流并将其流水线化。
我是Java流的新手,我想知道如何使用Java流模型来实现上述目标。
流量:
Stream<Message> ---> (Notification processor) ---> Stream<Notification> ---> ...
编辑:
我还没有真正开始实施,但是我可以想象代码会像这样:
public class NotificationProcessor {
@Autowired
private Averager averager;
@Autowired
private TrendAnalyser trendAnalyser;
private long prevNotificationTime;
public void consume(Message message) {
if (message.getRate() > averager.getAverage() + THRESHOLD) {
// Generate notification A here
}
// Adjust the moving average
averager.put(message);
trendAnalyser.analyze(message);
if (trendAnalyser.isFalling()) {
Date now = new Date();
// Throttle
if (now.getTime() - prevNotificationTime > 60) {
prevNotificationTime = now.getTime();
// Generate notification B here
}
}
}
}
这只是消耗消息的传统Java类。我仍在学习Stream模型,所以不确定如何将类连接到它。
更多修改:
Holger的方法非常整洁而扎实,我认为这是一个非常好的设计。但是,后来我发现我可以使用一个类来跟踪状态并在Stream.map()
NotificationProcessor processor;
stream.map(s -> processor.consume(s)).filter(s -> s != null)
答案 0 :(得分:4)
不适合功能API的自定义操作可以通过Spliterator
接口来实现。
举个简单的例子,以下操作会将String
元素与其前一个元素(如果非null
连接起来:
public static Stream<String> concatWithPrevious(Stream<String> source) {
boolean parallel = source.isParallel();
Spliterator<String> sp = source.spliterator();
return StreamSupport.stream(new Spliterators.AbstractSpliterator<String>(
sp.estimateSize(),
sp.characteristics()&~(Spliterator.DISTINCT|Spliterator.SORTED)) {
private String previous;
@Override
public boolean tryAdvance(Consumer<? super String> action) {
return sp.tryAdvance(s -> {
String p = previous;
previous = s;
action.accept(p == null? s: s == null? p: p.concat(s));
});
}
}, parallel).onClose(source::close);
}
中心元素是tryAdvance
方法,该方法必须用下一个元素调用Consumer
的{{1}}方法,如果有则返回accept
,或者如果已到达流的末尾,则只需返回true
。
还有一些特征和一个估计的大小(当存在false
特征时,它将是一个精确的大小),上面的示例基本上将从源流的分离器中获取。我将其留给读者作为练习,为什么在源流中存在SIZED
和DISTINCT
特性的情况下也会被删除。
将通过SORTED
方法启用并行处理,该方法将从此处的trySplit
继承。这种方法会将元素缓冲到数组中,效率不是很高,但是对于对前一个元素有这种依赖性的分离器,这是我们能得到的最好的结果。
当我们使用此示例运行
时AbstractSpliterator
我们得到
concatWithPrevious(
IntStream.range('A', 'Z')
.mapToObj(i -> String.valueOf((char)i))
.peek(s -> System.out.println("source stream: "+s))
)
.filter(Predicate.isEqual("EF"))
.findFirst()
.ifPresent(s -> System.out.println("result: "+s));
表明流的惰性仍然存在。
以您的任务示例草图为例,我考虑将代码更改为
source stream: A
source stream: B
source stream: C
source stream: D
source stream: E
source stream: F
result: EF
并将其用于类似Stream的操作
public class NotificationProcessor {
@Autowired
private Averager averager;
@Autowired
private TrendAnalyser trendAnalyser;
private long prevNotificationTime;
public void consume(Message message, Queue<Notification> queue) {
if (message.getRate() > averager.getAverage() + THRESHOLD) {
// Generate notification A here
queue.add(…);
}
// Adjust the moving average
averager.put(message);
trendAnalyser.analyze(message);
if (trendAnalyser.isFalling()) {
Date now = new Date();
// Throttle
if (now.getTime() - prevNotificationTime > 60) {
prevNotificationTime = now.getTime();
// Generate notification B here
queue.add(…);
}
}
}
}
由于每个源元素可能生成零到两个元素,因此不可能有public static Stream<Notification> notificationProcessor(Stream<Message> source) {
// replace with intended factory mechanism or make it a parameter
NotificationProcessor proc = new NotificationProcessor();
boolean parallel = source.isParallel();
Spliterator<Message> sp = source.spliterator();
return StreamSupport.stream(new Spliterators.AbstractSpliterator<Notification>(
sp.estimateSize(),
sp.characteristics() & Spliterator.ORDERED | Spliterator.NONNULL) {
final Queue<Notification> queue = new ArrayDeque<>(2);
@Override
public boolean tryAdvance(Consumer<? super Notification> action) {
while(queue.isEmpty()) {
if(!sp.tryAdvance(msg -> proc.consume(msg, queue))) {
return false;
}
}
action.accept(queue.remove());
return true;
}
}, parallel).onClose(source::close);
}
特征,实际上,我决定在这里保守一点,只保留SIZED
特征,您可以表示与您的操作有关,并添加了ORDERED
,这似乎适合您的代码。
由于每个NONNULL
调用仅在到达流末尾时才提供一个元素或不提供任何元素,因此队列最多需要两个元素¹。如果队列为空,将查询源,直到至少有一个元素或到达源的末尾。然后,如果队列中有一个元素,则将下一个元素传递给使用者。
¹我们可以在此处使用大小为1的队列,立即使用第一个未处理的元素而无需排队,但这会使代码显着复杂化