Разница в присваиваемости с вложенными подстановочными знаками в дженериках Java 7/8

Следующий код прекрасно компилируется в JDK8, но выдает ошибку несовместимые типы в JDK7.

List<List<? extends Number>> xs = Arrays.asList(Arrays.asList(0));

Согласно этому ответу, List<List<? extends Number>> не имеет отношения супертипа к List<List<Integer>>.

Что изменилось в Java 8, благодаря чему это задание сработало? Мне также трудно понять, почему это не работает в Java 7.


Оба этих утверждения компилируются без ошибок типа с использованием JDK7:

List<? extends Number> xs = Arrays.asList(0);
List<? extends List<? extends Number>> ys = Arrays.asList(Arrays.asList(0));

Мне кажется очень неинтуитивным, что оба они работают в JDK7, но исходный пример выше не работает. Все они, конечно, будут работать в JDK8. Я думаю, чтобы действительно понять, что здесь происходит, мне нужно понять, почему эти примеры являются законными в Java 7, а исходный пример - нет.


person DaoWen    schedule 12.06.2014    source источник
comment
Мне кажется, что, поскольку Arrays.asList(0) вернет List<Integer>, а Arrays.asList() из этого вернет List, элементами которого являются List<Integer>, мне кажется, что назначение List<List<? extends Number>> на самом деле будет правильным... Хотя я почти наверняка что-то неправильно понимаю   -  person awksp    schedule 12.06.2014
comment
@SotiriosDelimanolis Aaaaand оказывается, я забыл изменить строку, когда экспериментировал с ней. У меня тоже бывают неудачи.   -  person awksp    schedule 12.06.2014
comment
@user3580294 - ...мне кажется, что назначение List‹List‹? extends Number›› на самом деле было бы правильно... Это именно то, что меня смущает. Поведение Java 8 кажется мне интуитивно правильным поведением, поэтому я не понимаю, почему в Java 7 возникает ошибка типа. В чем причина этого? Видимо решили исправить в Java 8, но я не могу понять, что именно они изменили, чтобы это заработало.   -  person DaoWen    schedule 12.06.2014
comment
@DaoWen Я слышал, что в JLS были улучшения в отношении того, как работает вывод/обнаружение/проверка типов в Java 8, поэтому вполне возможно, что тогда спецификация не позволяла этого. Хотя я не могу сказать наверняка. Довольно интригующий вопрос....   -  person awksp    schedule 12.06.2014
comment
Правильная идиома для Java 7 будет List<List<? extends Number>> ys = Arrays.<List<? extends Number>asList(Arrays.asList(0));. Это показывает, что даже в Java 7 назначение является правильным и является только проблемой ограниченного вывода типа.   -  person Holger    schedule 13.06.2014


Ответы (2)


Я считаю, что это связано с контекстами вызова. и расширяющий справочник конверсия.

По сути, в этом контексте вызова тип аргумента 0 в Arrays.asList(0) может быть заключен в Integer, а затем расширен до Number. Когда это происходит, Arrays.asList(0) имеет возвращаемый тип List<Number>. С помощью того же процесса List<Number> можно преобразовать в List<? extends Number>, прежде чем использовать в качестве аргумента для внешнего Arrays.asList(..).

Это эквивалентно использованию явных аргументов типа в Java 7.

List<List<? extends Number>> xs = Arrays.<List<? extends Number>>asList(Arrays.<Number>asList(0));

В Java 7 тип выражения одинаков независимо от того, где оно используется, независимо от того, является ли оно отдельным выражением или используется в выражении присваивания.

В Java 8 появились поли-выражения, в которых тип выражения может зависеть от целевого типа выражения.

Например, в следующем выражении присваивания

List<Number> numbers = Arrays.asList(1);

Тип выражения Arrays.asList(1) — это возвращаемый тип вызываемого метода, который полностью зависит от параметра универсального типа. В этом случае этот аргумент типа будет выведен как Integer, потому что значение 1 можно преобразовать в Integer посредством преобразования упаковки (примитивы нельзя использовать с дженериками). Таким образом, тип выражения List<Integer>.

В Java 7 это выражение присваивания не будет компилироваться, потому что List<Integer> нельзя присвоить List<Number>. Это можно исправить, предоставив явный аргумент типа при вызове asList

List<Number> numbers = Arrays.<Number>asList(1);

в этом случае вызов метода ожидает аргумент Number для своего первого параметра, и значение 1 удовлетворяет этому.

В Java 8 выражением присваивания является поливыражение

Выражение вызова метода является поливыражением, если выполняются все следующие условия:

  • Вызов появляется в контексте назначения или в контексте вызова (§5.2, §5.3).

  • Если вызов квалифицирован (то есть любая форма MethodInvocation, кроме первой), то вызов опускается TypeArguments слева от идентификатора.

  • Вызываемый метод, как определено в следующих подразделах, является универсальным (§8.4.4) и имеет возвращаемый тип, в котором упоминается хотя бы один из параметров типа метода.

