从 Java 8 流中删除匹配模式的序列

remove sequence matching a pattern from a Java 8 stream

我正在发现 Java 8,尤其是 Stream 的使用,它看起来非常强大。不过,我在表达查询时遇到了问题。

我有一个要分析的事件对象列表。我想识别此列表中的一些不良模式(事件序列),应该将其删除。

基本上,一个事件对象有 3 个字段:

如果我有 2 个事件与相同的来源和相同的指标相关,但它们是相反的(例如,一个是 "VMHighCpu" 类型,另一个是 "VMLowCpu" 类型),我想从我的列表中删除这两个事件。

我尝试了几件事都没有成功...

    // Simple query
    Map<Element, List<EventToAnalyze>> bySource = (Map) eventsToPurge.stream().collect(Collectors.groupingBy(EventToAnalyze::getSource));

    // Another attempt
    Map<Element, List<EventToAnalyze>> bySourceWithFilter = (Map) eventsToPurge.stream().filter(e -> e.getEventName().contains("Low")).collect(Collectors.groupingBy(EventToAnalyze::getSource));

    // Last attempt
    Map<Element, List<EventToAnalyze>> bySourceByMetric = (Map) eventsToPurge.stream().collect(Collectors.groupingBy(
                                    EventToAnalyze::getSource, Collectors.groupingBy(
                                                    EventToAnalyze::getMetricName, Collectors.groupingBy(
                                                                    EventToAnalyze::getEventName))));

希望我的解释清楚。

你想做的事情使用 Streams 做起来有些困难,因为大多数 Streams 操作发生在流中的每个单独值上,独立于其他值。有像 distinctsort 这样的有状态操作,但这些操作有些不寻常,无法自定义。

您可以编写自己的有状态操作(类似于 )。在这种情况下,它将是一个有状态的平面映射器,但我不清楚如何让它工作。

这是一种基于数组的替代方法。它假定您可以随机访问事件。你说你有一个事件列表,所以我希望是这样。为了简单起见,让我们这样设置:

enum Event {
    HIGH, NORMAL, LOW
}

我们需要一个函数来获取两个事件并确定它们是否与要删除的模式相匹配:

boolean match(Event e1, Event e2) {
    return e1 == Event.HIGH && e2 == Event.LOW
        || e1 == Event.LOW && e2 == Event.HIGH;
}

请注意,这与 BiPredicate<Event> 功能接口匹配。

作为最后的设置,让我们介绍一个帮助器,它在给定子范围内的数组的每个索引上调用 lambda 函数。这就像 Arrays.setAll 除了它需要一个子范围,并且它在 boolean[].

上运行
void ArraySetRange(boolean[] array, int start, int end, IntPredicate op) {
    IntStream.range(start, end).forEach(i -> array[i] = op.test(i));
}

现在设置已完成。主要任务是什么?给定一个事件列表,我们想要删除匹配某种模式的事件序列,以及 return 一个事件列表:

List<Event> remove(List<Event> input, BiPredicate<Event,Event> matcher) {
    ...

我们要做的第一件事是 运行 遍历数组并找到匹配条件的一对事件的所有出现:

    int n = input.size();
    boolean[] flags = new boolean[n];
    ArraySetRange(flags, 1, n, i -> matcher.test(input.get(i-1), input.get(i)));

这会将布尔 true 值放在数组中此事件及其左侧的事件与模式匹配的每个位置。请注意,模式可以重叠。我们跳过数组的第一个元素,因为它的左边没有任何东西。

但是我们想删除整个模式。对于每个事件,如果它右边的元素是模式的右端,我们也想删除这个元素。那是另一个数组操作,但这次是对除最后一个以外的所有数组索引进行操作:

    ArraySetRange(flags, 0, n-1, i -> flags[i] || flags[i+1]);

(请注意,这会根据输入数组中的值修改输入数组。如果处理是从左到右的顺序,则此方法有效,但如果我们想并行执行此操作,最好存储结果放入一个单独的数组。)

现在我们有一个数组 flags,其中 true 表示存在于我们要删除的模式中。我们可以使用一个简单的过滤操作来做到这一点:

    return IntStream.range(0, n)
        .filter(i -> ! flags[i])
        .mapToObj(input::get)
        .collect(toList());

完整的例子在这里:

List<Event> remove(List<Event> input, BiPredicate<Event,Event> matcher) {
    int n = input.size();
    boolean[] flags = new boolean[n];
    ArraySetRange(flags, 1, n, i -> matcher.test(input.get(i-1), input.get(i)));
    ArraySetRange(flags, 0, n-1, i -> flags[i] || flags[i+1]);
    return IntStream.range(0, n)
        .filter(i -> ! flags[i])
        .mapToObj(input::get)
        .collect(toList());
}

你会这样称呼它:

    List<Event> purgedEvents = remove(eventsToPurge, this::match);

我暗暗怀疑这不是您想要的。这将很好地删除孤立的对:

NORMAL, HIGH, LOW, NORMAL → NORMAL, NORMAL

但如果三个 "opposite" 事件连续发生,它们将全部被删除:

NORMAL, HIGH, LOW, HIGH, NORMAL → NORMAL, NORMAL

如果有一系列事件并非完全相反,一些将被删除,但相反的部分可能会保留在信息流中:

NORMAL, HIGH, HIGH, LOW, LOW, NORMAL → NORMAL, HIGH, LOW, NORMAL

这取决于您要删除的模式的具体规范,但我相信您可以通过调整布尔数组的处理来完成您想要做的大部分事情。