Събирайте информация с помощта на потоци на 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