如何在Stream上重用filter和地图的应用程序?
我有一组从共享类型inheritance的域对象(即GroupRecord extends Record
, RequestRecord extends Record
)。 子类型具有特定属性(即GroupRecord::getCumulativeTime
, RequestRecord::getResponseTime
)。
此外,由于解析日志文件,我有一个混合子类型的记录列表。
List records = parseLog(...);
为了计算日志记录的统计数据,我想仅在与特定子类型匹配的记录子集上应用数学函数,即仅在GroupRecord
。 因此,我希望有一个特定子类型的过滤流。 我知道我可以使用filter
并map
到子类型
records.stream() .filter(GroupRecord.class::isInstance) .map(GroupRecord.class::cast) .collect(...
在流上多次应用此filter和强制转换(特别是当为不同的计算多次执行相同的子类型时)不仅繁琐,而且会产生大量重复。
我目前的方法是使用TypeFilter
class TypeFilter{ private final Class type; public TypeFilter(final Class type) { this.type = type; } public Stream filter(Stream inStream) { return inStream.filter(type::isInstance).map(type::cast); } }
要应用于流:
TypeFilter groupFilter = new TypeFilter(GroupRecord.class); SomeStatsResult stats1 = groupFilter.filter(records.stream()) .collect(...) SomeStatsResult stats2 = groupFilter.filter(records.stream()) .collect(...)
它有效,但我发现这种方法对于这么简单的任务来说有点多了。 因此我想知道,使用流和函数以简洁和可读的方式使这种行为可重用是否有更好或最好的方法是什么?
这取决于你发现什么“更简洁和可读”。 我自己会争辩说你已经实施的方式很好。
但是,确实有一种方法可以通过使用Stream.flatMap
以稍微短一些的方式执行此Stream.flatMap
:
static Function> onlyTypes(Class cls) { return el -> cls.isInstance(el) ? Stream.of((T) el) : Stream.empty(); }
如果元素具有预期类型,它会将每个原始流元素转换为一个元素的Stream
,如果不具有,则将其转换为空Stream
。
用途是:
records.stream() .flatMap(onlyTypes(GroupRecord.class)) .forEach(...);
这种方法有明显的权衡:
- 您确实丢失了管道定义中的“filter”字样。 这可能比原始版本更令人困惑,因此可能需要比
onlyTypes
类型更好的名称。 -
Stream
对象相对较重,并且创建它们中的大部分可能会导致性能下降。 但你不应该相信我的话,并在重载下描述两种变体。
编辑 :
由于这个问题要求稍微重复使用filter
和map
,我觉得这个答案也可以讨论更多的抽象。 因此,要重复使用filter和地图,您需要以下内容:
static Function> filterAndMap(Predicate super E> filter, Function super E, R> mapper) { return e -> filter.test(e) ? Stream.of(mapper.apply(e)) : Stream.empty(); }
原来的onlyTypes
实现现在变成:
static Function> onlyTypes(Class cls) { return filterAndMap(cls::isInstance, cls::cast); }
但是,再次进行权衡:得到的平面映射器函数现在将捕获两个对象(谓词和映射器)而不是上面实现中的单个Class
对象。 它也可能是过度抽象的情况,但这取决于您需要该代码的位置和原因。
您不需要整个类来封装一段代码。 用于此目的的最小代码单元将是一种方法:
public static Stream filter(Collection> source, Class 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 Stream filter(Collection> source, Class type) { return filter(source.stream(), type); } public static Stream filter(Stream> stream, Class type) { return stream.filter(type::isInstance).map(type::cast); }
但是,如果必须为同一类型多次重复此操作,则可能会有所帮助
List groupRecords = filter(records, GroupRecord.class) .collect(Collectors.toList()); SomeStatsResult stats1 = groupRecords.stream().collect(...); SomeStatsResult stats2 = groupRecords.stream().collect(...);
不仅消除了源代码中的代码重复,还执行了一次运行时类型检查。 所需额外堆空间的影响取决于实际用例。
实际需要的是收集器收集流中特殊类型实例的所有元素。 它可以轻松解决您的问题并避免过滤两次流:
List result = records.stream().collect( instanceOf(GroupRecord.class, Collectors.toList()) ); SomeStatsResult stats1 = result.stream().collect(...); SomeStatsResult stats2 = result.stream().collect(...);
你可以通过使用Collectors#mapping做一些像Stream#map更进一步的事情,例如:
List result = Stream.of(1, 2L, 3, 4.) .collect(instanceOf(Integer.class, mapping(it -> it * 2, Collectors.toList()))); | | | [2,6] [1,3]
如果您只想使用Stream
一次,您可以轻松地编写最后一个Collector
,如下所示:
SomeStatsResult stats = records.stream().collect( instanceOf(GroupRecord.class, ...) );
static Collector instanceOf(Class type , Collector downstream) { return new Collector() { @Override public Supplier supplier() { return downstream.supplier(); } @Override public BiConsumer accumulator() { BiConsumer target = downstream.accumulator(); return (result, it) -> { if (type.isInstance(it)) { target.accept(result, type.cast(it)); } }; } @Override public BinaryOperator combiner() { return downstream.combiner(); } @Override public Function finisher() { return downstream.finisher(); } @Override public Set characteristics() { return downstream.characteristics(); } }; }
你为什么需要collectionscollections家?
你还记得组合超过inheritance原则吗? 你还记得在unit testing中断言(foo).isEqualTo(bar)和assertThat(foo,是(bar))吗?
组合更灵活 ,它可以重用一段代码并在运行时组合组件,这就是为什么我更喜欢hamcrest
而不是fest-assert
因为它可以组合所有可能的Matcher
。 这就是函数式编程最受欢迎的原因,因为它可以重用任何较小的函数代码而不是类级重用。 你可以看到jdk在jdk-9中引入了Collectors#filtering ,这将使执行路由更短,而不会失去其表现力 。
你可以根据问题的分离进一步重构上面的代码,然后filtering
可以像jdk-9 Collectors#filtering一样重复使用:
static Collector instanceOf(Class type , Collector downstream) { return filtering(type::isInstance, Collectors.mapping(type::cast, downstream)); } static Collector filtering(Predicate super T> predicate , Collector downstream) { return new Collector() { @Override public Supplier supplier() { return downstream.supplier(); } @Override public BiConsumer accumulator() { BiConsumer target = downstream.accumulator(); return (result, it) -> { if (predicate.test(it)) { target.accept(result, it); } }; } @Override public BinaryOperator combiner() { return downstream.combiner(); } @Override public Function finisher() { return downstream.finisher(); } @Override public Set characteristics() { return downstream.characteristics(); } }; }