Почему StreamEx заставляет меня добавлять "? Extends" к типу переменной при сборе в список?

Я заметил, что в случае ниже стандартного потока Java лучше "вычислить" тип переменной, чем StreamEx. Это странно, потому что люди любят StreamEx и используют его повсюду, но затем код загрязняется знаком "?". Я хочу использовать List<Class<?> но StreamEx заставляет меня использовать List<? extends Class<?>>. Может ли кто-нибудь объяснить, почему StreamEx работает таким образом? Могу ли я как-то использовать желаемый тип переменной с StreamEx?

private static List<? extends Class<?>> 
fooList_StreamEx_Compiles(List<Integer> input) {
    return StreamEx.of(input)
            .map(x -> foo())
            .toList();
}

private static List<Class<?>> 
fooList_StreamEx_Error(List<Integer> input) {
    return StreamEx.of(input)
            .map(x -> foo())
// Error: incompatible types: java.util.List<java.lang.Class<capture#1 of ?>> 
// cannot be converted to java.util.List<java.lang.Class<?>>
            .toList();
}

private static List<Class<?>> fooList(List<Integer> input) {
    return input
            .stream()
            .map(x -> foo())
            .collect(Collectors.toList());
}

private static Class<?> foo() {
    return String.class;
}

Я использую StreamEx 0.7.0 и Java 11

1 ответ

Решение

Это не StreamExвопрос. Когда вы используете collect(Collectors.toList()) на StreamEx, он работает одинаково хорошо. Проблема связана с toList() метод удобства, который является стандартным Streamдаже не предлагает. Это общая проблема, что StreamEx не виноват.

В toList метод на StreamEx имеет следующую подпись:

public List<T> toList()

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

public <R super T> List<R> toList()

но этот синтаксис был опущен при создании универсальных шаблонов Java. В toArray методы обоих, Collection и Streamстрадают аналогичным ограничением; они не могут объявить тип элемента результирующего массива супертипом типа элемента коллекции. Но поскольку проверяется фактическая операция сохранения массива, эти методы просто допускают любой тип элемента. Для List результат, при условии стирания типа это невозможно.

Когда используешь collect(Collectors.toList()) с другой стороны, можно создать список с супертипом из-за collectподпись:

<R,​A> R collect​(Collector<? super T,​A,​R> collector)

что позволяет перейти в Collector параметризованный супертипом (? super T), который в большинстве сценариев будет выводиться из целевого типа.


Это ограничение toListобъявление взаимодействует с другим ограничением - необоснованной обработкой подстановочных знаков. Я не уверен, кроется ли причина этой проблемы в компиляторе или спецификации, но в map(x -> foo()) step, тип подстановочного знака был захвачен, и такой захваченный тип будет считаться отличным от любого другого захваченного типа подстановочного знака, даже если он происходит из того же источника.

Когда я компилирую ваш код с javac, он говорит:

error: incompatible types: List<Class<CAP#1>> cannot be converted to List<Class<?>>
                    .toList();
                           ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
1 error

В CAP#1это захваченный тип. Все захваченные типы нумеруются, чтобы различать их. Как уже было сказано, каждый из них считается отдельным типом, отличающимся от всех остальных.

Class<?> это супертип Class<CAP#1>, поэтому он работает с collect который позволяет собирать список, параметризованный этим супертипом, как сказано выше, но не toList.

Вы можете исправить это, используя возвращаемый тип List<? extends Class<?>>, чтобы обозначить, что фактический тип элемента списка является подтипом Class<?>, но лучший вариант - заставить компилятор не использовать захваченный тип:

private static List<Class<?>> fooList_StreamEx_Solved(List<Integer> input) {
    return StreamEx.of(input)
            .<Class<?>>map(x -> foo())
            .toList();
}

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

Другие вопросы по тегам