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

Почему класс Java 8 'Collector' разработан таким образом?

Мы знаем, что Java 8 представляет новый Stream API, а java.util.stream.Collector - это интерфейс для определения того, как агрегировать/собирать поток данных.

Однако интерфейс коллектора выполнен таким образом:

public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();
}

Почему он не сконструирован так, как показано ниже?

public interface Collector<T, A, R> {
    A supply();
    void accumulate(A accumulator, T value);
    A combine(A left, A right);
    R finish(A accumulator);
}

Последний гораздо проще реализовать. Каково было учесть его в качестве первого?

4b9b3361

Ответ 1

На самом деле он был первоначально разработан так же, как вы предлагаете. См. ранняя реализация в репозитории лямбда проекта (makeResult теперь supplier). Позднее обновлено до текущего дизайна. Я полагаю, обоснование такого обновления заключается в упрощении коллектора. Я не нашел какой-либо конкретной дискуссии по этой теме, но моя догадка подтверждается тем фактом, что коллекционер mapping появился в том же наборе изменений. Рассмотрим реализацию Collectors.mapping:

public static <T, U, A, R>
Collector<T, ?, R> mapping(Function<? super T, ? extends U> mapper,
                           Collector<? super U, A, R> downstream) {
    BiConsumer<A, ? super U> downstreamAccumulator = downstream.accumulator();
    return new CollectorImpl<>(downstream.supplier(),
                               (r, t) -> downstreamAccumulator.accept(r, mapper.apply(t)),
                               downstream.combiner(), downstream.finisher(),
                               downstream.characteristics());
}

Эта реализация должна переопределить только функцию accumulator, оставив supplier, combiner и finisher как есть, поэтому у вас нет дополнительной косвенности при вызове supplier, combiner или finisher: вы просто вызываете непосредственно функции, возвращаемые исходным коллектором. Это еще более важно с collectingAndThen:

public static<T,A,R,RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream,
                                                            Function<R,RR> finisher) {
    // ... some characteristics transformations ...
    return new CollectorImpl<>(downstream.supplier(),
                               downstream.accumulator(),
                               downstream.combiner(),
                               downstream.finisher().andThen(finisher),
                               characteristics);
}

Здесь изменяется только finisher, но используются оригинальные supplier, accumulator и combiner. Поскольку accumulator вызывается для каждого элемента, уменьшение косвенности может быть довольно важным. Попробуйте переписать mapping и collectingAndThen с предлагаемым дизайном, и вы увидите проблему. Новые коллекторы JDK-9, такие как filtering и flatMapping, также извлекают выгоду из текущего дизайна.

Ответ 2

Композиция предпочтительнее наследования.

Первый шаблон в вашем вопросе - это своего рода конфигурация модуля. Реализации интерфейса Collector могут обеспечить различные реализации для Поставщика, Аккумулятора и т.д. Это означает, что можно составить реализации коллектора из существующего пула реализации Поставщика, Аккумулятора и т.д. Это также помогает повторно использовать, и два коллектора могут использовать одну и ту же реализацию Accumulator. Stream.collect() использует предоставленные поведения.

Во втором шаблоне реализация Collector должна реализовать все функции сама по себе. Все виды изменений потребуют переопределения родительской реализации. Не так много возможностей для повторного использования, плюс дублирование кода, если два сборщика имеют аналогичную логику для шага, например, накопление.

Ответ 3

2 связанные причины

  • Функциональный состав через комбинаторы. (Обратите внимание, что вы все еще можете выполнять композицию OO, но смотрите ниже).
  • Возможность создания бизнес-логики в сжатом выразительном коде с помощью выражения лямбда или ссылки на метод, когда целевой целью назначения является функциональный интерфейс.

    Функциональный состав

    API-интерфейс коллектора открывает путь для функционального состава с помощью комбинаторов .i.e. создавать малые/минимальные возможности многократного использования и объединить некоторые из них часто интересным способом в расширенную функцию/функцию.

    сжатый выразительный код

    Ниже мы используем указатель функции (Employee:: getSalary), чтобы заполнить функциональность mapper от объекта Employee до int. sumingInt заполняет логику добавления int и, следовательно, объединяется вместе, у нас есть сумма зарплат, выписанная в одной строке декларативного кода.

    //Вычислить сумму окладов сотрудника  int total = employees.stream()                  .collect(Collectors.summingInt(Сотрудник:: getSalary)));