Java Generics: множественное наследование в параметрах ограниченного типа

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

public class A { }
public interface I { }

это заводской метод:

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

это компилирует все в порядке.

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

A $a = F.newThing();

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

I $i = F.newThing();

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

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

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

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

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

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

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

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

--

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());
    }
}

4 ответов


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


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

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

 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 - если вы не подавляете эти предупреждения.


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

параметризованная подпись методов:

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. какой из этих двух фактических типов компилятор должен использовать в качестве аргумента типа для вашего метода?

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

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

таким образом, вы должны понимать, что параметр type здесь является просто требованием для компилятора для проверки фактического типа, который вы используете, заполнителем для фактического типа; и этот фактический тип должен существовать в конце, и компилятор будет использовать его для замены внешнего вида 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 ... }

верно?

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


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

например.

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();
    }
}