Обобщения Java: множественное наследование в параметрах ограниченного типа - PullRequest
11 голосов
/ 22 марта 2012

Я собираюсь создать фабрику, которая создает объекты определенного типа 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 и a I)

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

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

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

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

  • Что здесь не так?
  • Есть лилегко достичь желаемого поведения (т. е. присвоить переменной статического типа A или I), как это происходит при решении проблемы возвращаемого типа с помощью приведения, в заводском методе?
  • Почему назначение 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 ]

9 голосов
/ 22 марта 2012

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

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

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

7 голосов
/ 22 марта 2012

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

4 голосов
/ 22 марта 2012

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

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

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 and 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 ... }

Верно?

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

0 голосов
/ 22 марта 2012

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

например.

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