Совокупная информация с использованием потоков Java 8

Я все еще пытаюсь полностью понять работу с пакетом Stream в Java 8 и надеялся на некоторую помощь.

У меня есть класс, описанный ниже, экземпляры которого я получаю в списке как часть вызова базы данных.

class VisitSummary {
    String source;
    DateTime timestamp;
    Integer errorCount;
    Integer trafficCount;
    //Other fields
}

Чтобы получить некоторую, возможно, полезную информацию об этом, у меня есть класс VisitSummaryBySource, который содержит общую сумму всех посещений (за заданный период времени):

class VisitSummaryBySource {
    String sourceName;
    Integer recordCount;
    Integer errorCount;
}

Я надеялся создать коллекцию List<VisitSummaryBySource>, которая, как звучит название, содержит список объектов VisitSummaryBySource, содержащих общую сумму записей и ошибок, обнаруженных для каждого отдельного источника.

Есть ли способ добиться этого, используя потоки за одну операцию? Или мне нужно обязательно разбить это на несколько операций? Лучшее, что я мог придумать, это:

Map<String, Integer> recordsBySrc = data.parallelStream().collect(Collectors.groupingBy(VisitSummaryBySource::getSource,
                    Collectors.summingInt(VisitSummaryBySource::getRecordCount)));

и считать ошибки

Map<String, Integer> errorsBySrc = data.parallelStream().collect(Collectors.groupingBy(VisitSummaryBySource::getSource,
                    Collectors.summingInt(VisitSummaryBySource::getErrorCount)));

и объединить две карты, чтобы составить список, который я ищу.


person Qrious    schedule 16.01.2015    source источник
comment
Вы должны включить используемые методы и конструкторы вашего класса вместо того, чтобы позволять нам угадывать их из поля. То есть ваши фрагменты кода того, что вы пробовали, используют ссылки на методы, не показанные в вашем вопросе. Кроме того, неясно, является ли ваш исходный источник List из VisitSummary или VisitSummaryBySource. Ваш код предполагает последнее, но тогда непонятно, почему вы показали нам неиспользуемый класс VisitSummary  -  person Holger    schedule 16.01.2015
comment
Мои извинения, я попытался сделать пост более кратким, осознав, что мой первоначальный пост был слишком длинным. Я буду знать лучше, чем обрезать код и опустить важные вещи :-)   -  person Qrious    schedule 16.01.2015


Ответы (1)


Вы на правильном пути. Примеры использования Collectors.summingInt являются примерами нисходящих коллекторов внешнего коллектора groupingBy. Эта операция извлекает одно целочисленное значение из каждого экземпляра VisitSummaryBySource в той же группе и суммирует их. По сути, это сокращение над целыми числами.

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

Суть заключается в том, чтобы учитывать сокращение не отдельных целочисленных значений, а всего объекта VisitSummaryBySource. Редукция принимает BinaryOperator, который берет два экземпляра рассматриваемого типа и объединяет их в один. Вот как это сделать, добавив статический метод в VisitSummaryBySource:

static VisitSummaryBySource merge(VisitSummaryBySource a,
                                  VisitSummaryBySource b) {
    assert a.getSource().equals(b.getSource());
    return new VisitSummaryBySource(a.getSource(), 
                                    a.getRecordCount() + b.getRecordCount(),
                                    a.getErrorCount() + b.getErrorCount());
}

Обратите внимание, что на самом деле мы не объединяем имена источников. Поскольку это сокращение выполняется только внутри группы, где имена источников совпадают, мы утверждаем, что можем объединить только два экземпляра с одинаковыми именами. Мы также предполагаем, что очевидный конструктор принимает имя, количество записей и количество ошибок, и вызываем его для создания объединенного объекта, содержащего суммы счетчиков.

Теперь наш поток выглядит так:

    Map<String, Optional<VisitSummaryBySource>> map =
        data.stream()
            .collect(groupingBy(VisitSummaryBySource::getSource,
                                reducing(VisitSummaryBySource::merge)));

Обратите внимание, что это сокращение создает значения карты типа Optional<VisitSummaryBySource>. Это несколько странно; мы разберемся с этим ниже. Мы могли бы избежать Optional, используя другую форму сборщика reducing, которая принимает значение идентификатора. Это возможно, но в чем-то бессмысленно, так как нет смысла использовать исходное имя удостоверения. (Мы могли бы использовать что-то вроде пустой строки, но нам пришлось бы отказаться от нашего утверждения, что мы объединяем только объекты, исходные имена которых совпадают.)

На самом деле нас не волнует карта; его нужно поддерживать достаточно долго, чтобы уменьшить количество экземпляров VisitSummaryBySource. Как только это будет сделано, мы можем просто извлечь значения карты с помощью values() и выбросить карту.

Мы также можем превратить это обратно в поток и развернуть Optional, сопоставив их с Optional::get. Это безопасно, потому что значение никогда не оказывается на карте, если в группе нет хотя бы одного члена.

Наконец, мы собираем результаты в список.

Окончательный код выглядит так:

    List<VisitSummaryBySource> output =
        data.stream()
            .collect(groupingBy(VisitSummaryBySource::getSource,
                                reducing(VisitSummaryBySource::merge)))
            .values().stream()
            .map(Optional::get)
            .collect(toList());
person Stuart Marks    schedule 16.01.2015
comment
Спасибо за потрясающее объяснение! - person Qrious; 16.01.2015
comment
@Qrious Добро пожаловать! И спасибо, что приняли. Я не уверен, что правильно понял вашу модель данных; но в любом случае я надеюсь, что концепция кода, который я показал, правильно перенесена в вашу реальную модель. - person Stuart Marks; 17.01.2015