Почему Stream # не позволяет неявно принимать элементы супертипной функции, связанные с накопительной функцией?

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

abstract static class Foo {
    abstract int getK();
}
static class Bar extends Foo {
    int k;
    Bar(int k) { this.k = k; }
    int getK() { return this.k; }
}

private static Foo combined(Foo a1, Foo a2) {
    return new Bar(a1.getK() + a2.getK());
}

Я попытался выполнить накопление элементов (исходных отчетов индексирования данных), опираясь на отдельную функцию combined, которая напрямую связана с элементами типа Foo.

Foo outcome = Stream.of(1,2,3,4,5)
        .map(Bar::new)
        .reduce((a,b) -> combined(a, b))
        .get();

Оказывается, этот код приводит к ошибке компиляции (OpenJDK "1.8.0_92" ): "Плохой тип возврата в выражении лямбда: Foo не может быть преобразован в Bar". Компилятор настаивает на попытке уменьшить поток, используя Bar как накопительный элемент, даже если существует Foo как общий тип для обоих аргументов кумулятивной функции и ее возвращаемого типа.

Я также считаю своеобразным, что я все еще могу использовать этот подход, пока я явно сопоставляю поток в поток Foo s:

Foo outcome = Stream.of(1,2,3,4,5)
        .<Foo>map(Bar::new)
        .reduce((a,b) -> combined(a, b))
        .get();

Является ли это ограничением вывода типа generic типа Java 8, небольшой проблемой с этой конкретной перегрузкой Stream#reduce или преднамеренным поведением, которое поддерживается спецификацией Java? Я прочитал еще несколько вопросов о SO, где вывод типа "провалился", но этот конкретный случай по-прежнему мне трудно понять.

Ответ 1

Я думаю, что это связано с тем, почему список Derived не является списком Base. Выполняя .map(Bar::new), вы создаете поток Bar. Очевидно, что он не тривиально конвертируется в поток Foo в соответствии с общим правилом "Если B является A, то X<B> не X<A>".

Затем вы пытаетесь выполнить reduce, но reduce должен создать поток точно такого же типа. Вы хотите reduce вести себя как обе reduce и map в том смысле, что хотите, чтобы он уменьшал поток до одного экземпляра и менял его тип. Это больше похоже на задание для collect (за исключением того, что collect работает с изменяемыми типами).

Но есть вариант reduce, который может изменить тип. И его подпись на самом деле дает нам подсказку, почему простая перегрузка не может изменить тип потока:

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)

Требуется две функции! Зачем? Поскольку потоки Java являются параллельными. Таким образом, поток может выполнять сокращение по частям, а затем объединять детали в одно значение. Здесь это возможно, потому что мы даем дополнительный combiner. В вашем случае одна функция должна действовать как как накопитель, так и сумматор, который создает все путаницу в том, что именно является его сигнатурой.

Поэтому причина, по которой это не работает, заключается в том, что в этой перегрузке отсутствует объединитель, который может сочетать частичные результаты. Конечно, это не "трудная" причина в том смысле, что поскольку Foo является суперклассом Bar, технически можно использовать то же, что и как аккумулятор, так и объединитель, как и в вашем примере с явным <Foo>.

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

Ответ 2

Проблема в том, что вы определенно создаете BinaryOperator<Foo> - вы должны быть, когда вы возвращаете Foo. Если вы измените combined(), чтобы объявить return Bar (пока все еще принимаете Foo), тогда все будет в порядке. Это тот факт, что тип возврата привязан к типу ввода, что проблема - она ​​не может быть либо ковариантной, либо контравариантной, поскольку она используется для ввода и вывода.

Иными словами, вы ожидаете, что reduce((a, b) -> combined(a, b)) вернет Optional<Foo>, правильно? Это означает, что вы ожидаете, что T вызова reduce() будет Foo - это означает, что он должен работать на Stream<Foo>. A Stream<Bar> имеет только один параметр reduce, который принимает BinaryOperator<Bar>, а ваше лямбда-выражение с использованием combined simple не является BinaryOperator<Bar>.

Другой альтернативой является добавление приведения к выражению лямбда:

