Java Generics: множественное наследование в параметрах ограниченного типа ‹T extends A & I›

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

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

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

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

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

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

person scravy    schedule 22.03.2012    source источник
comment
Не используйте $ в именах классов или переменных. Это допустимый символ, но он неофициально зарезервирован языком для предотвращения конфликтов имен.   -  person Dunes    schedule 22.03.2012
comment
Я думаю, что в вашем примере кода ошибка: A $a = F.newThing(); не работает. Вы уверены, что не объявили class A implements I { }?   -  person Andrew Spencer    schedule 22.03.2012
comment
@AndrewSpencer да, полностью. Я отредактировал полный фрагмент кода со строкой, которую я... закомментировал, но A... работает нормально. Использование Eclipse Indigo Service Release 2.   -  person scravy    schedule 22.03.2012
comment
@AndrewSpencer, как ни странно, A $a = F.newThing(); компилируется, но выдает ClassCastException во время выполнения, если B не расширяет A. Это даже компилируется с <T extends I>.   -  person Thomas    schedule 22.03.2012
comment
Есть аналогичная тема: Почему нельзя у вас есть несколько интерфейсов в ограниченном обобщенном шаблоне?   -  person Christophe Roussy    schedule 22.03.2012
comment
@Thomas, он не компилируется в Eclipse, но тогда я не доверяю компилятору Eclipse...   -  person Andrew Spencer    schedule 22.03.2012
comment
@AndrewSpencer хм, может быть, это еще одна ошибка компилятора, связанная с дженериками (по крайней мере, некоторые из них были в прошлом). В моем Eclipse (3.7.2 с JDK 1.6.0_30) он скомпилировался и запустился.   -  person Thomas    schedule 22.03.2012


Ответы (4)


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

person Louis Wasserman    schedule 22.03.2012
comment
Это совершенно не отвечает ни на один из моих вопросов. Почему тогда присваивание А работает? Есть ли исправление? - person scravy; 22.03.2012
comment
Вы должны указать какой-то явный тип B, который расширяет A и реализует I, и вы должны вернуть что-то, что расширяет этот класс. Приведение, которое вы выполняете, не будет работать на практике во время выполнения, даже если это разрешено компилятором. - person Louis Wasserman; 22.03.2012
comment
Хм хорошо. Это напоминает мне о проблеме, с которой я столкнулся в Haskell. Аналогичная причина заключалась в том, что вызывающая сторона должна указать тип возвращаемого значения (stackoverflow.com/questions/9148849). Сложный. Не могли бы вы отредактировать свой вопрос, чтобы я мог исправить голосование? Как вызывающая сторона может указать тип возвращаемого значения в Java? - person scravy; 22.03.2012
comment
Я не уверен, что ты хочешь, чтобы я изменил. Вы обнаружите, что почти невозможно заставить вызывающую программу указать тип возвращаемого значения в Java, если только один из входных данных вашего метода не является чем-то, что метод может использовать для создания объектов желаемого типа. Вы не можете предоставить фабричный метод в Java, как это было бы с ограничением класса типов в Haskell. - person Louis Wasserman; 22.03.2012
comment
Вы можете сделать это в Java: F.‹B›newThing(); Сейчас я играю с этим, странно работает этот фрагмент кода (хотя выглядит совершенно неправильно): pastebin.ca /2131130 - person scravy; 22.03.2012
comment
Вы не можете этого сделать. Ваш код не делает то, что вы думаете. Единственная причина, по которой строка I $i = F.<C>newThing() работает, заключается в том, что общая информация стирается во время выполнения, поэтому аргумент типа C фактически не важен. Если бы вы на самом деле попытались преобразовать этот объект в C, это бы не удалось. - person Louis Wasserman; 22.03.2012
comment
давайте продолжим это обсуждение в чате - person Louis Wasserman; 22.03.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 - если вы не подавляете эти предупреждения.

person Thomas    schedule 22.03.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 и 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 ... }

Правильно?

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

person Edwin Dalorzo    schedule 22.03.2012
comment
Ответ @Louis Wasserman был короче, но это дает более полное объяснение для всех, кто интересуется, почему разрешены/запрещены множественные ограничения там, где они есть. В частности, в объявлениях переменных и коллекциях. - person MandisaW; 04.12.2012
comment
Это объясняет, почему требуется явное приведение к T, но не почему Why does the assignment to A work, while to I does not? - person Kshitiz Sharma; 12.05.2015

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

eg.

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();
    }
}
person Dunes    schedule 22.03.2012