Подтвердить что ты не робот

Как повторно использовать приложение 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(...)

Это работает, но я нахожу этот подход немного большим для такой простой задачи. Поэтому я задаюсь вопросом, есть ли лучшее или что является лучшим способом для повторного использования этого поведения с использованием потоков и функций в сжатом и понятном виде?

4b9b3361

Ответ 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();
        }
    };
}