Java – how to reuse application filters and maps in streams?
I have a group of domain objects inherited from the shared type (i.e. grouprecord extension record, requestrecord extensions record) Subtypes have specific properties (i.e. grouprecord:: getcumulativetime, requestrecord:: getresponsetime)
In addition, due to parsing log files, I have a list of records with mixed subtypes
List<Record> records = parseLog(...);
In order to calculate the statistics of log records, I want to apply mathematical functions only on the subset of records matching the specific subtype (i.e. only on group records) Therefore, I want a filtered stream of a specific subtype I know I can apply filters and map to subtypes
records.stream() .filter(GroupRecord.class::isinstance) .map(GroupRecord.class::cast) .collect(...
Applying this filter to the stream multiple times & cast (especially for different calculations, multiple times for the same subtype) is not only troublesome, but also produces a lot of duplication
My current practice is to use 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); } }
Apply to stream:
TypeFilter<GroupRecord> groupFilter = new TypeFilter(GroupRecord.class); SomeStatsResult stats1 = groupFilter.filter(records.stream()) .collect(...) SomeStatsResult stats2 = groupFilter.filter(records.stream()) .collect(...)
It works, but I find this method a bit like such a simple task So, I wonder if using flows and functions makes this behavior reusable in a concise and understandable way? What's the best way?
Solution
It depends on what you find "more concise and readable" I myself would think that the way you have implemented is good because it is
However, there is a way to do this by using stream Flatmap is a little shorter from the perspective of your use of it:
static <E,T> Function<E,Stream<T>> onlyTypes(Class<T> cls) { return el -> cls.isinstance(el) ? Stream.of((T) el) : Stream.empty(); }
How will it convert each original stream element to a stream of one element, if the element has the expected type, or if not, to an empty stream
Used are:
records.stream() .flatMap(onlyTypes(GroupRecord.class)) .forEach(...);
This approach has obvious trade-offs:
>You will lose the word "filter" in the pipe definition This may be more confusing than the original, so perhaps a better name than just type is needed Stream objects are relatively heavyweight, and creating these objects may lead to performance degradation But you should not believe what I said here and analyze the two variants under this heavy load
Edit:
Since the question requires slightly more general reuse of filters and maps, I think this answer can also discuss a little abstraction Therefore, to reuse filters and maps, in general, you need the following:
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(); }
The original type only implementation is now:
static <E,Stream<R>> onlyTypes(Class<T> cls) { return filterAndMap(cls::isinstance,cls::cast); }
However, there is still a tradeoff: the flat mapper function will now capture two objects (predicate and mapper) in the above implementation instead of a single class object This can also be an overly abstract situation, but it depends on where and where you need the code