Почему компилятор Java не может вывести Iterable ‹String› из ограничений Iterable ‹? расширяет CharSequence ›и () -› (Iterator ‹String›)

История вопроса: недавно я написал ответ, где я предложил написать следующий код:

Files.write(Paths.get("PostgradStudent.csv"),
        Arrays.stream(PGstudentArray).map(Object::toString).collect(Collectors.toList()),
        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

Поразмыслив, я сказал: «На самом деле мне здесь не нужен список, мне просто нужен Iterable<? extends CharSequence>».
Поскольку у Stream<T> есть метод Iterator<T> iterator(), я подумал, ну, это просто:

Iterable<? extends CharSequence> iterable = () -> Arrays.stream(arr).map(Object::toString).iterator();

(Я извлек его в локальную переменную для этого вопроса, я хотел бы сделать это встроенным в конце.)
К сожалению, это не компилируется без дополнительных подсказок типа:

error: incompatible types: bad return type in lambda expression
Iterable<? extends CharSequence> iterable = () -> Arrays.stream(arr).map(Object::toString).iterator();
                                                                                                   ^
    Iterator<String> cannot be converted to Iterator<CharSequence>

Конечно, добавление подсказок типа сделает эту работу:

Iterable<? extends CharSequence> iterable2 = (Iterable<String>) () -> Arrays.stream(arr).map(Object::toString).iterator();
Iterable<? extends CharSequence> iterable3 = () -> Arrays.stream(arr).<CharSequence>map(Object::toString).iterator();

В моем понимании компилятор Java выполняет следующие функции:

  1. Он смотрит на целевой тип выражения, которым является Iterable<? extends CharSequence>.
  2. Затем он определяет тип функции этого интерфейса, в моем случае это () -> Iterator<? extends CharSequence>.
  3. Затем он смотрит на лямбду и проверяет, совместима ли она.
    В моем случае лямбда имеет тип () -> Iterator<String>.
    Что совместимо с типом функции, определенным на шаге 2.

Интересно, что если я изменю цель лямбды на Supplier:

Supplier<Iterator<? extends CharSequence>> supplier = () -> Arrays.stream(arr)
    .map(Object::toString)
    .iterator();

он будет компилироваться нормально.

Теперь возникает вопрос: почему javac не может определить правильный тип для этой лямбды?


person Johannes Kuhn    schedule 24.10.2019    source источник
comment
Это должно быть Iterable<? extends String> Не ? extends CharSequence   -  person Ravindra Ranwala    schedule 24.10.2019
comment
@RavindraRanwala Я не понимаю вашего комментария. Посмотрите на Документация Javadoc для Files.write. Это не то, что я могу изменить. И поскольку String является окончательным, он будет равен Iterable<String>.   -  person Johannes Kuhn    schedule 24.10.2019
comment
Может быть просто связано с Захват конверсии # jls-5.1.10   -  person Naman    schedule 24.10.2019
comment
@Naman совсем нет. Не имеет значения, использую ли я его как часть задания или при вызове Files.write. Присваивание - это простой способ указать цель лямбда.   -  person Johannes Kuhn    schedule 24.10.2019


Ответы (2)


Вы можете найти объяснение здесь:

Типы функциональных интерфейсов с подстановочными знаками должны быть преобразованы в тип функции (подпись метода) перед проверкой совместимости ... Это работает следующим образом:

Iterable<? extends CharSequence> becomes () -> Iterator<CharSequence>

Итак, если лямбда-выражение неявно типизировано, LHS становится Iterator<CharSequence>, а RHS - Iterator<String>. Следовательно, ошибка:

Iterator<String> cannot be converted to Iterator<CharSequence>

Это поведение также объясняется в JLS §18.5.3.

person Oleksandr Pyrohov    schedule 24.10.2019
comment
Интересным фактом является то, что Supplier<Iterator<? extends CharSequence>> как цель работает нормально. Какая здесь разница? - person Johannes Kuhn; 24.10.2019
comment
@JohannesKuhn Вы добавили еще один уровень вложенности, который не имеет подстановочного знака. - person Oleksandr Pyrohov; 24.10.2019
comment
В общем, поскольку параметр Iterator<? extends CharSequence> не является подстановочным знаком, он этого не делает? Тогда почему бы не сделать еще один уровень выше? - person Johannes Kuhn; 24.10.2019
comment
@JohannesKuhn Это вопрос к разработчикам языков. - person Oleksandr Pyrohov; 24.10.2019
comment
Итак, чтобы подвести итог, чтобы избежать сложной технической проблемы (вывод типа затруднен), они решили немного схитрить (и это нормально), но они сделали это таким образом, чтобы не избежать этой проблемы во всех случаях (что глупо. )? - person Johannes Kuhn; 24.10.2019
comment
@JohannesKuhn Если вы хотите глубже погрузиться в эту тему - проверьте раздел JLS, в котором описывается, как определен тип функции. - person Oleksandr Pyrohov; 24.10.2019
comment
@OleksandrPyrohov это ... не имеет смысла для меня. LHS становится Iterator<CharSequence>, предположим, что это правда; тогда как получилось, что приведение RHS (_4 _...) заставляет его работать? Он должен сгенерировать такую ​​же точную ошибку. - person Eugene; 24.10.2019
comment
@Eugene Цитата из связанной ошибки: Мы прилагаем дополнительные усилия, чтобы выбрать лучший тип функции, когда используется явно типизированное лямбда-выражение ... - в случае явно типизированного лямбда-выражения процесс отличается. - person Oleksandr Pyrohov; 24.10.2019
comment
@OleksandrPyrohov да, я это читал. тем не менее, для меня это не делает его менее странным. ваш ответ является точным, без сомнения, просто меня не полностью устраивает. - person Eugene; 24.10.2019
comment
@Eugene Спасибо, я вас полностью понимаю! Я также добавлю небольшое пояснение о типе лямбды. - person Oleksandr Pyrohov; 24.10.2019

После прочтения другого ответа (который абсолютно правильный) и кофе, объяснение ошибки кажется довольно логичным.

Здесь есть два случая: явный лямбда-тип и неявный лямбда-тип. Явный тип:

Iterable<String> one = () -> Arrays.stream(arr).map(Object::toString).iterator();
Iterable<? extends CharSequence> iterable = one;

или как в примере OP:

Iterable<? extends CharSequence> iterable2 = (Iterable<String>) () -> Arrays.stream(arr).map(Object::toString).iterator();

Мы напрямую сообщаем компилятору, какой тип лямбда-выражения: Iterable<String>.

В этом случае компилятору остается только одно: посмотреть, можно ли назначить цель этому типу; довольно легко узнать и не очень сильно связан с лямбдами как таковыми.

Другой тип - это неявный тип, когда компилятор должен определить тип, и здесь все становится немного сложнее. «Сложность» связана с тем, что цель использует подстановочные знаки, поэтому может соответствовать более чем одному варианту. Может быть бесконечное количество способов (конечно, конечно, но только для доказательства точки), что лямбда может быть выведена как.

Это может начинаться, например, примерно так:

Iterator<? extends Serializable> iter = Arrays.stream(arr).map(Object::toString).iterator();

Что бы ни делалось дальше, это не удастся: CharSequence не расширяет Serializable, а String расширяет; мы не сможем присвоить Iterable<? extends CharSequence> iterable "какой-либо-вводимый-тип-с-сериализуемым-есть".

Или это может начаться с:

Iterator<? extends Comparable<? extends CharSequence>> iter = Arrays.stream(arr).map(Object::toString).iterator();

Таким образом, теоретически компилятор мог бы просто начать делать вывод, что это за тип, и проверять один за другим, может ли «определенный» выведенный тип соответствовать целевому; но, очевидно, требует много работы; таким образом не сделано.

Другой способ намного проще: «вырезать» цель и, таким образом, снизить вероятность вывода до одной. Как только цель трансформируется в:

Iterable<CharSequence> iterable...

работа, которую должен выполнять компилятор, намного проще.

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

person Eugene    schedule 24.10.2019
comment
А как это работает с Supplier<Iterator<? extends CharSequence>>? - person Johannes Kuhn; 25.10.2019
comment
@JohannesKuhn, это связано с вложенными символами подстановки, я все еще пытаюсь понять, что происходит, но здесь - последний опубликованный мной ответ по этому поводу. - person Eugene; 31.10.2019