Вывод типа 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: Спасибо Хольгуру, за указание неполноты моего ответа.