Foo outcome = Stream.of(1,2,3,4,5)
    .map(Bar::new)                
    .reduce((a,b) -> (Bar)combined(a, b))
    .get();

Ответ 3

Update

Простой ответ на ваш вопрос заключается в том, что ваш сокращение принимает BinaryOperator<Bar>, в котором говорится, что вы должны передать функцию, которая принимает два значения Bar и возвращает Bar. Ваша лямбда принимает два параметра Bar просто отлично, потому что ваша функция combined видит, что Bar назначается Foo. Однако для типа возврата мышление идет в противоположном направлении; в то время как для параметров вы обеспокоены тем, что значения, переданные в соответствии с вашей функцией, для типа возврата, вам нужно беспокоиться о том, сможет ли вызывающий объект вашей функции использовать тип возвращаемой функции. Поскольку ваша функция возвращает Foo, а приемник ожидает Bar, возникает ошибка компилятора по той же причине, что и при этом:

Bar b = new Foo(1);

Добавление литых работ, но это небезопасно по той же причине, что

Bar b  = (Bar) new Foo(1);

работает, но не безопасен - не каждый экземпляр Foo обязательно будет Bar.

Существует безопасный способ сделать сокращение без бросков, используя уменьшение 3-параметра. Он позволяет свести к другому типу, чем тип потока.

Foo combined = Stream.of(1,2,3,4,5)
            .map(Bar::new)
            .reduce(
                new Bar(0),                       // starting value, type Foo
                (Foo a, Bar b) -> combined(a, b), // apply each Bar to create new Foo 
                (Foo a, Foo b) -> combined(a,b)); // combine intermediate results if parallel

Первым параметром (Foo)new Bar(0) является начальное значение для семени сокращения и будет значением, возвращаемым, если поток пуст.

Второй параметр складывается в каждом Bar из Stream в новое скопированное значение Foo.

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

Функция combine работает как для аккумулятора, так и для объединителя только потому, что Bar может быть назначена Foo.

Обратите внимание, что, как упоминалось в комментариях, это можно записать более кратко, потому что в этом случае тип элемента Stream можно присваивать типу Accumulator, поэтому его можно упростить до

Foo combined = Stream.of(1,2,3,4,5)
            .map(Bar::new)
            .reduce(new Bar(0), (a,b) -> combined(a, b), (a, b) -> combined(a,b));

Или

Foo combined = Stream.of(1,2,3,4,5)
            .map(Bar::new)
            .reduce(new Bar(0), MyClass::combined, MyClass::combined);

Ответ 4

Вы действительно используете ограничение вывода типа Java 8s. В одном предложении целевая типизация не работает для получателя вызова метода. Таким образом, в вашем коде вы вызываете метод reduce по результату Stream.of(1,2,3,4,5).map(Bar::new), что делает это выражение получателем вызова метода .reduce((a,b) -> combined(a, b)), который не может использовать любую информацию о том, что reduce может ожидать.

Вместо этого тип выражения Stream.of(1,2,3,4,5), который является получателем вызова .map, и аргументом Bar::new являются единственной информацией для определения типа полученного потока, и нет ничего, предполагающего, что Stream<Foo> может быть лучшим выбором по сравнению с Stream<Bar>.

Это та же проблема, что описана в этом ответе и который отвечает.

Можно легко продемонстрировать, как это будет работать, если бы было возможным вывод типа, путем растворения методов invocations:

Stream<Foo> s= Stream.of(1,2,3,4,5).map(Bar::new);
Foo outcome = s.reduce((a,b) -> combined(a, b)).get();

который работает плавно.

Помимо предоставления явного типа для map, как и для .<Foo>map(Bar::new), также существует альтернатива

Optional<Foo> outcome =
    Stream.of(1,2,3,4,5)
    .map(Bar::new)
    .collect(Collectors.reducing((a,b) -> combined(a, b)));

Это работает, потому что collect на Stream<T> допускает аргумент типа Collector<? super T,…>, поэтому компилятор может вывести Collector<Foo,…> из ожидаемого целевого типа, который действителен для вызова collect на Stream<Bar>. Однако вы не можете вызвать вызов get(), чтобы получить Foo немедленно, по той же причине, описанной выше; целевой тип не будет передаваться получателю вызова метода.