Как повторно использовать приложение filter & map в Stream?
У меня есть набор объектов домена, которые наследуются от общего типа (т.е. GroupRecord extends Record
, RequestRecord extends Record
). Подтипы имеют специфические свойства (т.е. GroupRecord::getCumulativeTime
, RequestRecord::getResponseTime
).
Кроме того, у меня есть список записей со смешанными подтипами в результате анализа файла журнала.
List<Record> records = parseLog(...);
Чтобы вычислить статистику записей журнала, я хочу применить математические функции только к подмножеству записей, которые соответствуют определенному подтипу, т.е. только на GroupRecord
s. Поэтому я хочу иметь отфильтрованный поток определенных подтипов. Я знаю, что я могу применить filter
и map
к подтипу, используя
records.stream()
.filter(GroupRecord.class::isInstance)
.map(GroupRecord.class::cast)
.collect(...
Применять этот фильтр и поток в потоке несколько раз (особенно при выполнении одного и того же подтипа несколько раз для разных вычислений) не только громоздко, но и создает много дублирования.
Мой текущий подход заключается в использовании 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();
}
Что бы он сделал, так это преобразовать каждый исходный элемент потока в Stream
одного элемента, если элемент имеет ожидаемый тип, или пустой Stream
, если он этого не делает.
И используется:
records.stream()
.flatMap(onlyTypes(GroupRecord.class))
.forEach(...);
В этом подходе есть очевидные компромиссы:
- Вы теряете слово "фильтр" из определения вашего конвейера. Это может быть более запутанным, чем оригинал, поэтому возможно лучшее имя, чем
onlyTypes
.
-
Stream
объекты относительно тяжеловесы, и создание такого количества из них может привести к ухудшению производительности. Но вы не должны доверять моему слову и профилировать оба варианта при большой нагрузке.
Edit
Поскольку вопрос задает вопрос о повторном использовании 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();
}
И первоначальная реализация 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(...);
не только устраняет дублирование кода в исходном коде, но также выполняет проверку типов выполнения только один раз. Влияние требуемого дополнительного пространства кучи зависит от фактического варианта использования.
Ответ 3
ЧТО вам действительно нужен Collector для сбора всех элементов в потоке, который является экземпляром специальный type. Он может легко решить вашу проблему и избежать фильтрации потока дважды:
List<GroupRecord> result = records.stream().collect(
instanceOf(GroupRecord.class, Collectors.toList())
);
SomeStatsResult stats1 = result.stream().collect(...);
SomeStatsResult stats2 = result.stream().collect(...);
И вы можете сделать что-то еще, например Карту потока #, используя Отображение коллектора #, например:
List<Integer> result = Stream.of(1, 2L, 3, 4.)
.collect(instanceOf(Integer.class, mapping(it -> it * 2, Collectors.toList())));
| |
| [2,6]
[1,3]
WHERE, вы только хотите использовать Stream
один раз, вы можете легко составить последний Collector
, как показано ниже:
SomeStatsResult stats = records.stream().collect(
instanceOf(GroupRecord.class, ...)
);
static <T, U extends T, A, R> Collector<T, ?, R> instanceOf(Class<U> type
, Collector<U, A, R> downstream) {
return new Collector<T, A, R>() {
@Override
public Supplier<A> supplier() {
return downstream.supplier();
}
@Override
public BiConsumer<A, T> accumulator() {
BiConsumer<A, U> target = downstream.accumulator();
return (result, it) -> {
if (type.isInstance(it)) {
target.accept(result, type.cast(it));
}
};
}
@Override
public BinaryOperator<A> combiner() {
return downstream.combiner();
}
@Override
public Function<A, R> finisher() {
return downstream.finisher();
}
@Override
public Set<Characteristics> characteristics() {
return downstream.characteristics();
}
};
}
Зачем вам нужны сборщики?
Вы помнили Композиция над принципом наследования? Вы помните assertThat (foo).isEqualTo(bar) и assertThat (foo, is (bar)) в модульном тесте?
Композиция гораздо более гибкая, она может повторно использовать часть кода и компоновать компоненты вместе во время выполнения, поэтому я предпочитаю hamcrest
, а не fest-assert
, так как он может составить все возможные Matcher
вместе. и именно поэтому функциональное программирование является самым популярным, поскольку оно может повторно использовать любую меньшую часть функционального кода, чем повторное использование класса. и вы можете видеть, что jdk внедрил фильтрация коллекционеров в jdk-9, которая сделает маршруты выполнения короче, не теряя выразительности .
И вы можете реорганизовать код выше в соответствии с Разделение проблем, а затем filtering
может повторно использовать как jdk-9 Фильтры коллекционеров:
static <T, U extends T, A, R> Collector<T, ?, R> instanceOf(Class<U> type
, Collector<U, A, R> downstream) {
return filtering(type::isInstance, Collectors.mapping(type::cast, downstream));
}
static <T, A, R>
Collector<T, ?, R> filtering(Predicate<? super T> predicate
, Collector<T, A, R> downstream) {
return new Collector<T, A, R>() {
@Override
public Supplier<A> supplier() {
return downstream.supplier();
}
@Override
public BiConsumer<A, T> accumulator() {
BiConsumer<A, T> target = downstream.accumulator();
return (result, it) -> {
if (predicate.test(it)) {
target.accept(result, it);
}
};
}
@Override
public BinaryOperator<A> combiner() {
return downstream.combiner();
}
@Override
public Function<A, R> finisher() {
return downstream.finisher();
}
@Override
public Set<Characteristics> characteristics() {
return downstream.characteristics();
}
};
}