Будучи поливыражением, на него может влиять тип переменной, которой оно назначается. И вот что происходит. Универсальный тип Number влияет на аргумент типа, выведенный при вызове Arrays.asList(1).

Обратите внимание, как это не сработает в следующем примере.

List<Number> numbers = ...;
List<Integer> integers = ...; // integers is not a poly expression
numbers = integers; // nope

Так что это не ковариация, но в некоторых местах мы получаем некоторые из ее преимуществ.

person Sotirios Delimanolis    schedule 12.06.2014
comment
Как вы находите эти вещи похороненными в JLS? - person awksp; 12.06.2014
comment
@ user3580294 Недавно я прочитал часть статьи Анжелики Лангер, в которой говорилось о контекстах вызова (хотя я думаю, что это было до того, как они были применены в Java 8). Я вспомнил этот термин и просто поставил ctrl+f. - person Sotirios Delimanolis; 12.06.2014
comment
Я не вижу существенных различий в разделах, которые вы связали между Java 7 и 8: (Java 7) Преобразование вызовов методов и (Java 7) Расширение преобразования ссылок. Также обратите внимание, что я обновил свой вопрос еще парой примеров. - person DaoWen; 12.06.2014
comment
@DaoWen Я сейчас на работе, но я вернусь к этому позже, если у вас нет ответов, которые вы ищете. - person Sotirios Delimanolis; 12.06.2014
comment
@DaoWen Я добавил некоторые детали. Дайте мне знать, что вы думаете. - person Sotirios Delimanolis; 13.06.2014

Это довольно просто:

В Java 7 контекст вызова метода не учитывался при выводе аргументов типа. Единственное, что было учтено, где аргументы вызова метода.

В вашем случае int помещается в Integer, что дает List<Integer> как тип для внутреннего вызова, а затем List<List<Integer>> как тип для внешнего вызова. Теперь у вас есть проблема, поскольку вы хотите присвоить результат переменной типа List<List<? extends Number>>, а это просто невозможно, поскольку дженерики инвариантны, пока не используются подстановочные знаки, т. е. List<X> никогда не может быть преобразовано в List<Y>. В вашем случае X это List<Integer>, а Y это List<? extends Number>. Даже если List<? extends Number> содержит подстановочный знак, он сам не использует подстановочный знак, т. е. это не ? extends List<? extends Number>. Вот почему он не компилируется в Java 7.

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

  1. Обычно A<Y> никогда не считается подтипом какого-либо A<Z>.
  2. Однако если Z наследуется от Y, то A<Z> является подтипом A<? extends Y>.
  3. Теперь о вложенных дженериках: мы знаем, что внутренние типы (A<Z> и A<? extends Y> связаны отношением подтипа. Но если мы обернем их в другой универсальный тип, например B<A<Z>> и B<A<? extends Y>>, тогда применяется правило 1. Поскольку нет подстановочных знаков , второй B не считается подтипом первого.Если мы снова введем подстановочный знак, то они будут: B<A<Z>> действительно является подтипом B<? extends A<? extends Y>>.Но теперь обратите внимание, что внешний ? extends отсутствует в вашем примере, поэтому он не скомпилировать в Java 7.

Теперь о Java 8. Java 8 также принимает во внимание контекст вызова при выводе аргументов типа. Таким образом, Java 8 считает, что вы хотите передать результат вызовов Arrays.asList в переменную типа List<List<? extends Number>>. Поэтому он пытается найти аргументы типа, которые сделают это присваивание допустимым. Затем он делает вывод, что аргумент типа для внутреннего вызова должен быть Number, иначе присваивание будет недопустимым.

Короче говоря, Java 8 просто намного умнее, чем Java 7, при выборе аргументов типа, поскольку она также смотрит на контекст, а не только на аргументы.

person gexicide    schedule 12.06.2014
comment
А разве List<Integer> не List<? extends Number>? Я думал, что подстановочные знаки допускают такую ​​ковариацию. - person awksp; 12.06.2014
comment
List<? extends Number> xs = Arrays.asList(0); компилируется без ошибок с JDK7. Разве это не противоречит вашему ответу? (Обратите внимание, что я обновил свой вопрос этим примером.) - person DaoWen; 12.06.2014
comment
@user3580294: Да! Но подстановочные знаки не являются транзитивными. Тот факт, что List<Integer> является List<? extends Number>, не означает, что List‹List‹Integer››` является List<List<? extends Number>>. - person gexicide; 13.06.2014
comment
@DaoWen: Посмотрите на мой комментарий выше. Я уточнил свой ответ. Проблема заключается в вложенных дженериках. - person gexicide; 13.06.2014
comment
Ах, это правда. Похоже, я получил списки задом наперед. Спасибо! - person awksp; 13.06.2014
comment
Я чувствую, что Сотириос ответил на мой основной вопрос, но вы прояснили мою путаницу по поводу подстановочных знаков. Чтобы быть справедливым, я голосую за ваш ответ, но принимаю (а не голосую) за другой ответ. - person DaoWen; 13.06.2014