如何在Stream上重用过滤器和地图的应用程序?

Ger*_*cke 13 java java-8 java-stream

我有一组从共享类型(即继承域对象的GroupRecord extends Record,RequestRecord extends Record).亚型具有特定属性(即GroupRecord::getCumulativeTime,RequestRecord::getResponseTime).

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

List<Record> records = parseLog(...);
Run Code Online (Sandbox Code Playgroud)

为了计算日志记录的统计数据,我想仅在与特定子类型匹配的记录子集上应用数学函数,即仅在GroupRecords上.因此,我希望有一个特定子类型的过滤流.我知道我可以使用一个filtermap一个子类型

records.stream()
       .filter(GroupRecord.class::isInstance)
       .map(GroupRecord.class::cast)
       .collect(...
Run Code Online (Sandbox Code Playgroud)

在流上多次应用此过滤器和强制转换(特别是对于不同的计算多次执行相同的子类型时)不仅繁琐,而且会产生大量重复.

我目前的做法是使用a 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

要应用于流:

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

SomeStatsResult stats1 = groupFilter.filter(records.stream())
                                      .collect(...)
SomeStatsResult stats2 = groupFilter.filter(records.stream())
                                      .collect(...)
Run Code Online (Sandbox Code Playgroud)

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

M. *_*rov 12

这取决于你发现什么"更简洁和可读".我自己会争辩说你已经实施的方式很好.

但是,确实有一种方法可以通过使用Stream.flatMap以下方式从使用它的位置略微缩短:

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

Stream如果元素具有期望的类型,它会将每个原始流元素转换为一个元素中的一个元素,如果不具有,Stream则将其转换为空元素.

用途是:

records.stream()
  .flatMap(onlyTypes(GroupRecord.class))
  .forEach(...);
Run Code Online (Sandbox Code Playgroud)

这种方法有明显的权衡:

  • 您确实丢失了管道定义中的"过滤器"字样.这可能比原版更令人困惑,所以可能是一个比onlyTypes需要更好的名字.
  • Stream对象是相对重量级的,创建它们可能会导致性能下降.但你不应该相信我的话,并在重负荷下描述两种变体.

编辑:

由于这个问题要求重复使用,filter而且map稍微更笼统一些,我觉得这个答案也可以讨论更多的抽象.因此,要重复使用过滤器和地图,您需要以下内容:

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();
}
Run Code Online (Sandbox Code Playgroud)

原始onlyTypes实现现在变为:

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

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


Hol*_*ger 9

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

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

这种方法可以用作

SomeStatsResult stats1 = filter(records, GroupRecord.class)
                            .collect(...);
SomeStatsResult stats2 = filter(records, GroupRecord.class)
                            .collect(...);
Run Code Online (Sandbox Code Playgroud)

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

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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • @Federico Peralta Schaffner:的确,谢谢 (2认同)