Нужен производный класс для объявления дженериков Java

Я столкнулся с липкой проблемой, которую я не могу решить с помощью дженериков Java. Это немного сложно, но я не мог придумать более простой сценарий, чтобы проиллюстрировать проблему... Вот:

У меня есть класс Processor, для которого требуется Context. Существуют разные типы контекста; большинству процессоров просто нужен любой абстрактный контекст, но другим требуется определенный подкласс. Как это:

abstract class AbstractProcessor<C extends Context> {
    public abstract void process(C context);
}

class BasicProcessor extends AbstractProcessor<Context> {
    @Override
    public void process(Context context) {
        // ... //
    }
}

class SpecificProcessor extends AbstractProcessor<SpecificContext> {
    @Override
    public void process(SpecificContext context) {
        // ... //
    }
}

Хорошо, круто: процессоры могут объявить тип контекста, который им нужен, и они могут предположить, что правильный тип будет передан в process() без приведения.

Теперь у меня есть класс Dispatcher, которому принадлежит сопоставление строк с процессорами:

class Dispatcher<C extends Context> {
    Map<String, AbstractProcessor<? super C>> processorMap = new HashMap<String, AbstractProcessor<? super C>>();

    public void registerProcessor(String name, AbstractProcessor<? super C> processor) {
        processorMap.put(name, processor);
    }

    public void dispatch(String name, C context) {
        processorMap.get(name).process(context);
    }
}

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

Теперь вот проблема: я хочу, чтобы абстрактный тип Context владел Dispatcher, а производные типы Context должны иметь возможность регистрировать дополнительные процессоры. Вот самое близкое, что я мог найти к рабочему решению, но оно не работает полностью:

class Context<C extends Context> {
    private final Dispatcher<C> dispatcher = new Dispatcher<C>();

    public Context() {
        // every context supports the BasicProcessor
        registerProcessor("basic", new BasicProcessor());
    }

    protected void registerProcessor(String name, AbstractProcessor<? super C> processor) {
        dispatcher.registerProcessor(name, processor);
    }

    public void runProcessor(String name) {
        dispatcher.dispatch(name, this); // ERROR: can't cast Context<C> to C
    }
}

// this is totally weird, but it was the only way I could find to provide the
// SpecificContext type to the base class for use in the generic type
class SpecificContext extends Context<SpecificContext> {
    public SpecificContext() {
        // the SpecificContext supports the SpecificProcessor
        registerProcessor("specific", new SpecificProcessor());
    }
}

Проблема в том, что мне нужно объявить универсальный Dispatcher в базовом классе Context, но я хочу, чтобы переменная типа ссылалась на конкретный производный тип для каждого подтипа Context. Я не вижу способа сделать это без дублирования некоторого кода в каждом подклассе Context (в частности, конструкции Dispatcher и метода registerProcessor). Вот что я думаю, что я действительно хочу:

Dispatcher<MyRealClass> dispatcher = new Dispatcher<MyRealClass>();

Есть ли способ объявить общий тип объекта с типом ПОДКЛАССА объявляющего класса?

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


ОБНОВЛЕНИЕ:

Вот полный исходный код, обновленный, чтобы включить предложение Анджея Дойла использовать <C extends Context<C>>; все равно не работает, потому что Context<C> != C:

class Context<C extends Context<C>> {
    private final Dispatcher<C> dispatcher = new Dispatcher<C>();

    public Context() {
        // every context supports the BasicProcessor
        registerProcessor("basic", new BasicProcessor());
    }

    protected void registerProcessor(String name, AbstractProcessor<? super C> processor) {
        dispatcher.registerProcessor(name, processor);
    }

    public void runProcessor(String name) {
        dispatcher.dispatch(name, this); // ERROR: can't cast Context<C> to C
    }
}

// this is totally weird, but it was the only way I could find to provide the
// SpecificContext type to the base class for use in the generic type
class SpecificContext extends Context<SpecificContext> {
    public SpecificContext() {
        // the SpecificContext supports the SpecificProcessor
        registerProcessor("specific", new SpecificProcessor());
    }
}

abstract class AbstractProcessor<C extends Context<C>> {
    public abstract void process(C context);
}

class BasicProcessor extends AbstractProcessor {
    @Override
    public void process(Context context) {
        // ... //
    }
}

class SpecificProcessor extends AbstractProcessor<SpecificContext> {
    @Override
    public void process(SpecificContext context) {
        // ... //
    }
}

class Dispatcher<C extends Context<C>> {
    Map<String, AbstractProcessor<? super C>> processorMap = new HashMap<String, AbstractProcessor<? super C>>();

