Java Generics: множественное наследование в параметрах ограниченного типа <T расширяет A & I>

Я собираюсь создать factory, который создает объекты определенного типа T, который расширяет некоторый класс A и другой интерфейс I. Однако T не должно быть известно. Вот минимальные объявления:

public class A { }
public interface I { }

Это метод factory:

public class F {
    public static <T extends A & I> T newThing() { /*...*/ }
}

Это компилирует все отлично.

Когда я пытаюсь использовать метод, следующее работает нормально:

A $a = F.newThing();

... пока это не так:

I $i = F.newThing();

Компилятор жалуется:

Связанное несоответствие: общий метод newThing() типа F не применим для аргументов(). Выведенный тип я & A не является допустимым заменой ограниченного параметра

Я не понимаю, почему. Ясно, что "newThing возвращает что-то определенного типа T, которое расширяет класс A и реализует интерфейс I". При назначении A все работает (так как T расширяет A), но присваивание я не делает (из-за чего?, ясно, что возвращаемая вещь является как A, так и I)

Также: при возврате объекта, скажем B типа class B extends A implements I, мне нужно передать его в тип возврата T, хотя B соответствует границам:

<T extends A & I> T newThing() {
    return (T) new B();
}

Однако компилятор не выдает никаких предупреждений, таких как UncheckedCast или т.п.

Таким образом, мой вопрос:

  • Что здесь происходит?
  • Легко ли достичь желаемого поведения (т.е. присваивания переменной статического типа A или I), как и при решении проблемы возвращаемого типа путем литья в методе factory?
  • Почему назначение A работает, а для меня нет?

-

EDIT: Здесь полный фрагмент кода, который полностью работает с использованием Eclipse 3.7, проект, созданный для JDK 6:

public class F {
    public static class A { }
    public static interface I { }

    private static class B extends A implements I {  }

    public static <T extends A & I> T newThing() {
        return (T) new B();
}

    public static void main(String... _) {
        A $a = F.newThing();
        // I $i = F.newThing();
    }
}

EDIT: Вот полный пример с методами и вызовами, которые работают во время выполнения:

public class F {
    public static class A {
        int methodA() {
            return 7;
        }
    }
    public static interface I {
        int methodI();
    }

    private static class B extends A implements I {
        public int methodI() {
            return 12;
        }
    }

    public static <T extends A & I> T newThing() {
        return (T) new B();
    }

    public static void main(String... _) {
        A $a = F.newThing();
        // I $i = F.newThing();
        System.out.println($a.methodA());
    }
}

Ответ 1

Это не делает то, что вы ожидаете. T extends A & I указывает, что вызывающий может указать любой тип, который расширяет A и I, и вы вернете его.

Ответ 2

Что касается второго вопроса:

Рассмотрим этот случай:

 class B extends A implements I {}
 class C extends A implements I {}

Теперь, используя вывод типа:

<T extends A & I> T newThing() {
  return (T) new B();
}

Итак, вы могли бы назвать это:

C c = F.newThing(); //T would be C here

Вы видите, что T может быть чем-либо, что расширяет A и I, вы не можете просто вернуть экземпляр B. В приведенном выше примере литой можно записать как (C)new B(). Это явно приведет к исключению, и, таким образом, компилятор выдает предупреждение: Unchecked cast from B to T - если вы не подавите эти предупреждения.

Ответ 3

Я думаю, что один из способов объяснить это - заменить параметр типа фактическим типом.

Параметрированная сигнатура методов:

public static <T extends A & B> T newThing(){
   return ...;
}

<T extends A & B> - это то, что называется параметром типа. Компилятор будет ожидать, что это значение фактически будет заменено фактическим типом (называемым аргументом типа), когда вы его используете.

В случае вашего метода фактический тип определяется с помощью вывода типа. То есть, <T extends A & B> следует заменить реальным существующим типом, который расширяет A и реализует B.

Итак, скажем, что классы C и D расширяют A и реализуют B, то если ваша подпись была такой:

public static <T extends A & B> T newThing(T obj){
   return obj;
}

Затем, по типу вывода, ваш метод будет оцениваться следующим образом:

public static C newThing(C obj){
   return obj;
}

если вы вызываете newThing(new C()).

И будет следующим образом

public static D newThing(D obj){
   return obj;
}

если вы вызываете newThing(new D()).

Это скомпилируется просто отлично!

Однако, поскольку вы фактически не предоставляете какой-либо тип для проверки вывода типа в объявлении метода, компилятор никогда не может быть уверен в том, что является фактическим типом (аргументом типа) вашего параметра типа <T extends A & B>.

Вы можете ожидать, что фактический тип - C, но могут быть тысячи разных классов, которые удовлетворяют этим критериям. Какой из них должен использовать компилятор как фактический тип аргумента вашего типа?

Скажем, что C и D - это два класса, которые расширяют A и реализуют B. Какой из этих двух фактических типов должен использовать компилятор в качестве аргумента типа для вашего метода?

Вы могли бы даже объявить аргумент типа, для которого не существует даже существующего типа, который вы можете использовать, например, что-то, что расширяет Serializable и Closable, Comparable и Appendable.

И, возможно, нет класса во всем мире, который удовлетворяет этому.

Таким образом, вы должны понимать, что параметр типа здесь является всего лишь требованием для компилятора проверить действительный тип, который вы используете, заполнителя для фактического типа; и этот фактический тип должен существовать в конце, и компилятор будет использовать его для замены появления T. Поэтому фактический тип (аргумент типа) должен быть выведен из контекста.

Так как компилятор не может с уверенностью сказать, что это фактический тип, который вы имеете в виду, в основном потому, что нет способа определить, что по типу вывода в этом случае вы вынуждены использовать свой тип, чтобы убедиться, что компилятор вам знайте, что вы делаете.

Таким образом, вы можете реализовать свой метод с помощью вывода типа следующим образом:

   public static <T extends A & B> T newThing(Class<T> t) throws Exception{
    return t.newInstance();
}

Таким образом, вы бы сказали компилятору, что такое фактический аргумент типа, который будет использоваться.

Учтите, что при генерации байт-кодов компилятор должен подставить T для реального типа. Невозможно написать метод на Java, как этот

public static A & B newThing(){ return ... }

Right?

Надеюсь, я сам объяснил! Это не просто объяснить.

Ответ 4

Самое простое решение - создать абстрактный базовый класс, который расширяет и реализует любые классы и интерфейсы, которые вы хотите, и возвращаете этот тип. Не имеет значения, что вы ограничиваете свой тип возвращаемого выражения, чтобы расширить этот базовый класс, поскольку вы уже ограничили возвращаемый тип его суперклассом.

например.

class C {}
interface I {}

abstract class BaseClass extends C implements I {}
// ^-- this line should never change. All it is telling us that we have created a
// class that combines the methods of C and I, and that concrete sub classes will
// implement the abstract methods of C and I    


class X extends BaseClass {}
class Y extends BaseClass {}

public class F {

    public static BaseClass newThing() {
        return new X();
    }


    public static void main(String[] args) {
        C c = F.newThing();
        I i = F.newThing();
    }
}