Java在管道中生成自定义流

时间:2019-05-24 09:27:46

标签: java java-stream

我需要消耗一堆消息并生成通知。假设传入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)

1 个答案:

答案 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特征时,它将是一个精确的大小),上面的示例基本上将从源流的分离器中获取。我将其留给读者作为练习,为什么在源流中存在SIZEDDISTINCT特性的情况下也会被删除。

将通过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的队列,立即使用第一个未处理的元素而无需排队,但这会使代码显着复杂化