Вывод типа Java: ссылка неоднозначна в Java 8, но не Java 7
Допустим, у нас есть 2 класса. Пустой класс Base
и подкласс этого класса Derived
.
public class Base {}
public class Derived extends Base {}
Тогда у нас есть несколько методов в другом классе:
import java.util.Collection
public class Consumer {
public void test() {
set(new Derived(), new Consumer().get());
}
public <T extends Base> T get() {
return (T) new Derived();
}
public void set(Base i, Derived b) {
System.out.println("base");
}
public void set(Derived d, Collection<? extends Consumer> o) {
System.out.println("object");
}
}
Скомпилируется и успешно выполняется в Java 7, но не компилируется в Java 8. Ошибка:
Error:(8, 9) java: reference to set is ambiguous
both method set(Base,Derived) in Consumer and
method set(Derived,java.util.Collection) in Consumer match
Почему работает Java 7, но не Java 8? Как <T extends Base>
может соответствовать коллекции?
Ответы
Ответ 1
Проблема в том, что вывод типа был улучшен. У вас есть такой метод, как
public <T extends Base> T get() {
return (T) new Derived();
}
который в основном говорит: "вызывающий может решить, какой подкласс из Base
я возвращаюсь", что является очевидной бессмыслицей. Каждый компилятор должен предоставить вам беспрепятственное предупреждение о типе (T)
здесь.
Теперь у вас есть вызов метода:
set(new Derived(), new Consumer().get());
Вспомните, что ваш метод Consumer.get()
говорит: "вызывающий может решить, что я верну". Поэтому совершенно правильно предположить, что может существовать тип, который расширяет Base
и реализует Collection
одновременно. Поэтому компилятор говорит: "Я не знаю, следует ли звонить set(Base i, Derived b)
или set(Derived d, Collection<? extends Consumer> o)
".
Вы можете "исправить" его, вызвав set(new Derived(), new Consumer().<Derived>get());
, но, чтобы проиллюстрировать безумие вашего метода, обратите внимание, что вы также можете изменить его на
public <X extends Base&Collection<Consumer>> void test() {
set(new Derived(), new Consumer().<X>get());
}
который теперь вызывается set(Derived d, Collection<? extends Consumer> o)
без предупреждения компилятора. Фактическая небезопасная операция произошла внутри метода get
.
Итак, правильным решением было бы удалить параметр типа из метода get
и объявить, что он действительно возвращает, Derived
.
Кстати, меня раздражает ваше утверждение о том, что этот код может быть скомпилирован под Java 7. Его ограниченный тип вывода с вложенными вызовами метода приводит к обработке метода get
во вложенном контексте вызова, например, возвращении Base
которые не могут быть переданы методу, ожидающему a Derived
. Как следствие, попытка скомпилировать этот код с использованием соответствующего Java-компилятора также не удастся, но по разным причинам.
Ответ 2
Ну, это не так, как если бы Java7 с радостью ее запускала. Он дает пару предупреждений перед выдачей ошибки:
[email protected]~$ javac -Xlint:unchecked -source 1.7 com/company/Main.java
warning: [options] bootstrap class path not set in conjunction with -source 1.7
com/company/Main.java:19: error: no suitable method found for set(Derived,Base)
set(new Derived(), new Consumer().get());
^
method Consumer.set(Base,Derived) is not applicable
(argument mismatch; Base cannot be converted to Derived)
method Consumer.set(Derived,Collection<? extends Consumer>) is not applicable
(argument mismatch; Base cannot be converted to Collection<? extends Consumer>)
com/company/Main.java:28: warning: [unchecked] unchecked cast
return (T) new Derived();
^
required: T
found: Derived
where T is a type-variable:
T extends Base declared in method <T>get()
Проблема заключается в следующем:
set(new Derived(), new Consumer().get());
Когда мы делаем new Consumer().get()
, если мы посмотрим на подпись get
. Он возвращает нам тип T
. Мы знаем, что T
как-то продолжается Base
. Но мы не знаем, что конкретно T
. Это может быть что угодно. Итак, если мы не можем конкретно решить, что такое T
, то как компилятор?
Один из способов сказать компилятор - это жесткое кодирование и его конкретное описание: set(new Derived(), new Consumer().<Derived>get());
.
Приведенная выше причина чрезвычайно (неоднократно преднамеренно) опасна, когда вы пытаетесь это сделать:
class NewDerived extends Base {
public String getName(){return "name";};
}
NewDerived d = new Consumer().<NewDerived>get();
System.out.println(d.getName());
В Java7 (или любой версии Java) он будет генерировать исключение во время выполнения:
Исключение в потоке "main" java.lang.ClassCastException: com.company.Derived не может быть добавлен в com.company.NewDerived
Потому что get
возвращает объект типа Derived
, но вы упоминали компилятору, что он имеет значение NewDerived
. И он не может правильно преобразовать Derived в NewDerived. Вот почему оно показывает предупреждение.
В соответствии с ошибкой, теперь мы понимаем, что не так с new Consumer().get()
. Он имеет тип something that extends base
. Выполнение set(new Derived(), new Consumer().get());
ищет метод, который принимает аргументы как Derived (or any super class of it), something that extends Base
.
Теперь оба ваших метода соответствуют первому аргументу. Согласно второму аргументу something that extends base
, снова оба имеют право на то, что что-то может быть Derived или продлить Derived или расширить Collection. Вот почему Java8 выдает ошибку.
В соответствии с Java7 его вывод типа бит слабее. Поэтому он пытается сделать что-то подобное,
Base base = new Consumer().get();
set(new Derived(), base);
И снова он не может найти правильный метод, который принимает Derived, Base
как аргументы. Следовательно, это порождает ошибку, но по разным причинам:
set(new Derived(), new Consumer().get());
^
method Consumer.set(Base,Derived) is not applicable
(argument mismatch; Base cannot be converted to Derived)
method Consumer.set(Derived,Collection<? extends Consumer>) is not applicabl e
(argument mismatch; Base cannot be converted to Collection<? extends Consu mer>)
PS: Спасибо Хольгуру, за указание неполноты моего ответа.