Java8 stream.reduce() с 3 параметрами - получение прозрачности

Я написал этот код, чтобы сократить список слов до длинного количества слов, начинающихся с буквы «А». Я просто пишу его, чтобы изучить Java 8, поэтому я хотел бы понять его немного лучше [Отказ от ответственности: я понимаю, что это, вероятно, не лучший способ написать этот код; это просто для практики!].

Long countOfAWords = results.stream().reduce(
    0L,
    (a, b) -> b.charAt(0) == 'A' ? a + 1 : a,
    Long::sum);

Средний параметр/лямбда (называемый аккумулятором), по-видимому, способен сократить полный список без окончательного параметра «Объединитель». На самом деле, Javadoc фактически говорит:

Функция {@code accumulator} действует как объединенный преобразователь и аккумулятор, * что иногда может быть более эффективным, чем раздельное сопоставление и преобразование, * например, когда знание ранее уменьшенного значения позволяет избежать * некоторых вычислений.

[Редактировать от автора] – следующее утверждение неверно, так что пусть оно вас не смущает; Я просто оставлю это здесь, чтобы не испортить первоначальный контекст ответов.

В любом случае, я могу сделать вывод, что аккумулятор должен просто выводить 1 и 0, которые объединяет объединитель. Однако я не нашел это особенно очевидным из документации.

Мой вопрос

Есть ли способ увидеть, что будет на выходе до того, как объединитель выполнит, чтобы я мог видеть список 1 и 0, которые объединяет объединитель? Это было бы полезно при отладке более сложных ситуаций, с которыми, я уверен, я столкнусь в конце концов.


person John Humphreys    schedule 03.05.2015    source источник
comment
Почему бы не просто long countOfAWords=results.stream().filter(x->x.charAt(0)=='A').count();?   -  person Holger    schedule 04.05.2015
comment
Я определенно мог бы, но я просто играл с новыми механизмами получения опыта; это был не настоящий код :)   -  person John Humphreys    schedule 04.05.2015


Ответы (2)


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

U result = identity;
for (T element : this stream)
    result = accumulator.apply(result, element)
return result;

Когда вы запускаете поток параллельно, задача разбивается на несколько потоков. Так, например, данные в конвейере разделены на фрагменты, которые оценивают и выдают результат независимо. Затем объединитель используется для объединения этих результатов.

Таким образом, вы не увидите сокращенный список, а скорее 2 значения, либо значение идентичности, либо другое значение, вычисленное задачей, которая суммируется. Например, если вы добавите оператор печати в объединитель

(i1, i2) -> {System.out.println("Merging: "+i1+"-"+i2); return i1+i2;}); 

вы могли видеть что-то вроде этого:

Merging: 0-0
Merging: 0-0
Merging: 1-0
Merging: 1-0
Merging: 1-1

Это было бы полезно при отладке более сложных ситуаций, с которыми я обязательно столкнусь.

В более общем случае, если вы хотите видеть данные в конвейере на ходу, вы можете использовать peek (или отладчик также может помочь). Итак, применительно к вашему примеру:

long countOfAWords = result.stream().map(s -> s.charAt(0) == 'A' ? 1 : 0).peek(System.out::print).mapToLong(l -> l).sum();

который может выводить:

100100

[Отказ от ответственности: я понимаю, что это, вероятно, не лучший способ написать этот код; это просто для практики!].

Идиоматический способ выполнить вашу задачу - filter поток, а затем просто использовать count:

long countOfAWords = result.stream().filter(s -> s.charAt(0) == 'A').count();

Надеюсь, поможет! :)

person Alexis C.    schedule 03.05.2015
comment
Это один из лучших ответов ТАК, которые у меня когда-либо были на вопрос :) Спасибо. - person John Humphreys; 03.05.2015
comment
@JohnHumpreys-w00te Вау, большое спасибо :-) Рад, что ответил на твой вопрос! - person Alexis C.; 03.05.2015

Один из способов увидеть, что происходит, — заменить ссылку на метод Long::sum лямбдой, включающей println.

List<String> results = Arrays.asList("A", "B", "A", "A", "C", "A", "A");
Long countOfAWords = results.stream().reduce(
        0L,
        (a, b) -> b.charAt(0) == 'A' ? a + 1 : a,
        (a, b) -> {
            System.out.println(a + " " + b);
            return Long.sum(a, b);
        });

В этом случае мы видим, что объединитель фактически не используется. Это потому, что поток не параллельный. Все, что мы на самом деле делаем, это используем аккумулятор для последовательного объединения каждого String с текущим результатом Long; никакие два значения Long никогда не объединяются.

Если вы замените stream на parallelStream, вы увидите, что используется объединитель, и посмотрите на значения, которые он объединяет.

person Paul Boddington    schedule 03.05.2015
comment
Спасибо и за ваш ответ; это было определенно легко использовать в моей ситуации. - person John Humphreys; 03.05.2015