Класс универсального типа Java не может быть назначен переменной, чей тип класса ограничен сверху универсальным супертипом.

Я использую Java 8. В моем дизайне есть несколько простых классов, которые моделируют параметры значений, такие как FloatParameter или EnumParameter<E>. A имеют общий универсальный суперкласс этих классов (GenericParameter<T>), который реализует имя параметра и его значение по умолчанию. Подклассы реализуют другие атрибуты, специфичные для них, такие как диапазон в случае FloatParameter.

Более того, я хочу работать с типами параметров независимо от их конкретного типа. Но я все еще хочу связать типы так, чтобы они были подтипами GenericParameter<T>. Для этого я создал такой метод, как process(Class<? extends GenericParameter<?>> paramType).

Теперь проблема в том, что EnumParameter.class нельзя присвоить переменной типа Class<? extends GenericParameter<?>>, а FloatParameter.class может.

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

public class GenericParameter<T> {
    protected String name;
    protected T defaultValue;
}

public class FloatGenericParameter extends GenericParameter<Float> {
    ...
}

public class TypedGenericParameter<T> extends GenericParameter<T> {
    ...
}

Class<? extends GenericParameter<?>> fgpc = FloatGenericParameter.class; // ok
Class<? extends GenericParameter<?>> tgpc = TypedGenericParameter.class; // error: incompatible types: Class<TypedGenericParameter> cannot be converted to Class<? extends GenericParameter<?>>
Class<? extends GenericParameter> tgpc2 = TypedGenericParameter.class; // warning: [rawtypes] found raw type: GenericParameter

Наконец, при использовании неуниверсального базового класса проблем нет:

public class Parameter {
    ....
}

public class FloatParameter extends Parameter {
    ...
}

public class TypedParameter<T> extends Parameter {
    ...
}

Class<? extends Parameter> fpc = FloatParameter.class; // ok
Class<? extends Parameter> tpc = TypedParameter.class; // ok

Пожалуйста, у вас есть предложения?

Я могу использовать process(Class<?> paramType) в качестве обходного пути или выполнять приведения типов, но я хотел извлечь выгоду из статической проверки типов компилятором.

РЕДАКТИРОВАТЬ:

Я хотел бы использовать приведение при регистрации фабрик, которые производят компоненты GUI для каждого типа параметра. Код выглядит так:

addParameterComponentFactory(EnumParameter.class, new ParameterComponentFactory() { ... })

В таком случае компилятор проверит добавленный тип параметра во время компиляции. Также код будет более понятным.

РЕДАКТИРОВАТЬ 2:

В настоящее время я использую предложенный подход, чтобы ввести параметр типа для метода addParameterComponentFactory. Подпись выглядит так:

public static <P extends GenericParameter<?>> addParameterComponentFactory(Class<P> clazz, ParameterComponentFactory pcf)

С помощью этого определения я могу указать TypedParameter.class (EnumParameter.class - также один параметр типа), а также получить проверку статического типа.


person Adam Jurčík    schedule 19.09.2017    source источник
comment
Пожалуйста, не могли бы вы опубликовать код, в котором вам нужно преобразовать TypedGenericParameter.class в Class<? extends GenericParameter<?>>?   -  person Ján Halaša    schedule 19.09.2017
comment
Привет, я попытался добавить пример.   -  person Adam Jurčík    schedule 19.09.2017


Ответы (1)


Давайте начнем с основных частей вашего API. У вас есть общий тип Parameter<T>, который представляет некоторый именованный параметр со значением типа T. У вас есть специализированные компоненты графического интерфейса, предназначенные для редактирования или отображения определенных типов параметров, и вы хотите иметь возможность регистрировать фабрики для создания этих компонентов.

class Parameter<T> {
    String name;
    T defaultValue;
}

class ParameterComponent<P extends Parameter> {
    void setParameter(final P p) {}
}

interface ParameterComponentFactory<P extends Parameter> {
    ParameterComponent<P> newComponent();
}

class FloatParameter extends Parameter<Float> {}
class FloatParameterComponent extends ParameterComponent<FloatParameter> {}

class EnumParameter extends Parameter<Enum> {}
class EnumParameterComponent extends ParameterComponent<EnumParameter> {}

Если я вас правильно понял, у вас возникли проблемы с выяснением того, как объявить метод, который статически обеспечивает связь между некоторым типом Parameter и фабрикой для компонентов GUI, специализированных для этого типа. Например, вы хотите иметь возможность написать это:

