Аннотационный процессор, похоже, нарушает универсальные шаблоны Java

Задний план

Я пытался использовать процессоры аннотаций для создания реализаций конкретных интерфейсов Factory. Эти интерфейсы выглядят следующим образом:

public interface ViewFactory<T extends View> {

    <S extends Presenter<T>> T create(S presenter);

}

и

public interface PresenterFactory<T extends View> {

    <S extends Presenter<T>> S create();

}

Обработчик аннотаций делает правильные вещи и генерирует фабрику для каждого соответствующего класса, которая аннотируется соответствующей аннотацией.

Эта проблема

Результат обработки аннотаций следующий:

public final class TestViewImplFactory implements ViewFactory {

    public final TestView create(TestPresenter presenter) {
        return new TestViewImpl(presenter);
    }
}

и соответствующий другой класс:

public final class TestPresenterImplFactory implements PresenterFactory {

    public final TestPresenter create() {
        return new TestPresenterImpl();
    }
}

Однако TestViewImplFactory не может быть скомпилирован. Сообщение об ошибке:

"Class 'TestViewImplFactory' must be declared abstract or implement abstract method create(S) in 'ViewFactory'"

Java говорит, что верно следующее:

@Override
public View create(Presenter presenter) {
    return new TestViewImpl(presenter);
}

что вообще не сработает, учитывая, что пользователь хочет знать, какой View будет возвращен и какой Presenter требуется. Я ожидал, что:

  1. либо оба автоматически сгенерированных файла неверны
  2. или оба верны

потому что они оба действительно похожи. Я ожидал, что первое окажется правдой.

Что мне здесь не хватает?


Если я добавлю Generic тип в TestViewImplFactory следующим образом:

public final class TestViewImplFactory implements ViewFactory<TestView> {

    @Override
    public <S extends Presenter<TestView>> TestView create(S presenter) {
        return new TestViewImpl(presenter);
    }
}

Возникает проблема, что параметр конструктора (который имеет тип TestPresenter) неверен. Изменение S на конкретный TestPresenter снова сделает класс не компилируемым по той же причине, что и выше.


Итак, я наткнулся на «решение», которое можно скомпилировать.

Что в основном нужно сделать, так это изменить интерфейс ViewFactory на следующее:

public interface ViewFactory<T extends View, S extends Presenter<T>> {

    T create(S presenter);

}

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

После компиляции (на этот раз со спецификацией универсального типа) вывод выглядит следующим образом:

public final class TestViewImplFactory implements ViewFactory<TestView, TestPresenter> {
    public TestViewImplFactory() {
    }

    public final TestView create(TestPresenter presenter) {
        return new TestViewImpl(presenter);
    }
}

Это может быть скомпилировано и успешно работает.

Однако это не отвечает на исходный вопрос. Почему универсальный тип явно указан в определении типа правильно, но унаследованный и, указанный в объявлении метода, неверен и не компилируется?

Чтобы быть конкретным: почему Java может наследовать один Generic автоматически (в PresenterFactory), а другие нет (в ViewFactory, в методе и в объявлении типа)?

ваш обработчик аннотаций, похоже, создает необработанные универсальные типы.

killjoy 30.08.2018 14:48

@killjoy Или, что более вероятно, неспособность отразить параметры типа в классах, которые он обрабатывает, и просто использовать стертые подписи.

chrylis -cautiouslyoptimistic- 30.08.2018 14:54

@killjoy Это не должно повлиять на результат. Если это будет проблемой, TestPresenterImplFactory также не будет компилироваться. Или я здесь не прав?

Thorben Kuck 30.08.2018 14:54

Если вы используете стертые типы, вам необходимо последовательно использовать их, брать в качестве аргумента только тип стирания и возвращать соответствующее стирание. Вы говорите, что пользователю потребуются конкретные (общие) типы, но на самом деле они ему не понадобятся, если они будут взаимодействовать только с общим интерфейсом, а не с сгенерированным типом реализации. Компилятор вставит правильные приведения.

Daniel Pryden 01.09.2018 22:03
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
20
4
908
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Почему не работает:

public interface PresenterFactory<T extends View> {
    <S extends Presenter<T>> S create();
}

Эта подпись заставляет компилятор определять S в том месте, где вызывается create(). S будет тем, что вы назначили create(), например:

FancyPresenter fp = presenterFactory.create();
SomeOtherPresenter sop = presenterFactory.create();

Это означает, что:

public TestPresenter create(){...}

не является реализацией:

<S extends Presenter<T>> S create();

но метод переопределения. Реализации метода интерфейса нет. Невозможно даже реализовать какую-либо реализацию с конкретным S. Это похоже на:

public interface ViewFactory<T extends View> {
    <S extends Presenter<T>> T create(S presenter);
}

Здесь универсальный снова выводится при вызове метода. Таким образом, реализация должна принимать каждый подтип Presenter<T>. Единственная допустимая реализация для этого:

public interface ViewFactory<T extends View> {
    T create(Presenter<T> presenter);
}

Но тип возврата зависит от параметра presenter. Это может сработать, если presenter предоставляет вам метод только для создания экземпляра T.

Почему работает другое решение:

Связывание универсального метода через тип означает, что реализация интерфейса предоставляет конкретный тип. Таким образом, для одного объекта вам не нужно предоставлять несколько разных привязок. Независимо от того, где вы вызываете метод create() для PresenterFactory<TestView, TestPresenter<TestView>>, универсальный тип возвращаемого значения привязан к TestPresenter<TestView>. Таким образом, существует возможная реализация для каждого подтипа PresenterFactory<...>.

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

Итак, учитывая подпись сообщения <S extends Presenter<T>> T create(S presenter), вы могли бы сгенерировать:

public class TestViewImplFactory implements ViewFactory<TestView> {
  @Override
  public <S extends Presenter<TestView>> TestView create(S presenter) { ... }
}

Или более минимально:

public class TestViewImplFactory implements ViewFactory<TestView> {
  @Override
  public TestView create(Presenter presenter) { ... }
}

Но тогда, с любым из них, вы не можете ограничить параметр до TestPresenter. Вам нужно будет заменить ViewFactory на что-то вроде

public interface ViewFactory<T extends View, U extends Presenter<T>>

и они реализуют ViewFactory<TestView, TestPresenter>. Вы как бы должны использовать параметры типа в реализации, чтобы достичь желаемых ограничений типа.

Другие вопросы по теме