Вывод типа 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>
когда-нибудь совпадать с коллекцией?
2 ответа
Проблема в том, что вывод типа был улучшен. У вас есть метод, как
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
который не может быть передан методу, ожидающему Derived
, Как следствие, попытка скомпилировать этот код с использованием соответствующего компилятора Java 7 также не удастся, но по другим причинам.
Ну, это не так, как если бы Java7 рада его запустить. Это дает пару предупреждений, прежде чем выдавать ошибку:
jatin@jatin-~$ 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
, опять же, оба имеют право, так как что-то может быть получено или расширять производное или расширять коллекцию. Вот почему 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: Спасибо Хольгеру за то, что он указал на неполноту моего ответа.