addComponentFactory(EnumParameter.class, EnumParameterComponent::new);    // OK
addComponentFactory(FloatParameter.class, FloatParameterComponent::new);  // OK
addComponentFactory(FloatParameter.class, EnumParameterComponent::new);   // ERROR!

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

static <P extends Parameter> void addComponentFactory(
    final Class<P> parameterType,
    final ParameterComponentFactory<? extends P> factory) { ... }

Объяснение[1]

Объясните разницу между введением нового типа P extends Parameter<?>, используемого в Class<P>, и прямым указанием Class<? extends Parameter<?>>

Это сложно, так что терпите меня. Давайте немного поговорим о подстановочных знаках, необработанных типах и преобразованиях. Рассмотрим следующее:

// Scenario 1(a)
GenericParameter raw = /* some value */;
GenericParameter<?> wc = raw;

// Scenario 1(b)
Class raw = GenericParameter.class; 
Class<?> wc = raw;

// Scenario 2
Class<GenericParameter> classOfRaw = GenericParameter.class; 
Class<GenericParameter<?>> classOfWC = classOfRaw;

Сценарии 1(a) и 1(b) компилируются по одной и той же причине: поскольку необработанный тип G может подвергнуться непроверенное преобразование в любой параметризованный тип вида G<T_1, ..., T_n>.

Сценарий 2 НЕ компилируется. Но почему?

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

Назначение от Class<String> до Class<? extends Object> допустимо. Class<String> является общим подтипом Class<? extends Object>, поскольку ? extends Object содержит String. В Сценарии 2, чтобы второе назначение было действительным, GenericParameter<?> должно содержать GenericParameter, но это не так. T не является подтипом T<?>; T является супертипом T<?>. Таким образом, согласно общим правилам создания подтипов, Class<T> не является подтипом Class<T<?>>, и такое присвоение недопустимо.

Так почему же работает следующее?

public static <P extends GenericParameter<?>> addParameterComponentFactory(
    Class<P> clazz, 
    ParameterComponentFactory pcf)

addParameterComponentFactory(EnumParameter.class, new ParameterComponentFactory() {})

В приведенном выше вызове вывод типа для P полностью управляется аргументом Class<P>. Вы передаете Class<EnumParameter>, поэтому P в этом случае привязывается к необработанному типу EnumParameter. Чтобы ограничение P extends GenericParameter<?> было удовлетворено, GenericParameter<?> должно назначаться из EnumParameter, и оно может назначаться через непроверенное преобразование, как и в сценариях 1(a) и 1(b).

[1] Это объяснение является откровенным плагаризмом и представляет собой смесь других отличных ответов на вопросы о переполнении стека, в основном от радиодеф.

person Mike Strobel    schedule 20.09.2017
comment
Ничего себе, добавление параметра типа к addComponentFactory решило проблему. Но не могли бы вы объяснить разницу между введением нового параметра типа P extends Parameter<?>, используемого в Class<P>, и прямым указанием Class<? extends Parameter<?>>? - person Adam Jurčík; 20.09.2017
comment
Я не использовал Parameter<?> в качестве верхней границы; Я использовал Parameter. Итак, на самом деле есть два вопроса: (1) зачем использовать необработанный тип в верхней границе и (2) зачем использовать общий параметр вместо подстановочных знаков? Я попытаюсь ответить как после завтрака, так и после кофеина :). - person Mike Strobel; 20.09.2017
comment
Конечно. Я знал о необработанном Parameter, но использовал Parameter<?> в качестве верхней границы, и это прекрасно работает, поскольку компилятор не жалуется на rawtype. Я мог бы прокомментировать это, извините. - person Adam Jurčík; 20.09.2017
comment
И это сработало? Не могли бы вы показать мне подпись для вашего метода добавления и объявление вашего фабричного класса? (Просто добавьте в конец вашего вопроса.) - person Mike Strobel; 20.09.2017
comment
Конечно, я добавил это как EDIT 2. - person Adam Jurčík; 20.09.2017
comment
Извините за задержку. Даже после многих лет написания кода Java и даже написания декомпилятора Java мне все еще трудно понять некоторые нюансы дженериков, особенно когда речь идет о подстановочных знаках. Надеюсь, мое дополнение будет полезным и правильным. - person Mike Strobel; 20.09.2017