    public void registerProcessor(String name, AbstractProcessor<? super C> processor) {
        processorMap.put(name, processor);
    }

    public void dispatch(String name, C context) {
        processorMap.get(name).process(context);
    }
}

person joshng    schedule 06.01.2010    source источник


Ответы (1)


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

class Context<C extends Context<C>>

Обратите внимание на рекурсивное использование универсального параметра — это немного сложно понять, но это заставляет подкласс ссылаться точно на самого себя. (Честно говоря, я не совсем понимаю это, но пока вы помните, что это работает, это работает. Для справки: класс Enum определяется точно таким же образом.) раздел в FAQ Анжелики Лангер по дженерикам, который покрывает это.

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

ОБНОВЛЕНИЕ: если подумать об этом немного подробнее, то мои вышеприведенные комментарии были на правильном пути, но не совсем по делу. С саморекурсивными универсальными границами, как указано выше, вы никогда не сможете использовать фактический класс, в котором вы их определяете. На самом деле я никогда полностью не замечал этого раньше, так как по счастливой случайности или суждению я, по-видимому, всегда использовал это в правильной точке иерархии классов.

Но я нашел время, чтобы попытаться скомпилировать ваш код, и кое-что понял. На класс с этими границами никогда нельзя ссылаться как на себя, на него можно ссылаться только в контексте определенного подкласса. Рассмотрим, например, определение BasicProcessorContext кажется необобщенным в общих границах для AbstractProcessor. Чтобы предотвратить появление необработанного типа, необходимо определить класс как:

class BasicProcessor extends AbstractProcessor<Context<Context<Context<...

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

class SpecificContext extends Context<SpecificContext>

Я думаю, что здесь в основном проблема - компилятор не может гарантировать, что C и Context<C> являются одними и теми же типами, потому что у него нет необходимой логики специального регистра, чтобы понять, что эти два типа на самом деле являются эквивалентным типом (что на самом деле может быть только случай, когда цепочка подстановочных знаков бесконечна, поскольку в любом не бесконечном смысле последняя всегда на один уровень глубже, чем первая при расширении).

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

Если вам удастся найти способ заставить это работать без приведения или необходимости вставлять фиктивный подкласс, пожалуйста, сообщите об этом, но я не вижу способа сделать это, который работал бы с синтаксисом и семантикой, доступной для дженериков Java. .

person Andrzej Doyle    schedule 06.01.2010
comment
Как общий тип, Context не должен появляться в коде без параметров. - person Tom Hawtin - tackline; 06.01.2010
comment
Также BasicDispatcher необходимо параметризовать и создать для каждого Dispatcher [типа], чтобы он мог переопределять process (в Java нет автоматического переопределения с контравариантными параметрами, в отличие от ковариантных возвращаемых типов). - person Tom Hawtin - tackline; 06.01.2010
comment
Спасибо, Анджей! Это хорошее начало (и я его понимаю, вроде как). Однако я обнаружил, что это не полностью решает мою проблему: я все еще получаю несоответствие типов, когда пытаюсь передать «это» в отправку (имя, это). 'this' - это Context‹C›, а для диспетчеризации нужен C. Мы знаем, что это одно и то же, но, по-видимому, компилятор этого не знает. - person joshng; 06.01.2010
comment
@josh - Вы обновили Dispatcher до class Dispatcher<C extends Context<C>>? Если нет, то компилятор должен (правильно) выдать вам предупреждение о необработанном типе, и я полагаю, что не сразу (на этой машине нет JDK) это объединит обе стороны текущего несоответствия с Context<C>. - person Andrzej Doyle; 06.01.2010
comment
@adrzej Ага, см. обновленный вопрос выше — обновление универсального Dispatcher не решило мою проблему. Диспетчер создан с правильным типом, но метод ‹code›process‹/code› по-прежнему требует ‹code›C‹/code›, в то время как экземпляр контекста знает только, что это ‹code›Context‹C›‹. /code›... Пока что я прибег к приведению к ‹code›(C)this‹/code›. - person joshng; 07.01.2010
comment
Прекрасно, мне потребовалось слишком много времени, чтобы понять, как правильно отформатировать мой комментарий, поэтому мы застряли с материалом ‹code› выше. Вот мой комментарий в правильном формате: @adrzej Ага, см. обновленный вопрос выше — обновление общего Dispatcher не решило мою проблему. Диспетчер создан с правильным типом, но метод процесса по-прежнему требует C, в то время как экземпляр контекста знает только, что это Context<C>... Сейчас я прибегнул к приведению (C) this. - person joshng; 07.01.2010