如何在流上重用过滤器和地图的应用?

2022-09-04 07:56:37

我有一组从共享类型(即 , )继承的域对象。子类型具有特定属性(即 、 )。GroupRecord extends RecordRequestRecord extends RecordGroupRecord::getCumulativeTimeRequestRecord::getResponseTime

此外,由于解析日志文件,我有一个具有混合子类型的记录列表。

List<Record> records = parseLog(...);

为了计算日志记录的统计信息,我想仅对与特定子类型匹配的记录子集(即仅在 s 上)应用数学函数。因此,我希望有一个特定子类型的过滤流。我知道我可以使用GroupRecordfiltermap

records.stream()
       .filter(GroupRecord.class::isInstance)
       .map(GroupRecord.class::cast)
       .collect(...

在流上多次应用此 filter&cast(尤其是当对同一子类型多次执行此筛选器以进行不同计算时)不仅很麻烦,而且会产生大量重复。

我目前的方法是使用TypeFilter

class TypeFilter<T>{

    private final Class<T> type;

    public TypeFilter(final Class<T> type) {
        this.type = type;
    }

    public Stream<T> filter(Stream<?> inStream) {
        return inStream.filter(type::isInstance).map(type::cast);
    }
}

要应用于流:

TypeFilter<GroupRecord> groupFilter = new TypeFilter(GroupRecord.class); 

SomeStatsResult stats1 = groupFilter.filter(records.stream())
                                      .collect(...)
SomeStatsResult stats2 = groupFilter.filter(records.stream())
                                      .collect(...)

它有效,但我发现这种方法对于如此简单的任务来说有点多。因此,我想知道,有没有更好或最好的方法,使这种行为以简洁易读的方式使用流和函数可重用?


答案 1

这取决于你觉得什么“更简洁和可读”。我自己会争辩说,你已经实现的方式很好。

但是,确实有一种方法可以以一种比您使用它的地方略短的方式执行此操作,方法是使用:Stream.flatMap

static <E, T> Function<E, Stream<T>> onlyTypes(Class<T> cls) {
  return el -> cls.isInstance(el) ? Stream.of((T) el) : Stream.empty();
}

它的作用是将每个原始流元素转换为一个元素中的一个(如果元素具有预期类型),或者如果元素没有,则转换为空。StreamStream

用途是:

records.stream()
  .flatMap(onlyTypes(GroupRecord.class))
  .forEach(...);

这种方法有明显的权衡:

  • 您确实会从管道定义中丢失“过滤器”一词。这可能比原来的更令人困惑,所以也许一个比需要的更好的名字。onlyTypes
  • Stream对象是相对重量级的,创建如此多的对象可能会导致性能下降。但是,您不应该相信我在这里的话,并在重负载下分析这两种变体。

编辑

由于这个问题询问了重用,并且用更一般的术语,我觉得这个答案也可以讨论更多的抽象。因此,要在一般意义上重用过滤器和地图,您需要具备以下条件:filtermap

static <E, R> Function<E, Stream<R>> filterAndMap(Predicate<? super E> filter, Function<? super E, R> mapper) {
   return e -> filter.test(e) ? Stream.of(mapper.apply(e)) : Stream.empty();
}

原来的实现现在变成:onlyTypes

static <E, R> Function<E, Stream<R>> onlyTypes(Class<T> cls) {
  return filterAndMap(cls::isInstance, cls::cast);
}

但是,再次存在一个权衡:生成的平面映射器函数现在将保存捕获的两个对象(谓词和映射器),而不是上述实现中的单个对象。这也可能是过度抽象的情况,但这取决于您需要该代码的位置和原因。Class


答案 2

您不需要整个类来封装一段代码。用于该目的的最小代码单元将是一个方法:

public static <T> Stream<T> filter(Collection<?> source, Class<T> type) {
    return source.stream().filter(type::isInstance).map(type::cast);
}

此方法可用作

SomeStatsResult stats1 = filter(records, GroupRecord.class)
                            .collect(...);
SomeStatsResult stats2 = filter(records, GroupRecord.class)
                            .collect(...);

如果过滤操作并不总是链中的第一步,则可能会重载该方法:

public static <T> Stream<T> filter(Collection<?> source, Class<T> type) {
    return filter(source.stream(), type);
}
public static <T> Stream<T> filter(Stream<?> stream, Class<T> type) {
    return stream.filter(type::isInstance).map(type::cast);
}

但是,如果必须对同一类型多次重复此操作,则执行该操作可能会有所帮助

List<GroupRecord> groupRecords = filter(records, GroupRecord.class)
                            .collect(Collectors.toList());
SomeStatsResult stats1 = groupRecords.stream().collect(...);
SomeStatsResult stats2 = groupRecords.stream().collect(...);

不仅消除了源代码中的代码重复,而且只执行一次运行时类型检查。所需额外堆空间的影响取决于实际用例。


推荐